diff --git a/AGENTS.md b/AGENTS.md index 10490b76..29fca9f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,6 +76,9 @@ with no silent fallback. (native in-process plugin injected via `OPENCODE_CONFIG_CONTENT`). - **`@cotal-ai/connector-hermes`** (`extensions/connector-hermes`): the Hermes (Nous Research) adapter; includes a Python sidecar. +- **`@cotal-ai/pi`** (`extensions/pi`): native-embed adapter — embeds `MeshAgent` in the pi + agent's own process and drives its loop straight off the inbox, with true mid-turn `steer()`. + See [docs/agent-frameworks.md](docs/agent-frameworks.md). - **`@cotal-ai/cmux`** (`extensions/cmux`): the cmux integration: a driver over the cmux CLI plus a self-registering `cmux` Runtime and `TerminalLayout` provider. - **`@cotal-ai/tmux`** (`extensions/tmux`): the tmux integration: a driver over the tmux CLI diff --git a/README.md b/README.md index 9aabe5f7..e954b6fb 100644 --- a/README.md +++ b/README.md @@ -135,14 +135,16 @@ Full index: [docs/examples.md](docs/examples.md). - - - + + + +

Claude Code

installed plugin + hooks

OpenCode

native in-process plugin

Hermes

gateway daemon + plugin

Claude Code

installed plugin + hooks

OpenCode

native in-process plugin

Hermes

gateway daemon + plugin

pi

native embed + live steer
-They attach differently but expose the same `cotal_*` tools, and all three push, so a -peer message wakes an idle agent the instant it arrives. Any agent that implements the +They attach differently but expose the same `cotal_*` tools, and all four push, so a +peer message wakes an idle agent the instant it arrives; pi additionally drives a live +turn, folding a same-scope message into an in-flight one with `steer()`. Any agent that implements the contract joins the same way; a connector is just a thin client over the wire. Want one for an agent that isn't here yet? [Vote for the next connector](https://github.com/Cotal-AI/Cotal/discussions/80). diff --git a/assets/agents/pi.svg b/assets/agents/pi.svg new file mode 100644 index 00000000..c403a4ea --- /dev/null +++ b/assets/agents/pi.svg @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/docs/agent-frameworks.md b/docs/agent-frameworks.md new file mode 100644 index 00000000..5cc4d227 --- /dev/null +++ b/docs/agent-frameworks.md @@ -0,0 +1,74 @@ +# Agent frameworks + +Besides Claude Code (see [claude-code-integration](claude-code-integration.md)), an agent +built on an embeddable agent library can join a Cotal space as a native lateral peer. The +first such adapter: + +| Extension | Framework | Language | +|---|---|---| +| `@cotal-ai/pi` | [pi coding agent](https://github.com/earendil-works/pi) | TypeScript | + +It joins the same mesh as the host-bound connectors and interoperates over the same +subjects, presence, and delivery modes. + +## The pattern: a native embedded peer + +The adapter embeds a Cotal endpoint in the framework's own process — not a separate +bridge. The shared piece is `MeshAgent` (in `@cotal-ai/connector-core`, the same runtime +behind the Claude Code and Codex connectors): it owns the NATS connection, presence, and a +stream-backed inbox, and emits `"incoming"` for each message. The adapter wires two things +around it: + +1. **Inbound drives the loop.** The peer drives straight off the inbox via an `InboxTurn` — + the inbox is the single source of truth, no parallel buffer. On `"incoming"` it surfaces + the front message, flips presence to `working`, runs the agent, and delivers the reply on + the same delivery mode (DM/anycast → DM the sender by id; channel → multicast back to that + channel), then flips to `idle`. Delivery is **ack-on-surface**: the surfaced run is + `drainInbox`-acked only once the turn completes, so a crash or restart redelivers and + nothing is lost. The loop owns delivery (always routed right, sent once); runs are + serialized; the peer answers DMs and anycasts but only replies on a channel when named + (never its own echoes — ambient chatter is ack-dropped). +2. **Mesh awareness as tools.** The model also gets read/presence tools via the framework's + tool mechanism (`defineTool()` for pi): `cotal_roster` (who's present) and `cotal_status` + (set its own status). Sending is left to the loop, so the model can't mis-route or + duplicate a reply. + +This makes the agent a real peer that wakes on traffic, like Claude Code — not a pull-only +tool caller. + +**pi** is a clean fit, since it ships as an embeddable library: `@cotal-ai/pi` embeds +`MeshAgent` alongside a pi `createAgentSession()` in one process, reads presence straight off +the session's own event stream (`agent_start` → `working`, `tool_execution_start` → the +running tool, `agent_end` → `idle`), and drives the loop with the session's own verbs — +`prompt()` to wake an idle session into a turn and `steer()` to fold a *same-scope* message +into a *live* one (true mid-turn drive, before the next LLM call; a different-scope message +waits for its own turn, so a private DM is never folded into a channel reply), with `abort()` +to interrupt. No external +channel, host process, or keystrokes — inbound `"incoming"` calls a method on the embedded +session. pi has no built-in permission gate, but in TUI mode (`PI_PEER_MODE=tui`, or the +agent file's `peerMode: tui` frontmatter hint) any pi extension that calls `ctx.ui.*` +(approval gates, prompts, selects, editors) is rendered live in the peer's tmux pane and +becomes operator-answerable per-pane (e.g. an edit-approval gate); headless peers +(unset/`headless`) run unattended (sandbox/containerize per pi's own guidance). It needs +Node ≥22.19 and a provider key in the env. + +A `Connector` extension (`buildLaunch`) lets the manager spawn it: it launches the peer via +`tsx` (dev) or `node` (built `dist/`) and forwards the launcher's identity (`COTAL_ID`), minted +creds (`COTAL_CREDS`), and the agent file (`COTAL_AGENT_FILE`). At launch it parses the agent +file and forwards the resolved model (`COTAL_MODEL` — the `cotal start --model` flag takes +precedence over the file's `model:`); each runtime path reads the file's persona body and +injects it as the agent's system prompt, so a spawned peer runs as its declared persona. +Under auth the peer authenticates as the id the manager provisioned. The connector +self-registers on import (`pi`); a composition root just imports it. + +## Running + +The pi connector has an example composition root under `examples/03-pi` (a manager that +imports `@cotal-ai/pi`). See its README to run it; in short: + +```bash +export ANTHROPIC_API_KEY=sk-... +pnpm cotal up +pnpm --filter @cotal-ai/example-03-pi manager +pnpm cotal start --name pi1 --role research --agent pi +``` diff --git a/docs/architecture.md b/docs/architecture.md index 240031ac..1cb0b532 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -170,6 +170,10 @@ Implementations stay self-contained and never import each other: the `cli` drive purely over the mesh (`start`/`stop`/`ps` control requests), so neither imports the other. Only the example wires them together. +Beyond the host-bound connectors, `@cotal-ai/pi` is a **native-embed** adapter: it embeds a +Cotal endpoint directly in the pi agent's own process and drives its run loop off the inbox — +see [agent-frameworks](agent-frameworks.md). + ## Integration surfaces (Claude Code + OpenCode) Each target agent exposes the same four surfaces. The adapters share one runtime diff --git a/examples/03-pi/README.md b/examples/03-pi/README.md new file mode 100644 index 00000000..e3e9a692 --- /dev/null +++ b/examples/03-pi/README.md @@ -0,0 +1,27 @@ +# Example 03 — pi coding-agent peers + +An agent built with the [pi coding agent](https://github.com/earendil-works/pi) joining a +Cotal space as a native lateral peer. The adapter (`@cotal-ai/pi`) embeds a Cotal endpoint +alongside a pi `createAgentSession()` in one process, reads presence off the session's own +event stream, exposes the mesh to the model as `cotal_*` tools, and drives the session on +inbound messages — `prompt()` wakes an idle session, `steer()` folds a same-scope message +into a live turn (true mid-turn drive) — so it answers DMs and anycasts, and replies on a +channel when mentioned by name. + +## Run + +```bash +pnpm cotal up # local NATS/JetStream (auth on; --open for a dev mesh) +export ANTHROPIC_API_KEY=sk-... # the peer's pi session calls the model (any pi-supported provider key works) +pnpm --filter @cotal-ai/example-03-pi manager # start the manager + +# spawn a peer (either form works) +pnpm cotal start --name pi1 --role research --agent pi +pnpm cotal watch # watch it join and reply +pnpm cotal join --name me --role human # then DM it: /dm pi1 hello +``` + +pi resolves its model and credentials via its own `AuthStorage` (falling back to provider +keys in the environment, e.g. `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`); pi has no permission +gate, so sandbox/containerize a spawned peer per pi's own guidance. Requires Node ≥22.19. See +[docs/agent-frameworks.md](../../docs/agent-frameworks.md) for how the adapter works. diff --git a/examples/03-pi/package.json b/examples/03-pi/package.json new file mode 100644 index 00000000..0f86825d --- /dev/null +++ b/examples/03-pi/package.json @@ -0,0 +1,20 @@ +{ + "name": "@cotal-ai/example-03-pi", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "type": "module", + "scripts": { + "manager": "tsx src/manager.ts", + "typecheck": "tsc -p tsconfig.json --noEmit", + "build": "tsc -p tsconfig.json" + }, + "dependencies": { + "@cotal-ai/core": "workspace:*", + "@cotal-ai/manager": "workspace:*", + "@cotal-ai/pi": "workspace:*" + }, + "devDependencies": { + "tsx": "^4.22.4" + } +} diff --git a/examples/03-pi/src/manager.ts b/examples/03-pi/src/manager.ts new file mode 100644 index 00000000..e1bce367 --- /dev/null +++ b/examples/03-pi/src/manager.ts @@ -0,0 +1,36 @@ +/** + * Composition root for example 03 (pi coding agent). Runs a manager that spawns + * pi peers into the space. Each spawn is a real pi agent session + * (extensions/pi) that embeds a Cotal endpoint and answers DMs, anycasts, and + * @-mentions on channels — waking an idle session with prompt() and folding + * same-scope traffic into a live turn with steer(). Importing the connector + * self-registers it as "pi"; we also alias it under "cotal" so a bare + * `cotal start --name x` (the default agent type) spawns one too. + */ +import { DEFAULT_SERVER, isReachable, registry, type Connector } from "@cotal-ai/core"; +import { Manager } from "@cotal-ai/manager"; +import { piConnector } from "@cotal-ai/pi"; // self-registers "pi" + +const cotalAlias: Connector = { + kind: "connector", + name: "cotal", + buildLaunch: piConnector.buildLaunch, +}; +registry.register(cotalAlias); + +const space = process.env.COTAL_SPACE?.trim() || "demo"; +const server = process.env.COTAL_SERVERS?.trim() || DEFAULT_SERVER; + +if (!(await isReachable(server))) { + console.error(`Can't reach NATS at ${server}. Run: pnpm cotal up`); + process.exit(1); +} + +const mgr = new Manager({ space, servers: server }); +await mgr.start(); +console.log(`example-03-pi manager up in space "${space}" — connectors: pi, cotal`); +console.log(`console: ${mgr.consoleUrl}`); + +process.on("SIGINT", () => void mgr.stop().then(() => process.exit(0))); +process.on("SIGTERM", () => void mgr.stop().then(() => process.exit(0))); +await new Promise(() => {}); diff --git a/examples/03-pi/tsconfig.json b/examples/03-pi/tsconfig.json new file mode 100644 index 00000000..051d08e2 --- /dev/null +++ b/examples/03-pi/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src"] +} diff --git a/extensions/connector-core/inbox-turn.smoke.ts b/extensions/connector-core/inbox-turn.smoke.ts new file mode 100644 index 00000000..78bc3510 --- /dev/null +++ b/extensions/connector-core/inbox-turn.smoke.ts @@ -0,0 +1,120 @@ +/** + * Smoke test for the InboxTurn ack-on-surface helper (no NATS/LLM needed): drives it against + * a fake inbox — including the MAX_INBOX front-eviction the real MeshAgent does — and asserts + * the surface/ack invariants the embed adapters rely on. + * + * pnpm smoke:inbox + */ +import { InboxTurn, type InboxSource } from "./src/inbox-turn.js"; +import type { InboxItem } from "./src/agent.js"; + +function item(id: string, fromId: string, kind: InboxItem["kind"] = "dm"): InboxItem { + return { id, ts: 0, fromId, fromName: fromId, kind, mentionsMe: false, text: id }; +} + +/** A fake inbox that mirrors MeshAgent: ingest force-acks + evicts from the front past `cap`, + * drainInbox acks by position, ackInbox acks by id (no-op for an absent id). */ +class FakeInbox implements InboxSource { + items: InboxItem[] = []; + acked: InboxItem[] = []; + constructor(private cap = Infinity) {} + ingest(it: InboxItem): void { + this.items.push(it); + if (this.items.length > this.cap) { + for (const ev of this.items.splice(0, this.items.length - this.cap)) this.acked.push(ev); + } + } + peekInbox(): InboxItem[] { + return [...this.items]; + } + drainInbox(limit?: number): InboxItem[] { + const n = limit && limit > 0 ? Math.min(limit, this.items.length) : this.items.length; + const taken = this.items.splice(0, n); + this.acked.push(...taken); + return taken; + } + ackInbox(ids: string[]): InboxItem[] { + const wanted = new Set(ids); + const taken: InboxItem[] = []; + this.items = this.items.filter((p) => { + if (!wanted.has(p.id)) return true; + this.acked.push(p); + taken.push(p); + return false; + }); + return taken; + } +} + +function assert(cond: boolean, msg: string): void { + if (!cond) throw new Error(`FAIL: ${msg}`); +} + +const ids = (xs: InboxItem[]): string => xs.map((x) => x.id).join(","); +const sameScope = (a: InboxItem, b: InboxItem): boolean => + a.fromId === b.fromId && a.kind === b.kind; + +// 1) drop leading non-actionable, start on the front, commit acks exactly the origin +{ + const fake = new FakeInbox(); + fake.items = [item("echo", "self"), item("b", "alice")]; + const turn = new InboxTurn(fake); + turn.drop((i) => i.fromId === "self"); + assert(ids(fake.acked) === "echo", "drop ack-drops the self echo"); + assert(turn.start()?.id === "b", "start surfaces the front actionable"); + assert(turn.count === 1, "surfaced exactly the origin"); + turn.commit(); + assert(ids(fake.acked) === "echo,b", "commit acks the origin"); + assert(fake.items.length === 0 && !turn.inFlight, "inbox drained, turn idle"); +} + +// 2) extend folds the front-contiguous same-scope run, stops at a different-scope gap +{ + const fake = new FakeInbox(); + fake.items = [item("1", "alice"), item("2", "alice"), item("3", "bob"), item("4", "alice")]; + const turn = new InboxTurn(fake); + assert(turn.start()?.id === "1", "origin = 1"); + assert(ids(turn.extend(sameScope)) === "2", "folds only contiguous same-scope #2, stops at #3"); + assert(turn.count === 2, "surfaced the 2-message run"); + turn.commit(); + assert(ids(fake.acked) === "1,2", "commit acks exactly the surfaced run [1,2]"); + assert(ids(fake.items) === "3,4", "cross-scope #3 and gapped #4 stay on the stream"); +} + +// 3) abandon acks nothing — the surfaced run redelivers +{ + const fake = new FakeInbox(); + fake.items = [item("x", "alice")]; + const turn = new InboxTurn(fake); + turn.start(); + turn.abandon(); + assert(fake.acked.length === 0, "abandon acks nothing"); + assert(ids(fake.items) === "x" && !turn.inFlight, "item stays on the stream; turn idle"); +} + +// 4) 200+ ambient burst mid-turn: the overflow evicts the in-flight prefix from the front; +// ack-by-id no-ops the evicted origin, acks the surviving folded peer, and never touches +// the newer messages that took the prefix's place +{ + const fake = new FakeInbox(200); + fake.ingest(item("origin", "alice")); + const turn = new InboxTurn(fake); + assert(turn.start()?.id === "origin", "origin surfaced"); + fake.ingest(item("peer", "alice")); + assert(ids(turn.extend(sameScope)) === "peer", "folds the same-scope peer"); + for (let i = 0; i < 199; i++) fake.ingest(item(`amb${i}`, "bob", "channel")); // 201 → evict 1 + assert( + fake.acked.some((x) => x.id === "origin") && fake.items.some((x) => x.id === "peer"), + "overflow evicted+acked the origin; the folded peer survived", + ); + const before = fake.acked.length; + turn.commit(); // ackInbox(["origin","peer"]) + assert(fake.acked.length === before + 1, "commit acks only the survivor — evicted origin no-ops"); + assert(!fake.items.some((x) => x.id === "peer"), "the survivor was acked by id"); + assert( + fake.items.length === 199 && fake.items.every((x) => x.id.startsWith("amb")), + "all 199 newer ambient messages left untouched — none mis-acked", + ); +} + +console.log("INBOX-TURN SMOKE OK ✅"); diff --git a/extensions/connector-core/src/agent.ts b/extensions/connector-core/src/agent.ts index 1a15c35f..28fbb28a 100644 --- a/extensions/connector-core/src/agent.ts +++ b/extensions/connector-core/src/agent.ts @@ -304,6 +304,24 @@ export class MeshAgent extends EventEmitter { return taken.map((p) => p.item); } + /** Ack + remove the buffered messages with these ids, wherever they sit in the inbox. An id + * that's no longer buffered — already acked, e.g. force-evicted by the MAX_INBOX overflow — is + * a harmless no-op. This lets a consumer ack exactly the messages it surfaced, immune to the + * front-eviction that shifts positions out from under {@link drainInbox}. */ + ackInbox(ids: string[]): InboxItem[] { + if (!ids.length) return []; + const wanted = new Set(ids); + const taken: InboxItem[] = []; + this.inbox = this.inbox.filter((p) => { + if (!wanted.has(p.item.id)) return true; + p.ack(); + this.markHandled(p.item.id); + taken.push(p.item); + return false; + }); + return taken; + } + /** Record an id as surfaced/handled, for {@link ingest}'s commit-aware cross-path dedup. Bounded via * two rotating windows: when the live set fills, it becomes the previous window and a fresh one * starts — so memory stays ~2× the cap while the lookup horizon never shrinks below it. */ diff --git a/extensions/connector-core/src/inbox-turn.ts b/extensions/connector-core/src/inbox-turn.ts new file mode 100644 index 00000000..771da05e --- /dev/null +++ b/extensions/connector-core/src/inbox-turn.ts @@ -0,0 +1,122 @@ +import type { InboxItem } from "./agent.js"; + +/** The slice of {@link MeshAgent} an {@link InboxTurn} drives off of. */ +export interface InboxSource { + /** Buffered messages, oldest first, without acking. */ + peekInbox(): InboxItem[]; + /** Ack + remove the front `limit` messages and return them. */ + drainInbox(limit?: number): InboxItem[]; + /** Ack + remove the messages with these ids (any position); an absent id is a no-op. */ + ackInbox(ids: string[]): InboxItem[]; +} + +/** + * Ack-on-surface delivery off a {@link MeshAgent}'s stream-backed inbox. + * + * The inbox is the single source of truth — there is no parallel buffer to drift out of + * sync. A turn *surfaces* messages by id and acks them (via `ackInbox`) only once the turn + * COMPLETES; a crash or interrupt before {@link commit} leaves them on the stream, so they + * redeliver — nothing is acked merely by being read. Acking by id (rather than by front + * position) keeps it correct even when the `MAX_INBOX` overflow force-evicts the in-flight + * prefix from the front mid-turn: those ids are already gone, so the ack no-ops them and + * never touches the newer messages that took their place. + * + * Fits both shapes the embed adapters use: + * - a one-message serialize loop: `start()` → run → `commit()`; + * - pi's same-scope steer loop: `start()` → `extend(match)`* (fold contiguous peers, e.g. + * same-scope messages, as they stream in) → `commit()` on a clean/failed finish, or + * `abandon()` on interrupt. + * + * `cotal_inbox` (where exposed) must stay on `peekInbox` so a model call can't double-drain + * what the loop already surfaced. + */ +export class InboxTurn { + private surfacedIds: string[] = []; + private _origin?: InboxItem; + + constructor(private readonly source: InboxSource) {} + + /** True while a turn holds surfaced-but-unacked messages. */ + get inFlight(): boolean { + return this.surfacedIds.length > 0; + } + + /** The message that opened the current turn (its reply scope), or undefined when idle. */ + get origin(): InboxItem | undefined { + return this._origin; + } + + /** How many messages this turn has surfaced. */ + get count(): number { + return this.surfacedIds.length; + } + + /** + * Ack-drop the leading messages matching `skip` (own echoes, ambient chatter) so they + * neither block the front nor linger to the inbox cap. Only valid with no turn in flight + * (a between-turns, synchronous front trim — no eviction can interleave). + */ + drop(skip: (item: InboxItem) => boolean): void { + if (this.surfacedIds.length) return; + const pending = this.source.peekInbox(); + let n = 0; + while (n < pending.length && skip(pending[n])) n++; + if (n) this.source.drainInbox(n); + } + + /** + * Open a turn on the front message (its origin) and surface it. Returns the origin, or + * undefined if the inbox is empty. Idempotent while a turn is in flight (returns the + * current origin). Call {@link drop} first so the front is the message you mean to answer. + */ + start(): InboxItem | undefined { + if (this.surfacedIds.length) return this._origin; + const front = this.source.peekInbox()[0]; + if (!front) return undefined; + this._origin = front; + this.surfacedIds = [front.id]; + return front; + } + + /** + * Fold the front-contiguous run of not-yet-surfaced messages that `match(item, origin)` + * into this turn, stopping at the first unsurfaced non-match (so a cross-scope message is + * left to open its own turn, preserving FIFO + scope isolation). Already-surfaced messages + * are skipped, so an overflow that evicts part of the prefix can't desync this. Returns the + * newly surfaced messages for the caller to feed in (e.g. via steer). No-op until + * {@link start}. + */ + extend(match: (item: InboxItem, origin: InboxItem) => boolean): InboxItem[] { + if (!this._origin) return []; + const surfaced = new Set(this.surfacedIds); + const run: InboxItem[] = []; + for (const item of this.source.peekInbox()) { + if (surfaced.has(item.id)) continue; // already surfaced (still buffered) — skip + if (!match(item, this._origin)) break; // first unsurfaced non-match → stop at the gap + run.push(item); + this.surfacedIds.push(item.id); + } + return run; + } + + /** + * Ack the surfaced messages by id — the sole ack site. Call on a terminal status that + * should consume them: a clean finish, or a failed/dropped turn (drop, no retry-loop). Ids + * already evicted by the overflow no-op. Do NOT call on interrupt/crash — use + * {@link abandon} so the run redelivers. + */ + commit(): void { + if (this.surfacedIds.length) this.source.ackInbox(this.surfacedIds); + this.reset(); + } + + /** End the turn without acking — the surfaced run stays on the stream and redelivers. */ + abandon(): void { + this.reset(); + } + + private reset(): void { + this.surfacedIds = []; + this._origin = undefined; + } +} diff --git a/extensions/connector-core/src/index.ts b/extensions/connector-core/src/index.ts index ee15298e..0db4edf5 100644 --- a/extensions/connector-core/src/index.ts +++ b/extensions/connector-core/src/index.ts @@ -1,5 +1,6 @@ export * from "./config.js"; export * from "./agent.js"; +export * from "./inbox-turn.js"; export * from "./runtime.js"; export * from "./launch.js"; export * from "./tool-specs.js"; diff --git a/extensions/pi/README.md b/extensions/pi/README.md new file mode 100644 index 00000000..a182b1f4 --- /dev/null +++ b/extensions/pi/README.md @@ -0,0 +1,13 @@ +# @cotal-ai/pi + +The pi adapter: a native mesh peer that embeds a Cotal endpoint inside a [pi coding +agent](https://github.com/earendil-works/pi) process and answers mesh traffic through the +agent's own loop. Reuses `MeshAgent` from +[`@cotal-ai/connector-core`](../connector-core); unlike the host-bound connectors it also +drives a *live* turn — `steer()` folds a same-scope message into an in-flight one. + +**Tier:** `extensions/`. Peer-depends [`@cotal-ai/core`](../../packages/core); self-registers on +import. + +See [docs/agent-frameworks.md](../../docs/agent-frameworks.md) for the native-embed pattern, +and the [root AGENTS.md](../../AGENTS.md) for the tier rules. diff --git a/extensions/pi/package.json b/extensions/pi/package.json new file mode 100644 index 00000000..1ed49dcd --- /dev/null +++ b/extensions/pi/package.json @@ -0,0 +1,40 @@ +{ + "name": "@cotal-ai/pi", + "version": "0.1.0", + "license": "Apache-2.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "engines": { + "node": ">=22.19.0" + }, + "scripts": { + "typecheck": "tsc -p tsconfig.json --noEmit", + "build": "tsc -p tsconfig.json", + "prepublishOnly": "pnpm run build" + }, + "dependencies": { + "@earendil-works/pi-ai": "^0.79.0", + "@earendil-works/pi-coding-agent": "^0.79.0", + "@cotal-ai/connector-core": "workspace:*", + "tsx": "^4.22.4" + }, + "peerDependencies": { + "@cotal-ai/core": "workspace:*" + }, + "devDependencies": { + "@cotal-ai/core": "workspace:*" + }, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/extensions/pi/src/connector.ts b/extensions/pi/src/connector.ts new file mode 100644 index 00000000..d1172a66 --- /dev/null +++ b/extensions/pi/src/connector.ts @@ -0,0 +1,89 @@ +import { fileURLToPath } from "node:url"; +import { loadAgentFile, registry, type Connector, type LaunchOpts, type LaunchSpec } from "@cotal-ai/core"; + +/** The peer loop runs via tsx when launched from source (.ts, dev) or via node directly + * when launched from built dist (.js). Using node for the built artifact matters: the + * manager's pty runtime sends SIGTERM to the spawned command on `cotal stop`, and a + * tsx wrapper does not forward that signal to its node child (the child is SIGKILLed + * after the grace window instead of running its shutdown handler). Running the built + * .js with node lets the peer's SIGTERM/SIGINT handlers fire for a clean mesh + * disconnect + raw-mode restore. `main` is loaded with the same extension as this + * module — `main.ts` in dev, `main.js` from built `dist/` — so the entrypoint + * resolves to a file that actually exists in either mode. */ +const ext = import.meta.url.endsWith(".ts") ? ".ts" : ".js"; +const MAIN = fileURLToPath(new URL(`./main${ext}`, import.meta.url)); +const CMD = ext === ".ts" + ? fileURLToPath(new URL("../node_modules/.bin/tsx", import.meta.url)) // dev: tsx transpiles .ts + : process.execPath; // built: node runs .js + forwards signals + +/** Provider API keys pi resolves from the environment (AuthStorage falls back to env). + * Forwarded when present so a spawned peer has credentials for its model. */ +export const PROVIDER_KEYS = [ + "ANTHROPIC_API_KEY", + "OPENAI_API_KEY", + "GEMINI_API_KEY", + "GOOGLE_API_KEY", + "GROQ_API_KEY", + "XAI_API_KEY", + "DEEPSEEK_API_KEY", + "MISTRAL_API_KEY", + "OPENROUTER_API_KEY", +]; + +/** + * The pi connector: launches an embedded Cotal peer that runs the pi coding-agent SDK + * loop and answers mesh traffic as a lateral peer. Inbound drives the loop directly + * (prompt to wake, steer to interject mid-turn). Forwards the launcher's identity + + * minted creds so the peer authenticates as `id` under auth. Parses the agent file at + * launch (so a malformed persona fails loud at `cotal start`, matching the sibling + * connectors) and forwards the resolved model (`COTAL_MODEL`); the runtime path reads + * the persona body from the agent file and injects it as the system prompt, so a + * spawned peer runs as its declared persona. `PI_PEER_MODE=tui` (or the agent file's `peerMode: tui` frontmatter hint) + * selects the interactive per-pane host (operator-answerable `ctx.ui.*` dialogs); + * unset/`headless` runs the headless embedded loop. Self-registers on import; the manager resolves it by + * agent type "pi". + */ +export const piConnector: Connector = { + kind: "connector", + name: "pi", + buildLaunch(opts: LaunchOpts): LaunchSpec { + const env: Record = { COTAL_SPACE: opts.space, COTAL_NAME: opts.name }; + if (opts.role) env.COTAL_ROLE = opts.role; + if (opts.id) env.COTAL_ID = opts.id; + if (opts.creds) env.COTAL_CREDS = opts.creds; + if (opts.servers) env.COTAL_SERVERS = opts.servers; + if (opts.configPath) { + env.COTAL_AGENT_FILE = opts.configPath; + // Parse the persona file here (not in the child) so a malformed persona + // fails loud at `cotal start` (matching the sibling connectors) and so the + // `cotal start --model` flag precedence over the file's `model:` resolves + // once, here. Forward only the short resolved model string; the persona + // body is read from disk by whichever runtime path runs (TUI/headless), so + // a large persona never rides in env (ARG_MAX ceiling on spawn). + const def = loadAgentFile(opts.configPath); + const model = opts.model ?? def.model; + if (model) env.COTAL_MODEL = model; + // Launch mode (headless embedded loop vs. interactive TUI host). The agent + // file's `peerMode:` frontmatter key is the persistent per-agent knob — an + // unmodelled scalar swept into AgentDef.meta, the same passthrough the + // OpenCode connector uses for `face:`, so core stays ignorant of it. An + // operator one-off still wins: `PI_PEER_MODE` env overrides the file and is + // forwarded verbatim (the peer validates it, see main.ts). Only "tui" is + // worth forwarding — "headless"/unset leave the var absent so the peer + // defaults to headless; an invalid file value fails loud here, not silently. + const peerMode = def.meta?.peerMode; + if (peerMode !== undefined && peerMode !== "tui" && peerMode !== "headless") + throw new Error(`agent file ${opts.configPath}: peerMode must be "tui" or "headless" (got ${JSON.stringify(peerMode)})`); + if (!process.env.PI_PEER_MODE && peerMode === "tui") env.PI_PEER_MODE = "tui"; + } + for (const key of PROVIDER_KEYS) { + const value = process.env[key]; + if (value) env[key] = value; + } + // Operator one-off override, forwarded by NAME (never via ...process.env). + if (process.env.PI_PEER_MODE) env.PI_PEER_MODE = process.env.PI_PEER_MODE; + return { command: CMD, args: [MAIN], env }; + }, +}; + +registry.register(piConnector); diff --git a/extensions/pi/src/index.ts b/extensions/pi/src/index.ts new file mode 100644 index 00000000..cdcd3295 --- /dev/null +++ b/extensions/pi/src/index.ts @@ -0,0 +1,3 @@ +export * from "./connector.js"; +export { runPiPeer } from "./peer.js"; +export { runTuiClient } from "./tui-client.js"; diff --git a/extensions/pi/src/main.ts b/extensions/pi/src/main.ts new file mode 100644 index 00000000..5f4aa897 --- /dev/null +++ b/extensions/pi/src/main.ts @@ -0,0 +1,15 @@ +import { configFromEnv } from "@cotal-ai/connector-core"; +import { runPiPeer } from "./peer.js"; +import { runTuiClient } from "./tui-client.js"; + +const config = configFromEnv(); +const mode = process.env.PI_PEER_MODE; +if (mode !== undefined && mode !== "" && mode !== "tui" && mode !== "headless") + throw new Error( + `PI_PEER_MODE must be "tui" or "headless" (got ${JSON.stringify(mode)}); unset => headless`, + ); +const entry = mode === "tui" ? runTuiClient : runPiPeer; // "headless" | unset => current path +entry(config).catch((e) => { + process.stderr.write(`[pi-peer] fatal: ${(e as Error).message}\n`); + process.exit(1); +}); diff --git a/extensions/pi/src/peer.ts b/extensions/pi/src/peer.ts new file mode 100644 index 00000000..2bf8e110 --- /dev/null +++ b/extensions/pi/src/peer.ts @@ -0,0 +1,284 @@ +import { MeshAgent, InboxTurn, configFromEnv } from "@cotal-ai/connector-core"; +import type { InboxItem, AgentConfig } from "@cotal-ai/connector-core"; +import { loadAgentFile } from "@cotal-ai/core"; +import { + AuthStorage, + DefaultResourceLoader, + ModelRegistry, + SessionManager, + SettingsManager, + createAgentSession, + defineTool, + getAgentDir, +} from "@earendil-works/pi-coding-agent"; +import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent"; +import { Type, type Api, type Model } from "@earendil-works/pi-ai"; + +function log(e: unknown): void { + process.stderr.write(`[pi-peer] ${e instanceof Error ? e.message : String(e)}\n`); +} + +/** + * Read-only / awareness tools. Replies are NOT sent by the model — the run loop delivers + * the agent's final text on the right delivery mode (see runPiPeer), so the model can't + * mis-route or duplicate a reply. These just let it see who is present and report its own + * status. Mirrors the openai-agents / vercel-ai adapters. + */ +function buildTools(mesh: MeshAgent) { + const cotal_roster = defineTool({ + name: "cotal_roster", + label: "Cotal roster", + description: "List the peers currently present on the Cotal mesh.", + parameters: Type.Object({}), + execute: async () => { + const peers = mesh.roster(); + const text = peers.length + ? peers + .map((p) => `${p.card.name}${p.card.role ? `/${p.card.role}` : ""} [${p.status}]`) + .join("\n") + : "roster is empty"; + return { content: [{ type: "text", text }], details: {} }; + }, + }); + + const cotal_status = defineTool({ + name: "cotal_status", + label: "Cotal status", + description: "Update this peer's presence status on the mesh.", + parameters: Type.Object({ + status: Type.Union([Type.Literal("idle"), Type.Literal("waiting"), Type.Literal("working")]), + activity: Type.Optional(Type.String()), + }), + execute: async (_id, params) => { + await mesh.setStatus(params.status, params.activity); + return { content: [{ type: "text", text: `status set to ${params.status}` }], details: {} }; + }, + }); + + return [cotal_roster, cotal_status]; +} + +/** Actionable = a DM, an anycast to our role, or a channel message that names us — and not + * our own echo. Pure ambient channel chatter is dropped (acked, never answered). */ +function actionable(mesh: MeshAgent, item: InboxItem): boolean { + if (item.fromId === mesh.id) return false; + return item.kind !== "channel" || item.mentionsMe; +} + +/** The audience a reply goes back to. A channel message is answered ON that channel + * (sender-independent — everyone there already saw it); a DM/anycast is answered privately + * to its sender. Two messages with the same scope can share one turn + reply; mixing scopes + * cannot (a DM folded into a channel turn would broadcast private content), so different- + * scope messages get their own scope-isolated turn. */ +function scopeKey(item: InboxItem): string { + return item.kind === "channel" && item.channel ? `channel:${item.channel}` : `dm:${item.fromId}`; +} + +/** Pull this turn's final assistant text from the agent_end payload (not the session-wide + * last message), so a turn that produced no text never re-delivers a previous reply. */ +function turnReplyText(messages: readonly unknown[]): string | undefined { + for (let i = messages.length - 1; i >= 0; i--) { + const m = messages[i] as { role?: unknown; content?: unknown }; + if (m.role !== "assistant" || !Array.isArray(m.content)) continue; + const text = m.content + .map((p) => + p && typeof p === "object" && (p as { type?: unknown }).type === "text" + ? String((p as { text?: unknown }).text ?? "") + : "", + ) + .join(""); + return text.length ? text : undefined; + } + return undefined; +} + +/** + * Resolve a persona's pinned `model:` string to a pi {@link Model} object using + * only public API (`ModelRegistry.getAvailable` + the public `Model.provider`/ + * `Model.id` fields). Exact-match only — mirrors pi's internal + * `findExactModelReferenceMatch` contract: canonical `provider/id`, then a split + * `provider`+`id`, then a bare `id` (rejected if ambiguous across providers). No + * fuzzy/partial matching: that is pi's CLI convenience (internal, not exported) + * and not appropriate for a declarative persona pin. Throws on not-found/ + * ambiguous so a bad pin fails loud at startup instead of silently falling back + * to pi's default model. (TUI mode passes the string to pi's CLI which fuzzy- + * resolves it; headless runs in-process where only exact resolution is public.) + */ +function resolveModel(ref: string, registry: ModelRegistry): Model { + const avail = registry.getAvailable(); + const r = ref.trim().toLowerCase(); + if (!r) throw new Error("pi peer: empty persona model"); + // canonical "provider/id" + let m = avail.find((x) => `${x.provider}/${x.id}`.toLowerCase() === r); + // split provider + id + if (!m && r.includes("/")) { + const slash = ref.indexOf("/"); + const provider = ref.slice(0, slash).trim().toLowerCase(); + const id = ref.slice(slash + 1).trim().toLowerCase(); + if (provider && id) m = avail.find((x) => x.provider.toLowerCase() === provider && x.id.toLowerCase() === id); + } + // bare id — must be unique across providers + if (!m) { + const ids = avail.filter((x) => x.id.toLowerCase() === r); + if (ids.length === 1) m = ids[0]; + } + if (!m) throw new Error(`pi peer: persona model "${ref}" not found or ambiguous — use provider/id`); + return m; +} + +/** + * Embed a pi coding-agent session in-process and drive it from mesh traffic. This is the + * native-embed pattern (cf. docs/agent-frameworks.md): MeshAgent owns the NATS connection, + * presence, and a stream-backed inbox; pi's loop is driven straight off that inbox via an + * {@link InboxTurn} — `prompt()` wakes an idle session on the front message, `steer()` + * interjects into a live one (true mid-turn drive, pi's distinctive capability), and presence + * is read off the session's event stream. The loop owns reply routing, so the model never + * mis-routes. + * + * Delivery is ack-on-surface: the inbox is the single source of truth (no parallel buffer); + * a turn surfaces a front-contiguous run and `commit()`s (drainInbox-acks) it only once the + * turn completes, so a crash/interrupt redelivers. Each turn is owned by one reply scope — + * a mid-turn message is steered in only when it shares that scope; a different-scope message + * stays on the stream and becomes the next turn's origin — so a private DM is never folded + * into a channel broadcast. + */ +export async function runPiPeer(config: AgentConfig = configFromEnv()): Promise { + const mesh = new MeshAgent(config); + mesh.start(); + + const authStorage = AuthStorage.create(); + const modelRegistry = ModelRegistry.create(authStorage); + + // Persona + model from the agent file. buildLaunch already parsed the file for + // launch-time validation + `--model` precedence and forwarded COTAL_MODEL (the + // resolved model string). The persona body is read here, in-process, and handed + // to the ResourceLoader as appendSystemPrompt — the in-process equivalent of + // TUI's --append-system-prompt. Construct the loader with the same defaults + // createAgentSession would have used (cwd/agentDir/settingsManager) so passing + // our own doesn't silently drop extension/skill/theme loading. + const def = process.env.COTAL_AGENT_FILE ? loadAgentFile(process.env.COTAL_AGENT_FILE) : undefined; + const cwd = process.cwd(); + const agentDir = getAgentDir(); + const settingsManager = SettingsManager.create(cwd, agentDir); + const resourceLoader = new DefaultResourceLoader({ + cwd, + agentDir, + settingsManager, + appendSystemPrompt: def?.persona ? [def.persona] : undefined, + }); + await resourceLoader.reload(); + const model = process.env.COTAL_MODEL ? resolveModel(process.env.COTAL_MODEL, modelRegistry) : undefined; + + const { session } = await createAgentSession({ + cwd, + agentDir, + authStorage, + modelRegistry, + settingsManager, + sessionManager: SessionManager.inMemory(), + customTools: buildTools(mesh), + resourceLoader, + model, + }); + + const turn = new InboxTurn(mesh); + let streaming = false; // gates steer(): only valid once the agent is actually streaming + + const setStatus = (status: "idle" | "working", activity?: string): void => { + void mesh.setStatus(status, activity).catch(() => {}); + }; + + const framed = (item: InboxItem): string => + `from ${item.fromName} via ${item.kind}: ${item.text}`; + + function deliver(to: InboxItem, text: string): void { + if (to.kind === "channel" && to.channel) void mesh.send(text, to.channel).catch(log); + else void mesh.dm(to.fromId, text).catch(log); + } + + /** Start the next turn on the front actionable message, dropping leading non-actionable + * (own echoes, ambient chatter) first. No-op while a turn is in flight. */ + function pump(): void { + if (turn.inFlight) return; + turn.drop((i) => !actionable(mesh, i)); + const origin = turn.start(); + if (!origin) { + setStatus("idle"); + return; + } + streaming = false; + void session.prompt(framed(origin)).catch(onStartError); // wake into a fresh turn + } + + /** Fold any front-contiguous, same-scope actionable messages into the live turn (mid-turn + * steer). A cross-scope or ambient message breaks contiguity and waits for its own turn. */ + function foldSameScope(): void { + if (!turn.origin || !streaming) return; + for (const item of turn.extend((i, o) => actionable(mesh, i) && scopeKey(i) === scopeKey(o))) { + void session.steer(framed(item)).catch(log); + } + } + + function onStartError(e: unknown): void { + log(e); + if (streaming) return; // already running → agent_end will complete the turn + turn.commit(); // pre-flight failure (e.g. no model/key): drop, no retry-loop + setStatus("idle"); + pump(); + } + + mesh.on("incoming", () => { + if (turn.inFlight) foldSameScope(); + else pump(); + }); + mesh.on("wake", () => { + if (!turn.inFlight) pump(); + }); + + session.subscribe((event: AgentSessionEvent) => { + switch (event.type) { + case "agent_start": + streaming = true; + setStatus("working", "thinking"); + foldSameScope(); // flush same-scope peers that landed before streaming began + break; + case "tool_execution_start": + setStatus("working", `running ${event.toolName}`); + break; + case "tool_execution_end": + setStatus("working", "thinking"); // clear the per-tool activity so it can't read stale + break; + case "agent_end": { + if (event.willRetry) break; // transient failure; a retry turn follows + const to = turn.origin; + const reply = turnReplyText(event.messages); + turn.commit(); // ack the surfaced run — clean or failed both consume (no retry-loop) + streaming = false; + if (to && reply) deliver(to, reply); + pump(); // next scope + break; + } + } + }); + + // Drain anything already buffered before the listeners were attached. + pump(); + + async function shutdown(): Promise { + try { + if (turn.inFlight) { + turn.abandon(); // leave the in-flight run on the stream → redeliver, no peer dropped + await session.abort(); + } + session.dispose(); + await mesh.stop(); + } finally { + process.exit(0); + } + } + process.on("SIGINT", () => void shutdown()); + process.on("SIGTERM", () => void shutdown()); + + // Keep alive. + await new Promise(() => {}); +} diff --git a/extensions/pi/src/rpc-frames.ts b/extensions/pi/src/rpc-frames.ts new file mode 100644 index 00000000..7e9e25ff --- /dev/null +++ b/extensions/pi/src/rpc-frames.ts @@ -0,0 +1,74 @@ +import type { ChildProcess } from "node:child_process"; +import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent"; + +/** + * pi RPC protocol frames, as used by the Cotal pi connector's TUI host. + * + * pi in `--mode rpc` reads NDJSON commands from stdin and emits NDJSON events on + * stdout. The agent activity events are raw {@link AgentSessionEvent}s (the + * same type the in-process loop subscribes to); command acknowledgements are + * `response` frames; extension UI calls (ctx.ui.confirm/select/input/editor/…) + * arrive as `extension_ui_request` frames and are answered with + * `extension_ui_response`. Shapes mirrored from the installed pi SDK + * (modes/rpc/rpc-types.ts + core/agent-session.ts) so this module has no + * dependency on pi's internal rpc-types export. + */ + +/** Commands the host writes to the child's stdin. */ +export type RpcCommand = + | { type: "prompt"; message: string; id?: string } + | { type: "steer"; message: string; id?: string } + | { type: "follow_up"; message: string; id?: string } + | { type: "abort"; id?: string } + // Extension UI answers. `confirm` -> {confirmed}; `select`/`input`/`editor` -> {value}; + // a dismissed dialog -> {cancelled: true}. All keyed by the request `id`. + | { type: "extension_ui_response"; id: string; confirmed: boolean } + | { type: "extension_ui_response"; id: string; value: string } + | { type: "extension_ui_response"; id: string; cancelled: true }; + +/** A request from a pi extension for human input (ctx.ui.*). */ +export type RpcExtensionUIRequest = + | { type: "extension_ui_request"; id: string; method: "confirm"; title: string; message: string; timeout?: number } + | { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[]; timeout?: number } + | { type: "extension_ui_request"; id: string; method: "input"; title: string; placeholder?: string; timeout?: number } + | { type: "extension_ui_request"; id: string; method: "editor"; title: string; prefill?: string } + | { type: "extension_ui_request"; id: string; method: "notify"; message: string; notifyType?: "info" | "warning" | "error" } + | { type: "extension_ui_request"; id: string; method: "setStatus"; statusKey: string; statusText: string | undefined } + | { type: "extension_ui_request"; id: string; method: "setWidget"; widgetKey: string; widgetLines: string[] | undefined; widgetPlacement?: "aboveEditor" | "belowEditor" } + | { type: "extension_ui_request"; id: string; method: "setTitle"; title: string } + | { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string }; + +/** A command acknowledgement. */ +export type RpcResponse = + | { type: "response"; command: string; success: true; id?: string; data?: unknown } + | { type: "response"; command: string; success: false; id?: string; error: string }; + +/** Everything the host reads off the child's stdout. */ +export type RpcFrame = AgentSessionEvent | RpcExtensionUIRequest | RpcResponse | { type: "extension_error"; extensionPath?: string; event?: string; error: string }; + +/** Write one command as an NDJSON line to the child's stdin. */ +export function writeRpc(child: ChildProcess, cmd: RpcCommand): void { + if (!child.stdin) throw new Error("rpc: child has no stdin"); + child.stdin.write(JSON.stringify(cmd) + "\n"); +} + +/** Read NDJSON frames from the child's stdout. Throws on a malformed line (no fallback). */ +export async function* readJsonLines(stream: NodeJS.ReadableStream): AsyncGenerator { + let buf = ""; + for await (const chunk of stream) { + buf += chunk; + let nl: number; + while ((nl = buf.indexOf("\n")) >= 0) { + const line = buf.slice(0, nl).trim(); + buf = buf.slice(nl + 1); + if (!line) continue; + let parsed: RpcFrame; + try { + parsed = JSON.parse(line) as RpcFrame; + } catch (e) { + throw new Error(`rpc: malformed stdout line: ${(e as Error).message}: ${line}`); + } + yield parsed; + } + } +} \ No newline at end of file diff --git a/extensions/pi/src/tui-client.ts b/extensions/pi/src/tui-client.ts new file mode 100644 index 00000000..57e9adf4 --- /dev/null +++ b/extensions/pi/src/tui-client.ts @@ -0,0 +1,404 @@ +import { fileURLToPath } from "node:url"; +import { spawn } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { loadAgentFile } from "@cotal-ai/core"; +import { MeshAgent, configFromEnv, launchEnv, type AgentConfig, type InboxItem } from "@cotal-ai/connector-core"; +import { writeRpc, readJsonLines } from "./rpc-frames.js"; +import { PROVIDER_KEYS } from "./connector.js"; +import { createTuiRenderer, type TuiRenderer } from "./tui-render.js"; + +// Resolve the `pi` wrapper the same way connector.ts resolves `tsx`: as the +// extension's own node_modules/.bin entry (pnpm provides it for the +// @earendil-works/pi-coding-agent dependency). Spawn PI_CLI directly, matching +// how the connector spawns TSX as the command. +const PI_CLI = fileURLToPath(new URL("../node_modules/.bin/pi", import.meta.url)); + +// ---- helpers duplicated from peer.ts. Kept inline rather than shared via a +// `peer-util.ts` because the two paths diverge in places (e.g. `actionable` +// writes `mentionsMe === true` here vs bare `mentionsMe` in peer.ts, and `framed` +// is a top-level function here vs an inline arrow there) — forcing a shared module +// would paper over those intentional differences. Small, contract-stable helpers; +// revisit the extraction only if both paths converge to byte-identical bodies. + +/** Actionable = a DM, an anycast to our role, or a channel message that names us. */ +function actionable(mesh: MeshAgent, item: InboxItem): boolean { + if (item.fromId === mesh.id) return false; + return item.kind !== "channel" || item.mentionsMe === true; +} + +/** The audience a reply goes back to. Same-scope messages share a turn. */ +function scopeKey(item: InboxItem): string { + return item.kind === "channel" && item.channel ? `channel:${item.channel}` : `dm:${item.fromId}`; +} + +function framed(item: InboxItem): string { + return `from ${item.fromName} via ${item.kind}: ${item.text}`; +} + +/** Pull this turn's final assistant text from the agent_end messages array. */ +function turnReplyText(messages: readonly unknown[]): string | undefined { + for (let i = messages.length - 1; i >= 0; i--) { + const m = messages[i] as { role?: unknown; content?: unknown }; + if (m.role !== "assistant" || !Array.isArray(m.content)) continue; + const text = m.content + .map((p) => + p && typeof p === "object" && (p as { type?: unknown }).type === "text" + ? String((p as { text?: unknown }).text ?? "") + : "", + ) + .join(""); + return text.length ? text : undefined; + } + return undefined; +} + +function log(e: unknown): void { + process.stderr.write(`[pi-peer] ${e instanceof Error ? e.message : String(e)}\n`); +} + +/** + * Interactive mesh-driven peer — the TUI launch mode selected by `PI_PEER_MODE=tui`. + * Spawns stock `pi --mode rpc` as a child and drives it from the mesh while rendering + * `extension_ui_request` dialogs locally in the pane, so any pi extension that calls + * `ctx.ui.*` (approval gates, prompts, selects, editors) becomes live and + * operator-answerable per-pane. + */ +export async function runTuiClient(config: AgentConfig = configFromEnv()): Promise { + const mesh = new MeshAgent(config); + mesh.start(); + + const renderer: TuiRenderer & { _setStreaming?: (on: boolean) => void } = createTuiRenderer(); + renderer.start(); + + // Persona body → tempfile → --append-system-prompt . pi reads the file + // contents (resource-loader resolvePromptInput: existsSync → readFileSync), so + // only the short temp path rides in argv — no ARG_MAX ceiling for a large + // persona. The body is frontmatter-stripped via loadAgentFile; the raw persona + // file has `---` metadata that must not enter the system prompt. The resolved + // model string (COTAL_MODEL) is forwarded by buildLaunch; pi's CLI resolves it + // (exact + fuzzy) the same as any `pi --model` invocation. + let personaDir: string | undefined; + const agentFile = process.env.COTAL_AGENT_FILE; + if (agentFile) { + const def = loadAgentFile(agentFile); + if (def.persona) { + personaDir = mkdtempSync(join(tmpdir(), "cotal-persona-")); + writeFileSync(join(personaDir, "persona.md"), def.persona, { mode: 0o600 }); + } + } + const cleanupPersona = (): void => { + if (personaDir) { + try { rmSync(personaDir, { recursive: true, force: true }); } catch { /* */ } + personaDir = undefined; + } + }; + + const childArgs = ["--mode", "rpc", "--no-session"]; + if (personaDir) childArgs.push("--append-system-prompt", join(personaDir, "persona.md")); + if (process.env.COTAL_MODEL) childArgs.push("--model", process.env.COTAL_MODEL); + const child = spawn(PI_CLI, childArgs, { + cwd: process.cwd(), + stdio: ["pipe", "pipe", "pipe"], + env: launchEnv({ providerKeys: PROVIDER_KEYS }), + }); + + // turn/pending/streaming/halt — the loop state. + // `InboxTurn` in `extensions/connector-core` — same API as peer.ts. + const { InboxTurn } = await import("@cotal-ai/connector-core"); + const turn = new InboxTurn(mesh); + let pending = false; // prompt sent, awaiting preflight response + let halt = false; // set on preflight failure → STOP pump + let stderrBuf = ""; + + const setMeshStatus = (status: "idle" | "working" | "waiting", activity?: string): void => { + void mesh.setStatus(status, activity).catch(log); + }; + + const deliver = (to: InboxItem, text: string): void => { + if (to.kind === "channel" && to.channel) void mesh.send(text, to.channel).catch(log); + else void mesh.dm(to.fromId, text).catch(log); + }; + + function pump(): void { + if (turn.inFlight || pending || halt) return; + turn.drop((i) => !actionable(mesh, i)); + const origin = turn.start(); + if (!origin) { + setMeshStatus("idle"); + return; + } + pending = true; + renderer.flushTrailing(); + writeRpc(child, { type: "prompt", message: framed(origin) }); + } + + function foldSameScope(): void { + if (!turn.origin || pending) return; + for (const item of turn.extend((i, o) => actionable(mesh, i) && scopeKey(i) === scopeKey(o))) { + void renderer; // mark touched + writeRpc(child, { type: "steer", message: framed(item) }); + } + } + + // ----- child stderr → transcript (line 6 of slice 3 design) ----- + child.stderr?.on("data", (d: Buffer) => { + stderrBuf += d.toString(); + }); + + // ----- child exit → renderer.stop() + parent exits loud with child code ----- + child.on("exit", (code) => { + if (stderrBuf) renderer.pushError(`child stderr: ${stderrBuf}`); + renderer.pushError(`child exit ${code}`); + cleanupPersona(); + renderer.stop(); + process.exit(code ?? 1); + }); + + // ----- Esc while streaming → writeRpc {type:"abort"} ----- + renderer.onAbort(() => { + writeRpc(child, { type: "abort" }); + }); + + // ----- mesh → pump or foldSameScope ----- + mesh.on("incoming", () => { + if (turn.inFlight) foldSameScope(); + else pump(); + }); + mesh.on("wake", () => { + if (!turn.inFlight) pump(); + }); + + // ----- the frame loop ----- + void (async () => { + try { + for await (const frame of readJsonLines(child.stdout)) { + const t = (frame as { type?: string }).type; + switch (t) { + // ---- preflight ack ---- + case "response": { + const r = frame as { command?: string; success?: boolean; error?: string }; + if (r.command === "prompt") { + if (r.success === true) { + pending = false; + // streaming will start when agent_start arrives + } else { + // Prompt preflight failed. This is NOT the no-model / no-key + // startup case: pi resolves --model only to providers with + // configured auth and falls back to a scoped model otherwise, and + // zero configured models makes pi exit(1) before rpc mode starts. + // The reachable trigger is an OAuth credential that expires + // MID-SESSION (short-lived-token providers like GitHub Copilot — + // the reason pi's getApiKey callback exists): the next prompt throws + // "authentication failed" here. Abandon so JetStream redelivers the + // message once auth is restored, surface a visible misconfigured + // status, and halt the pump so we don't burn the inbox retrying. + pending = false; + turn.abandon(); + setMeshStatus("waiting", "misconfigured: preflight failed; inbox will redeliver"); + halt = true; + renderer.pushError(`preflight failed: ${r.error ?? "(no error message)"}`); + } + } + // other command acks (steer, abort, ...) are silent. + break; + } + + // ---- agent activity (AgentEvent union: agent_start/end, turn_start/end, + // message_start/update/end, tool_execution_start/update/end) ---- + case "agent_start": { + renderer._setStreaming?.(true); + setMeshStatus("working", "thinking"); + foldSameScope(); // flush same-scope peers that landed before streaming began + break; + } + case "turn_start": { + // A new turn. Set streaming here too — some providers emit turn_start + // without a preceding agent_start, so relying on agent_start alone + // would leave streaming=false and Esc-while-streaming wouldn't abort. + renderer._setStreaming?.(true); + break; + } + case "turn_end": { + // Turn finished; agent_end (with messages + willRetry) follows and is the + // commit point. No-op here. + break; + } + case "message_start": { + // A message (user/assistant/toolResult) begins. The streamed text lands + // via message_update.text_delta; the final assistant text also appears + // in message_end + agent_end.messages. No-op. + break; + } + case "message_end": { + // Message complete. No-op (text was streamed via message_update; the + // turn's final text is committed from agent_end.messages). + break; + } + case "message_update": { + const ev = frame as { + assistantMessageEvent?: { type?: string; delta?: string }; + }; + const am = ev.assistantMessageEvent; + if (am?.type === "text_delta" && typeof am.delta === "string") { + renderer.pushAssistantText(am.delta); + } + break; + } + case "tool_execution_start": { + const ev = frame as { toolName?: string; args?: Record }; + const name = ev.toolName ?? "tool"; + const args = ev.args ?? {}; + const pathLike = + (typeof args["path"] === "string" ? args["path"] as string : undefined) ?? + (typeof args["file_path"] === "string" ? args["file_path"] as string : undefined); + renderer.pushToolEvent(name, pathLike); + setMeshStatus("working", `running ${name}`); + break; + } + case "tool_execution_update": { + // Partial tool result streaming (e.g. long bash output). No-op — the + // tool_execution_end line + status reset is enough for the pane. + break; + } + case "tool_execution_end": { + setMeshStatus("working", "thinking"); + break; + } + // ---- session metadata (AgentSessionEvent extensions) — no-op, do not + // spam the pane. These are session lifecycle signals, not turn data. ---- + case "queue_update": + case "compaction_start": + case "compaction_end": + case "session_info_changed": + case "thinking_level_changed": + case "auto_retry_start": + case "auto_retry_end": + break; + + // ---- extension UI ---- + case "extension_ui_request": { + const req = frame as { + id: string; + method: string; + title?: string; + message?: string; + options?: string[]; + placeholder?: string; + prefill?: string; + timeout?: number; + statusKey?: string; + statusText?: string | undefined; + widgetKey?: string; + widgetLines?: string[] | undefined; + widgetPlacement?: "aboveEditor" | "belowEditor"; + text?: string; + }; + switch (req.method) { + case "confirm": { + const ok = await renderer.popConfirm(req.title ?? "Confirm", req.message ?? "", { timeout: req.timeout }); + writeRpc(child, { type: "extension_ui_response", id: req.id, confirmed: ok }); + break; + } + case "select": { + const v = await renderer.popSelect(req.title ?? "Select", req.options ?? [], { timeout: req.timeout }); + writeRpc(child, { type: "extension_ui_response", id: req.id, value: v }); + break; + } + case "input": { + const v = await renderer.popInput(req.title ?? "Input", req.placeholder, { timeout: req.timeout }); + writeRpc(child, { type: "extension_ui_response", id: req.id, value: v }); + break; + } + case "editor": { + const v = await renderer.popEditor(req.title ?? "Editor", req.prefill, { timeout: req.timeout }); + // popEditor returns "" on cancel/timeout, the post-edit string otherwise. + // Extension API (per per-edit-approval.ts) treats undefined as cancel + // and string as content — but we never send undefined; we send the + // content (or "" on cancel). The rpc-side handler (rpc-mode.js) maps + // any non-cancelled response to the string value, so passing "" + // resolves to "" on the extension end (which the extension then + // can treat as "empty/cancelled" by convention). + writeRpc(child, { type: "extension_ui_response", id: req.id, value: v }); + break; + } + case "notify": { + // Fire-and-forget. Render in pane. + renderer.pushError(`[notify] ${req.message ?? ""}`.trimEnd()); + break; + } + case "setStatus": { + renderer.setStatus("working", `${req.statusKey}: ${req.statusText ?? ""}`); + break; + } + case "setWidget": + case "setTitle": + case "set_editor_text": + // Render-in-place; no response. + renderer.pushError(`[extension ui: ${req.method}] (render-only)`); + break; + default: + renderer.pushError(`[extension ui: unknown method ${req.method}]`); + } + break; + } + + case "extension_error": { + const err = frame as { error?: string; event?: string; extensionPath?: string }; + renderer.pushError(`extension error (${err.event ?? ""}): ${err.error ?? ""}`); + // do not crash; keep processing. + break; + } + + // ---- turn completion ---- + case "agent_end": { + const ev = frame as { willRetry?: boolean; messages?: readonly unknown[] }; + if (ev.willRetry) { + // Auto-retry follows; do NOT commit, do NOT pump. + break; + } + // Clean finish (or failed-but-terminal — both consume the turn). + renderer._setStreaming?.(false); + renderer.flushTrailing(); + const to = turn.origin; + const reply = turnReplyText(ev.messages ?? []); + turn.commit(); // SOLE ack site + if (to && reply) deliver(to, reply); + setMeshStatus("idle"); + pump(); + break; + } + + default: + renderer.pushError(`[unknown frame: ${JSON.stringify(frame)}]`); + } + } + } catch (e) { + renderer.pushError(`bridge read error: ${(e as Error).message}`); + renderer.stop(); + process.exit(1); + } + })(); + + // ----- crash safety: SIGINT / SIGTERM → renderer.stop() + child.kill + mesh.stop + exit 0 ----- + let shuttingDown = false; + const shutdown = async (): Promise => { + if (shuttingDown) return; + shuttingDown = true; + renderer.stop(); + cleanupPersona(); + try { child.kill("SIGTERM"); } catch { /* */ } + try { await mesh.stop(); } catch { /* */ } + process.exit(0); + }; + process.on("SIGINT", () => void shutdown()); + process.on("SIGTERM", () => void shutdown()); + process.on("exit", () => { cleanupPersona(); renderer.stop(); }); + + // Drain anything already buffered before the listeners were attached. + pump(); + + // Keep alive. + await new Promise(() => {}); +} diff --git a/extensions/pi/src/tui-render.ts b/extensions/pi/src/tui-render.ts new file mode 100644 index 00000000..eaba88ac --- /dev/null +++ b/extensions/pi/src/tui-render.ts @@ -0,0 +1,398 @@ +import { spawn as childSpawn } from "node:child_process"; +import { promises as fsp, mkdtempSync, type Dirent } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import * as readline from "node:readline"; + +/** Why this file exists: the pi connector's tui mode needs a renderer that + * depends only on Node builtins so it can ride on top of any pi release that + * keeps the rpc wire stable. No @earendil-works/pi-tui dep. No terminal- + * screen takeover — line-oriented, so a terminal crash leaves the pane + * cookable and avoids the restore-rawmode death-spiral that full-screen TUIs + * die in. + * + * Dialog block semantics: only one dialog at a time. While a dialog is open, + * the renderer ignores subsequent `popXxx` calls (returns a never-resolving + * promise) — rpc-mode's extensions block on a Promise per dialog, so child + * emits are inherently serial, but in case a fire-and-forget method arrives + * mid-dialog, render-in-place (pushError / setStatus) doesn't disturb the + * active dialog. */ + +export interface TuiRenderer { + start(): void; + stop(): void; + pushAssistantText(delta: string): void; + flushTrailing(): void; + pushToolEvent(toolName: string, pathLike?: string): void; + pushError(line: string): void; + setStatus(status: string, activity?: string): void; + popConfirm(title: string, message: string, opts?: { timeout?: number }): Promise; + popSelect(title: string, options: string[], opts?: { timeout?: number }): Promise; + popInput(title: string, placeholder?: string, opts?: { timeout?: number }): Promise; + popEditor(title: string, prefill?: string, opts?: { timeout?: number; editorOverride?: string }): Promise; + onAbort(cb: () => void): void; +} + +/** Options for the renderer. `editor` lets the platform override $EDITOR; the + * default picks $VISUAL then $EDITOR then `vi`. */ +export interface TuiRendererOptions { + editor?: string; + out?: NodeJS.WritableStream; + err?: NodeJS.WritableStream; +} + +export function createTuiRenderer(opts: TuiRendererOptions = {}): TuiRenderer { + const out = opts.out ?? process.stdout; + const err = opts.err ?? process.stderr; + + // Raw-mode + keypress state. `keyListener` is the readline-attached keypress + // handler; we keep a reference so stop() can detach. + let raw = false; + let keyListener: ((chunk: string, key: readline.Key) => void) | null = null; + let stopped = false; // stop() is called from multiple shutdown paths; guard the offline line + + // Abort-on-Esc callback: only fires OUTSIDE a dialog while STREAMING. Client + // sets this; renderer fires it; client sends {type:"abort"} to the child. + let onAbortCb: (() => void) | null = null; + + // Dialog state. While `dialogActive` is true, keypresses route to the + // dialog's resolver; outside, plain Esc while STREAMING triggers onAbortCb. + // STREAMING is owned by the client, which calls renderer.markStreaming(true/false). + let streaming = false; + let dialogActive = false; + + // Line buffer for pushAssistantText — accumulates partial lines, writes + // whole lines so the terminal isn't re-painted mid-word. + let trailing = ""; + + function writeLine(line: string): void { + out.write(line.endsWith("\n") ? line : line + "\n"); + } + + function writeRaw(s: string): void { + out.write(s); + } + + function emitError(line: string): void { + err.write(`[pi-peer] ! ${line}\n`); + } + + function setRaw(on: boolean): void { + if (on === raw) return; + if (!process.stdin.isTTY) { + // No TTY (e.g. piped CI): the renderer degrades — dialogs cannot be + // answered interactively and auto-cancel. Throwing would be louder but + // would break scripted use; degrading with a visible banner is friendlier. + raw = false; + return; + } + process.stdin.setRawMode(on); + raw = on; + } + + function attachKeypress(): void { + if (keyListener) return; + readline.emitKeypressEvents(process.stdin); + keyListener = (_chunk, key) => onKey(key); + process.stdin.on("keypress", keyListener); + } + + function detachKeypress(): void { + if (!keyListener) return; + process.stdin.off("keypress", keyListener); + keyListener = null; + } + + function onKey(key: readline.Key): void { + // Plain Esc: in-dialog cancels (resolver handles); OUT-of-dialog while + // STREAMING triggers abort; otherwise ignore (idle Esc is a no-op). + if (key.name === "escape") { + if (dialogActive) { + // dialog resolver handles this — they listen via the raw stream too. + } else if (streaming) { + const cb = onAbortCb; + if (cb) cb(); + } + return; + } + if (key.ctrl && key.name === "c") { + // SIGINT equivalent: tear the renderer down and let the client's handler exit. + // Don't call process.exit here — the client owns lifecycle. + emitError("Ctrl-C in pane (renderer will close)"); + // No-op for now; client decides. We surface the keypress. + } + // Other keys are routed to the active dialog's own listener (via raw stdin) + // — none here at the renderer level. + } + + // restore on every exit / signal path. + function restore(): void { + detachKeypress(); + if (raw) { + try { setRaw(false); } catch { /* terminal may already be gone */ } + } + } + + // Lifecycle: register once at start(); stop() unregisters (idempotent). + let installed = false; + function installLifecycle(): void { + if (installed) return; + installed = true; + process.on("exit", restore); + process.on("SIGINT", restore); + process.on("SIGTERM", restore); + } + + // ----- dialog helpers ----- + + /** Run an interactive dialog: turn on raw mode, push the prompt to the + * pane, attach a one-shot keypress handler that resolves the promise. + * `parse(key)` returns {done, value?}; done=true resolves the dialog. */ + function runDialog( + title: string, + body: () => string, + parse: (key: readline.Key) => { done: true; value: T } | { done: false }, + defaultValue: T, + timeout?: number, + ): Promise { + installLifecycle(); + setRaw(true); + dialogActive = true; + writeLine(`\n┌─ ${title}\n${body()}\n└─`); + writeRaw("> "); + return new Promise((resolve) => { + let settled = false; + const settle = (v: T): void => { + if (settled) return; + settled = true; + dialogActive = false; + writeRaw("\n"); + try { setRaw(false); } catch { /* terminal may already be gone */ } + process.stdin.off("keypress", dialogListener); + resolve(v); + }; + const dialogListener = (_chunk: string, key: readline.Key): void => { + // Esc cancels — confirm→false, select/input/editor→"" (matches rpc-side + // `createDialogPromise(opts, defaultValue, ...)` resolve-on-timeout). + if (key.name === "escape") { settle(defaultValue); return; } + if (key.ctrl && key.name === "c") { settle(defaultValue); return; } + const r = parse(key); + if (r.done) settle(r.value); + }; + process.stdin.on("keypress", dialogListener); + if (timeout !== undefined) { + setTimeout(() => settle(defaultValue), timeout); + } + }); + } + + // ----- public API ----- + + return { + start() { + installLifecycle(); + attachKeypress(); + // Keep raw mode ON for the renderer's lifetime so Esc (and other keystrokes) + // are captured by onKey, not eaten by the cooked-mode line discipline. The + // renderer owns the pane's input surface; dialogs toggle raw mode redundantly + // (idempotent). Ctrl-C in raw mode arrives as a keypress, not SIGINT — the + // supervisor's `cotal stop` is the operator's exit path for a managed peer. + setRaw(true); + writeLine("[pi-peer] renderer online"); + }, + stop() { + // Idempotent: unregisters key listener and exits raw mode. Safe to call + // from process.on("exit") / SIGINT / SIGTERM / finally. Emits a visible + // offline line so the operator sees a clean shutdown (and so the shutdown + // path is observable in tests). + if (!stopped) { writeLine("[pi-peer] renderer offline"); stopped = true; } + restore(); + }, + setStatus(_status, activity) { + // Render the status as a brief annotation line at the bottom of the pane + // (no flicker on repaint). Activity null/undefined clears it. + if (activity === undefined) writeRaw("\n\u001b[2m[working]\u001b[0m\n"); + else writeRaw(`\n\u001b[2m[working: ${activity}]\u001b[0m\n`); + }, + markStreaming(_on: boolean): void { + // The renderer's "streaming" state is set via attachStreaming() so that + // the keypress handler can route Esc to onAbortCb while STREAMING. + // Exposed on the returned object to keep the public surface narrow. + }, + // Internal hooks — exposed via private fields on the returned object below. + _streaming: (on: boolean) => { streaming = on; }, + pushAssistantText(delta) { + // Buffered line rendering: accumulate until a full line, then write. + // flushTrailing() is called by the client on agent_end to handle a final + // partial line. + const i = trailing.length ? trailing + delta : delta; + const parts = i.split("\n"); + trailing = parts.pop() ?? ""; + for (const line of parts) writeLine(line); + }, + flushTrailing() { + if (trailing) { + writeLine(trailing); + trailing = ""; + } + }, + pushToolEvent(toolName, pathLike) { + const target = pathLike ? ` ${pathLike}` : ""; + writeLine(`▸ ${toolName}${target}`); + }, + pushError(line) { + emitError(line); + }, + onAbort(cb) { + onAbortCb = cb; + }, + async popConfirm(title, message, opts) { + // y/Enter → true; n/Esc/Ctrl-C → false. + const defaultV: boolean = false; + return runDialog( + title, + () => `${message}\n(y/n, Esc to cancel)`, + (key) => { + if (key.name === "return" || key.name === "y") return { done: true, value: true }; + if (key.name === "n") return { done: true, value: false }; + return { done: false }; + }, + defaultV, + opts?.timeout, + ); + }, + async popSelect(title, options, opts) { + // arrow keys: up/down move; enter commits; esc cancels. Default value + // "" matches rpc-side `"value" in r ? r.value : undefined` for select. + let idx = 0; + const render = (): void => { + writeRaw("\u001b[2J\u001b[H"); // clear screen, home + writeLine(`┌─ ${title} (↑/↓, Enter, Esc to cancel)`); + options.forEach((o, i) => writeLine(` ${i === idx ? "▶" : " "} ${i + 1}. ${o}`)); + writeRaw("└─"); + }; + const defaultV: string = ""; // undefined if absolute blank + return new Promise((resolve) => { + installLifecycle(); + setRaw(true); + dialogActive = true; + render(); + let settled = false; + const settle = (v: string): void => { + if (settled) return; + settled = true; + dialogActive = false; + writeRaw("\n"); + try { setRaw(false); } catch { /* */ } + process.stdin.off("keypress", dialogListener); + resolve(v); + }; + const dialogListener = (_c: string, key: readline.Key): void => { + if (key.name === "escape") { settle(defaultV); return; } + if (key.ctrl && key.name === "c") { settle(defaultV); return; } + if (key.name === "up") { idx = (idx - 1 + options.length) % options.length; render(); return; } + if (key.name === "down") { idx = (idx + 1) % options.length; render(); return; } + if (key.name === "return") { settle(options[idx] ?? defaultV); return; } + }; + process.stdin.on("keypress", dialogListener); + if (opts?.timeout !== undefined) setTimeout(() => settle(defaultV), opts.timeout); + }); + }, + async popInput(title, placeholder, opts) { + // Raw-mode single-line input so Esc cancels cleanly without readline eating it. + // Handles backspace editing + echo (the cooked tty would do echo; raw doesn't, so we + // echo printable chars ourselves, render backspace as `\b \b`). + const defaultV = ""; // matches rpc-side `"value" in r ? r.value : undefined` for input + installLifecycle(); + setRaw(true); + dialogActive = true; + writeLine(`\n┌─ ${title}${placeholder ? ` (${placeholder})` : ""}`); + writeRaw("└─ > "); + let buf = ""; + const echo = (s: string): void => { + writeRaw(s.replace(/\r/g, "").replace(/\u0007/g, "")); + }; + return new Promise((resolve) => { + let settled = false; + const settle = (v: string): void => { + if (settled) return; + settled = true; + dialogActive = false; + writeRaw("\n"); + try { setRaw(false); } catch { /* */ } + process.stdin.off("keypress", dialogListener); + resolve(v); + }; + const dialogListener = (_c: string, key: readline.Key): void => { + if (key.name === "escape") { settle(defaultV); return; } + if (key.ctrl && key.name === "c") { settle(defaultV); return; } + if (key.name === "return") { writeRaw("\n"); settle(buf); return; } + if (key.name === "backspace") { + if (buf.length) { + buf = buf.slice(0, -1); + writeRaw("\b \b"); + } + return; + } + // Printable: append to buffer + echo. + if (!key.ctrl && key.sequence && key.sequence.length === 1) { + buf += key.sequence; + echo(key.sequence); + } + }; + process.stdin.on("keypress", dialogListener); + if (opts?.timeout !== undefined) setTimeout(() => settle(defaultV), opts.timeout); + }); + }, + async popEditor(title, prefill, opts) { + // Spawn $EDITOR on a tempfile; on save → return content; on :q/non-zero + // exit or Esc → empty string. The tempfile is unlinked on every exit path. + const editor = opts?.editorOverride + ?? process.env.VISUAL + ?? process.env.EDITOR + ?? "vi"; + const tmpDir = mkdtempSync(join(tmpdir(), "pi-edit-")); + const file = join(tmpDir, "edit.md"); + if (prefill) await fsp.writeFile(file, prefill, "utf8"); + else await fsp.writeFile(file, "", "utf8"); + + installLifecycle(); + // Editor runs in normal tty mode (not raw) so vi/nano/etc. work. + dialogActive = true; + writeLine(`┌─ ${title} (external editor: ${editor})`); + + const defaultV = ""; // matches rpc-side editor resolved value + const result = await new Promise((resolve) => { + const proc = childSpawn(editor, [file], { + stdio: "inherit", + env: process.env, + }); + const settle = (v: string): void => { + dialogActive = false; + resolve(v); + }; + proc.on("exit", async (code) => { + if (code !== 0) { settle(defaultV); return; } + try { + const content = await fsp.readFile(file, "utf8"); + settle(content); + } catch { + settle(defaultV); + } + }); + if (opts?.timeout !== undefined) { + setTimeout(() => { try { proc.kill("SIGTERM"); } catch { /* */ } settle(defaultV); }, opts.timeout); + } + }); + try { await fsp.rm(tmpDir, { recursive: true, force: true }); } catch { /* */ } + return result; + }, + // Force-render a status set by client (test path). + _setStreaming(on: boolean): void { streaming = on; }, + } as TuiRenderer & { _setStreaming: (on: boolean) => void }; +} + +/** Note: we mark the returned object with `_setStreaming` so the client can + * transition the streaming flag on agent_start / agent_end. The TypeScript + * intersection isn't visible externally but the runtime hook is there. + * (Avoiding an `as any` cast at call sites that need to drive Esc routing.) */ diff --git a/extensions/pi/tsconfig.json b/extensions/pi/tsconfig.json new file mode 100644 index 00000000..051d08e2 --- /dev/null +++ b/extensions/pi/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src"] +} diff --git a/package.json b/package.json index 75eef978..d91f0c7f 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "smoke": "tsx packages/core/smoke.ts", "smoke:auth": "tsx packages/core/smoke-auth.ts", "smoke:orientation": "tsx extensions/connector-core/smoke/orientation.smoke.ts", + "smoke:inbox": "tsx extensions/connector-core/inbox-turn.smoke.ts", "smoke:view": "tsx implementations/cli/smoke/view.smoke.ts", "smoke:members": "tsx packages/core/smoke/members.smoke.ts", "smoke:plane3:auth": "tsx packages/core/smoke/plane3-auth.smoke.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70f74e8c..4936ce80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,22 @@ importers: specifier: ^4.22.4 version: 4.22.4 + examples/03-pi: + dependencies: + '@cotal-ai/core': + specifier: workspace:* + version: link:../../packages/core + '@cotal-ai/manager': + specifier: workspace:* + version: link:../../implementations/manager + '@cotal-ai/pi': + specifier: workspace:* + version: link:../../extensions/pi + devDependencies: + tsx: + specifier: ^4.22.4 + version: 4.22.4 + extensions/cmux: dependencies: '@cotal-ai/core': @@ -200,6 +216,25 @@ importers: specifier: ^4.22.4 version: 4.22.4 + extensions/pi: + dependencies: + '@cotal-ai/connector-core': + specifier: workspace:* + version: link:../connector-core + '@earendil-works/pi-ai': + specifier: ^0.79.0 + version: 0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-coding-agent': + specifier: ^0.79.0 + version: 0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + tsx: + specifier: ^4.22.4 + version: 4.22.4 + devDependencies: + '@cotal-ai/core': + specifier: workspace:* + version: link:../../packages/core + extensions/tmux: dependencies: '@cotal-ai/core': @@ -329,6 +364,116 @@ packages: resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} engines: {node: '>=18'} + '@anthropic-ai/sdk@0.91.1': + resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.23': + resolution: {integrity: sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.49': + resolution: {integrity: sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.51': + resolution: {integrity: sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.56': + resolution: {integrity: sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.55': + resolution: {integrity: sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.58': + resolution: {integrity: sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.49': + resolution: {integrity: sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.55': + resolution: {integrity: sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.55': + resolution: {integrity: sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.22': + resolution: {integrity: sha512-tqPJv0dz4+O0hWGm1a6YekcMZyPhDFs/zH73Von7icaVT5n0Jqvm86typ3jRrG+qoUdPhALOnboRLTmnWQTlYQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.18': + resolution: {integrity: sha512-OHpk8YoZi3yexPq8aFt1vN1IxA2zLKvsIR5GpWYylX/ve6kQmY7wxHNSFy/D3t2apMZ16rs76Co4dJWcDyIk3A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.31': + resolution: {integrity: sha512-ps1rumU1LybSFHaW9dTDgkhCMJLVaedEY78kKSzUDDY+b9974/g6aiaYYA0U9WV0oL4CJCJrVWG+EZ/qr4or7g==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.23': + resolution: {integrity: sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.35': + resolution: {integrity: sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1048.0': + resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1074.0': + resolution: {integrity: sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.13': + resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.8': + resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.31': + resolution: {integrity: sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} @@ -396,6 +541,24 @@ packages: resolution: {integrity: sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==} engines: {node: '>= 20.12.0'} + '@earendil-works/pi-agent-core@0.79.10': + resolution: {integrity: sha512-XKxgdjhcPuyjrthCOFSgfzT3xZ1uBrJ1IMVDxci1to6hIN6BIg9J5iY8q0pGXK1DLgATLP23da+1UyZLwA360Q==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-ai@0.79.10': + resolution: {integrity: sha512-9jR23tOl0BIUdQMn70Gr72xYBpM7Xgl9Lyv7gAnU1USfkNRuYG/f/edLl+n/Dp/RafDW3JI4DF7y/GhgkORuew==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-coding-agent@0.79.10': + resolution: {integrity: sha512-YxaRhmgyDTvLDdGVbe7YzTHV80oL5mX5odg6EhGHz3w5Wu1Ix8DCw7bhtiOBLGQNFRcknia0zPmVWIj30XP1EA==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-tui@0.79.10': + resolution: {integrity: sha512-FUVOjDn1DVwM1uHD5MNYboXQrXjIDbSt+BQ3py7nQWCY62tKfxgiM1OBMxTcwRWLfSdZHUPpV0hm1loIdUJnPw==} + engines: {node: '>=22.19.0'} + '@eplightning/nats-server-darwin-arm64@2.14.0': resolution: {integrity: sha512-tBCOf4anrlTnbQimh3KSz6C+0IbdJTyfTFLcol2YfGWzfmMgsXqjfrSZvnRnqMK2hWD/TJ6eIhsv3/UlfdTzEg==} engines: {bun: '>=1', deno: '>=2', node: '>=22'} @@ -744,6 +907,15 @@ packages: cpu: [x64] os: [win32] + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + '@hono/node-server@1.19.14': resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} @@ -798,6 +970,82 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@mariozechner/clipboard-darwin-arm64@0.3.9': + resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@mariozechner/clipboard-darwin-universal@0.3.9': + resolution: {integrity: sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==} + engines: {node: '>= 10'} + os: [darwin] + + '@mariozechner/clipboard-darwin-x64@0.3.9': + resolution: {integrity: sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + resolution: {integrity: sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + resolution: {integrity: sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@mariozechner/clipboard@0.3.9': + resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} + engines: {node: '>= 10'} + + '@mistralai/mistralai@2.2.6': + resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -895,6 +1143,84 @@ packages: '@opencode-ai/sdk@1.16.2': resolution: {integrity: sha512-Z/xZ7q79dYeE0afqIk/yFEcRNGEQFcE+H8ssYivUiy+xGZ1mGwT72jpaQZKBwPn3JH4sRCu4KA2lcktBQfcOjg==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + + '@smithy/core@3.26.0': + resolution: {integrity: sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.2': + resolution: {integrity: sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.5.2': + resolution: {integrity: sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.8.2': + resolution: {integrity: sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.5.2': + resolution: {integrity: sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.15.0': + resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -913,6 +1239,9 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -933,6 +1262,10 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -982,14 +1315,23 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} @@ -998,6 +1340,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1072,6 +1417,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1093,6 +1442,10 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1101,6 +1454,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -1181,6 +1537,9 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -1210,6 +1569,10 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1225,6 +1588,10 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1249,6 +1616,14 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@7.1.5: + resolution: {integrity: sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} @@ -1273,6 +1648,14 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + google-auth-library@10.9.0: + resolution: {integrity: sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1288,14 +1671,29 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + hono@4.12.23: resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} engines: {node: '>=16.9.0'} + hosted-git-info@9.0.3: + resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==} + engines: {node: ^20.17.0 || >=22.9.0} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-id@4.2.0: resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} hasBin: true @@ -1308,6 +1706,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + indent-string@5.0.0: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} @@ -1375,6 +1777,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + jose@6.2.3: resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} @@ -1386,6 +1792,13 @@ packages: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -1400,6 +1813,12 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + kubernetes-types@1.30.0: resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} @@ -1410,10 +1829,18 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@11.5.1: resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} + marked@18.0.5: + resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1475,6 +1902,15 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true @@ -1502,6 +1938,18 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + openai@6.26.0: + resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -1521,6 +1969,10 @@ packages: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -1532,6 +1984,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + patch-console@2.0.0: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1575,6 +2030,13 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + protobufjs@7.6.4: + resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -1626,6 +2088,14 @@ packages: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1637,6 +2107,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -1647,6 +2120,11 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -1763,6 +2241,9 @@ packages: resolution: {integrity: sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==} engines: {node: '>=20'} + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-json-schema-generator@2.9.0: resolution: {integrity: sha512-NR5ZE108uiPtBHBJNGnhwoUaUx5vWTDJzDFG9YlRoqxPU76n+5FClRh92dcGgysbe1smRmYalM9Saj97GW1J4Q==} engines: {node: '>=22.0.0'} @@ -1787,6 +2268,9 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} + typebox@1.1.38: + resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1798,6 +2282,10 @@ packages: undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + undici@8.5.0: + resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} + engines: {node: '>=22.19.0'} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -1814,6 +2302,10 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1868,6 +2360,234 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + '@anthropic-ai/sdk@0.91.1(zod@4.4.3)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.4.3 + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1048.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/credential-provider-node': 3.972.58 + '@aws-sdk/eventstream-handler-node': 3.972.22 + '@aws-sdk/middleware-eventstream': 3.972.18 + '@aws-sdk/middleware-websocket': 3.972.31 + '@aws-sdk/token-providers': 3.1048.0 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.23': + dependencies: + '@aws-sdk/types': 3.973.13 + '@aws-sdk/xml-builder': 3.972.31 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.26.0 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.49': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.51': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.56': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/credential-provider-env': 3.972.49 + '@aws-sdk/credential-provider-http': 3.972.51 + '@aws-sdk/credential-provider-login': 3.972.55 + '@aws-sdk/credential-provider-process': 3.972.49 + '@aws-sdk/credential-provider-sso': 3.972.55 + '@aws-sdk/credential-provider-web-identity': 3.972.55 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.58': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.49 + '@aws-sdk/credential-provider-http': 3.972.51 + '@aws-sdk/credential-provider-ini': 3.972.56 + '@aws-sdk/credential-provider-process': 3.972.49 + '@aws-sdk/credential-provider-sso': 3.972.55 + '@aws-sdk/credential-provider-web-identity': 3.972.55 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.49': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/token-providers': 3.1074.0 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/eventstream-handler-node@3.972.22': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.18': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.31': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.23': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.35': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1048.0': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1074.0': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.13': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.8': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.31': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/runtime@7.29.7': {} '@changesets/apply-release-plan@7.1.1': @@ -2025,6 +2745,76 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 + '@earendil-works/pi-agent-core@0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-ai': 0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + ignore: 7.0.5 + typebox: 1.1.38 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-ai@0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': + dependencies: + '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) + '@aws-sdk/client-bedrock-runtime': 3.1048.0 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) + '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.0 + '@smithy/node-http-handler': 4.7.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + openai: 6.26.0(ws@8.21.0)(zod@4.4.3) + partial-json: 0.1.7 + typebox: 1.1.38 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-coding-agent@0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-agent-core': 0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.79.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-tui': 0.79.10 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cross-spawn: 7.0.6 + diff: 8.0.4 + glob: 13.0.6 + highlight.js: 10.7.3 + hosted-git-info: 9.0.3 + ignore: 7.0.5 + jiti: 2.7.0 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + semver: 7.8.0 + typebox: 1.1.38 + undici: 8.5.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.9 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-tui@0.79.10': + dependencies: + get-east-asian-width: 1.6.0 + marked: 18.0.5 + '@eplightning/nats-server-darwin-arm64@2.14.0': optional: true @@ -2199,6 +2989,19 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': + dependencies: + google-auth-library: 10.9.0 + p-retry: 4.6.2 + protobufjs: 7.6.4 + ws: 8.21.0 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@hono/node-server@1.19.14(hono@4.12.23)': dependencies: hono: 4.12.23 @@ -2253,6 +3056,62 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@mariozechner/clipboard-darwin-arm64@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-universal@0.3.9': + optional: true + + '@mariozechner/clipboard-darwin-x64@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + optional: true + + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + optional: true + + '@mariozechner/clipboard@0.3.9': + optionalDependencies: + '@mariozechner/clipboard-darwin-arm64': 0.3.9 + '@mariozechner/clipboard-darwin-universal': 0.3.9 + '@mariozechner/clipboard-darwin-x64': 0.3.9 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.9 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-musl': 0.3.9 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.9 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 + optional: true + + '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/semantic-conventions': 1.41.1 + ws: 8.21.0 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + optionalDependencies: + '@opentelemetry/api': 1.9.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.23) @@ -2349,6 +3208,86 @@ snapshots: dependencies: cross-spawn: 7.0.6 + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/semantic-conventions@1.41.1': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + + '@silvia-odwyer/photon-node@0.3.4': {} + + '@smithy/core@3.26.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.5.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.8.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.5.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/types@4.15.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@standard-schema/spec@1.1.0': {} '@types/json-schema@7.0.15': {} @@ -2367,6 +3306,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/retry@0.12.0': {} + '@types/ws@8.18.1': dependencies: '@types/node': 26.0.0 @@ -2386,6 +3327,8 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 + agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -2421,10 +3364,14 @@ snapshots: balanced-match@4.0.4: {} + base64-js@1.5.1: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 + bignumber.js@9.3.1: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -2439,6 +3386,8 @@ snapshots: transitivePeerDependencies: - supports-color + bowser@2.14.1: {} + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -2447,6 +3396,8 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-equal-constant-time@1.0.1: {} + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -2505,6 +3456,8 @@ snapshots: csstype@3.2.3: {} + data-uri-to-buffer@4.0.1: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2516,6 +3469,8 @@ snapshots: detect-libc@2.1.2: optional: true + diff@8.0.4: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -2526,6 +3481,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} effect@4.0.0-beta.74: @@ -2672,6 +3631,8 @@ snapshots: transitivePeerDependencies: - supports-color + extend@3.0.2: {} + extendable-error@0.1.7: {} fast-check@4.8.0: @@ -2704,6 +3665,11 @@ snapshots: dependencies: reusify: 1.1.0 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -2726,6 +3692,10 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -2747,6 +3717,22 @@ snapshots: function-bind@1.1.2: {} + gaxios@7.1.5: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.5 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + get-east-asian-width@1.6.0: {} get-intrinsic@1.3.0: @@ -2786,6 +3772,19 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + google-auth-library@10.9.0: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.5 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -2796,8 +3795,14 @@ snapshots: dependencies: function-bind: 1.1.2 + highlight.js@10.7.3: {} + hono@4.12.23: {} + hosted-git-info@9.0.3: + dependencies: + lru-cache: 11.5.1 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -2806,6 +3811,20 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-id@4.2.0: {} iconv-lite@0.7.2: @@ -2814,6 +3833,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + indent-string@5.0.0: {} inherits@2.0.4: {} @@ -2882,6 +3903,8 @@ snapshots: isexe@2.0.0: {} + jiti@2.7.0: {} + jose@6.2.3: {} js-yaml@3.14.2: @@ -2893,6 +3916,15 @@ snapshots: dependencies: argparse: 2.0.1 + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.7 + ts-algebra: 2.0.0 + json-schema-traverse@1.0.0: {} json-schema-typed@8.0.2: {} @@ -2903,6 +3935,17 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + kubernetes-types@1.30.0: {} locate-path@5.0.0: @@ -2911,8 +3954,12 @@ snapshots: lodash.startcase@4.4.0: {} + long@5.3.2: {} + lru-cache@11.5.1: {} + marked@18.0.5: {} + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -2964,6 +4011,14 @@ snapshots: negotiator@1.0.0: {} + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.2 @@ -2987,6 +4042,11 @@ snapshots: dependencies: mimic-fn: 2.1.0 + openai@6.26.0(ws@8.21.0)(zod@4.4.3): + optionalDependencies: + ws: 8.21.0 + zod: 4.4.3 + outdent@0.5.0: {} p-filter@2.1.0: @@ -3003,6 +4063,11 @@ snapshots: p-map@2.1.0: {} + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + p-try@2.2.0: {} package-manager-detector@0.2.11: @@ -3011,6 +4076,8 @@ snapshots: parseurl@1.3.3: {} + partial-json@0.1.7: {} + patch-console@2.0.0: {} path-exists@4.0.0: {} @@ -3036,6 +4103,26 @@ snapshots: prettier@2.8.8: {} + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + protobufjs@7.6.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 22.20.0 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -3083,6 +4170,10 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + retry@0.12.0: {} + + retry@0.13.1: {} + reusify@1.1.0: {} router@2.2.0: @@ -3099,12 +4190,16 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} scheduler@0.27.0: {} + semver@7.8.0: {} + semver@7.8.5: {} send@1.2.1: @@ -3229,6 +4324,8 @@ snapshots: toml@4.1.1: {} + ts-algebra@2.0.0: {} + ts-json-schema-generator@2.9.0: dependencies: '@types/json-schema': 7.0.15 @@ -3260,12 +4357,16 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typebox@1.1.38: {} + typescript@5.9.3: {} undici-types@6.21.0: {} undici-types@8.3.0: {} + undici@8.5.0: {} + universalify@0.1.2: {} unpipe@1.0.0: {} @@ -3274,6 +4375,8 @@ snapshots: vary@1.1.2: {} + web-streams-polyfill@3.3.3: {} + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0774c3ad..c4976edf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,5 +6,7 @@ packages: - "examples/*/*" - "bin" allowBuilds: + '@google/genai': false esbuild: true msgpackr-extract: false + protobufjs: false