@tekmidian/pai 0.41.0 → 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aibroker-client-BP74gGwD.mjs +215 -0
- package/dist/aibroker-client-BP74gGwD.mjs.map +1 -0
- package/dist/aibroker-client-C6m71p6O.mjs +3 -0
- package/dist/{auto-route-BwzmlW3W.mjs → auto-route-_5zzJz5q.mjs} +2 -2
- package/dist/{auto-route-BwzmlW3W.mjs.map → auto-route-_5zzJz5q.mjs.map} +1 -1
- package/dist/{chain-zMjCDW-R.mjs → chain-DsvSHnO9.mjs} +133 -361
- package/dist/chain-DsvSHnO9.mjs.map +1 -0
- package/dist/cli/index.mjs +12 -10
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.mjs +12 -10
- package/dist/{clusters-DRHhfli9.mjs → clusters-j0ZuNYp9.mjs} +1 -1
- package/dist/{clusters-DRHhfli9.mjs.map → clusters-j0ZuNYp9.mjs.map} +1 -1
- package/dist/{config-D1G9IFpn.mjs → config-B64vFg14.mjs} +3 -50
- package/dist/config-B64vFg14.mjs.map +1 -0
- package/dist/{config-Dl8lT4Lu.mjs → config-Bi_F6lXf.mjs} +32 -3
- package/dist/config-Bi_F6lXf.mjs.map +1 -0
- package/dist/{config-DD52BPB-.mjs → config-C_ErGddD.mjs} +1 -1
- package/dist/{context-handover-cache-C6_rs7JT.mjs → context-handover-cache--bm6Siim.mjs} +28 -9
- package/dist/context-handover-cache--bm6Siim.mjs.map +1 -0
- package/dist/daemon/index.mjs +13 -12
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BOAPyL_b.mjs +23 -0
- package/dist/{daemon-CpdVUnl_.mjs → daemon-DvXih78C.mjs} +359 -27
- package/dist/daemon-DvXih78C.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +10 -6
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/detector-C-7KRi42.mjs +5 -0
- package/dist/{detector-smpCm7ku.mjs → detector-C73Nxm94.mjs} +1 -1
- package/dist/{detector-smpCm7ku.mjs.map → detector-C73Nxm94.mjs.map} +1 -1
- package/dist/{factory-Bc5EyN54.mjs → factory-DEb9NmHJ.mjs} +7 -7
- package/dist/{factory-Bc5EyN54.mjs.map → factory-DEb9NmHJ.mjs.map} +1 -1
- package/dist/factory-Dbo8yNPi.mjs +3 -0
- package/dist/{fallback-CgQ_x4I7.mjs → fallback-8bzmg6No.mjs} +6 -5
- package/dist/fallback-8bzmg6No.mjs.map +1 -0
- package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
- package/dist/hooks/worker-proxy.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +13 -5
- package/dist/hooks/worker-status-line.mjs.map +3 -3
- package/dist/hooks/worker-supervision.mjs +593 -0
- package/dist/hooks/worker-supervision.mjs.map +7 -0
- package/dist/{indexer-backend-rohVVKrA.mjs → indexer-backend-BPTzKFZU.mjs} +1 -1
- package/dist/{indexer-backend-rohVVKrA.mjs.map → indexer-backend-BPTzKFZU.mjs.map} +1 -1
- package/dist/{ipc-client-C7AVpsyv.mjs → ipc-client-BmypMNYk.mjs} +2 -2
- package/dist/{ipc-client-C7AVpsyv.mjs.map → ipc-client-BmypMNYk.mjs.map} +1 -1
- package/dist/{kg-entity-DbOMPdF9.mjs → kg-entity-r8duqhi9.mjs} +1 -1
- package/dist/{kg-entity-DbOMPdF9.mjs.map → kg-entity-r8duqhi9.mjs.map} +1 -1
- package/dist/{latent-ideas-iNp5s55R.mjs → latent-ideas-CtSca4qG.mjs} +2 -2
- package/dist/{latent-ideas-iNp5s55R.mjs.map → latent-ideas-CtSca4qG.mjs.map} +1 -1
- package/dist/{link-boost-NNzADHX_.mjs → link-boost-Bucnz3n8.mjs} +1 -1
- package/dist/{link-boost-NNzADHX_.mjs.map → link-boost-Bucnz3n8.mjs.map} +1 -1
- package/dist/main-resolver-CqppP_Bw.mjs +7 -0
- package/dist/{main-resolver-CM1IHbuu.mjs → main-resolver-Dip2VLB5.mjs} +4 -214
- package/dist/main-resolver-Dip2VLB5.mjs.map +1 -0
- package/dist/{neighborhood-AGL7oXQ2.mjs → neighborhood-CM7Jgb51.mjs} +1 -1
- package/dist/{neighborhood-AGL7oXQ2.mjs.map → neighborhood-CM7Jgb51.mjs.map} +1 -1
- package/dist/{note-context-BHwYpSFf.mjs → note-context-DCwE2A-M.mjs} +1 -1
- package/dist/{note-context-BHwYpSFf.mjs.map → note-context-DCwE2A-M.mjs.map} +1 -1
- package/dist/{planner-DsmMRIZR.mjs → planner-ogy-Zy4O.mjs} +6 -5
- package/dist/{planner-DsmMRIZR.mjs.map → planner-ogy-Zy4O.mjs.map} +1 -1
- package/dist/{postgres-Dwxe290H.mjs → postgres-CtOzfoQU.mjs} +1 -1
- package/dist/{postgres-Dwxe290H.mjs.map → postgres-CtOzfoQU.mjs.map} +1 -1
- package/dist/{program-Bed-Pd-8.mjs → program-B7N5OIJR.mjs} +36 -33
- package/dist/{program-Bed-Pd-8.mjs.map → program-B7N5OIJR.mjs.map} +1 -1
- package/dist/query-feedback-DGoVRaWD.mjs +3 -0
- package/dist/{query-feedback-DZfjI34w.mjs → query-feedback-Dqz-HC__.mjs} +1 -1
- package/dist/{query-feedback-DZfjI34w.mjs.map → query-feedback-Dqz-HC__.mjs.map} +1 -1
- package/dist/router-BTIGZR2_.mjs +3 -0
- package/dist/{router-DHracTUC.mjs → router-DD6D4j31.mjs} +1 -1
- package/dist/{router-DHracTUC.mjs.map → router-DD6D4j31.mjs.map} +1 -1
- package/dist/runtime-paths-rni52zHX.mjs +50 -0
- package/dist/runtime-paths-rni52zHX.mjs.map +1 -0
- package/dist/scope-CfYnjy5R.mjs +360 -0
- package/dist/scope-CfYnjy5R.mjs.map +1 -0
- package/dist/{server-BOuAOj9b.mjs → server-DvN-IFIV.mjs} +42 -3
- package/dist/server-DvN-IFIV.mjs.map +1 -0
- package/dist/skills/Worker/SKILL.md +3 -0
- package/dist/{sources-ju6AA7lL.mjs → sources-CS43CpuS.mjs} +1 -1
- package/dist/{sources-ju6AA7lL.mjs.map → sources-CS43CpuS.mjs.map} +1 -1
- package/dist/{sqlite-kFZeWCJ2.mjs → sqlite-Cq3gJcq9.mjs} +1 -1
- package/dist/{sqlite-kFZeWCJ2.mjs.map → sqlite-Cq3gJcq9.mjs.map} +1 -1
- package/dist/{state-ahFAbD8b.mjs → state-BTa10SOM.mjs} +1 -1
- package/dist/{state-ahFAbD8b.mjs.map → state-BTa10SOM.mjs.map} +1 -1
- package/dist/{state-DH_mp5Xe.mjs → state-RXepvbnO.mjs} +1 -1
- package/dist/{themes-DqhNUUQk.mjs → themes-B7h9YDiy.mjs} +1 -1
- package/dist/{themes-DqhNUUQk.mjs.map → themes-B7h9YDiy.mjs.map} +1 -1
- package/dist/{tools-COna9vZR.mjs → tools-BW3NLMPk.mjs} +1 -1
- package/dist/{tools-CMlHa9lZ.mjs → tools-LlzcV_eK.mjs} +14 -14
- package/dist/{tools-CMlHa9lZ.mjs.map → tools-LlzcV_eK.mjs.map} +1 -1
- package/dist/{trace-DTP6k4B_.mjs → trace-u4hT-pLD.mjs} +1 -1
- package/dist/{trace-DTP6k4B_.mjs.map → trace-u4hT-pLD.mjs.map} +1 -1
- package/dist/{vault-indexer-YMH22mLU.mjs → vault-indexer-mata9S7b.mjs} +1 -1
- package/dist/{vault-indexer-YMH22mLU.mjs.map → vault-indexer-mata9S7b.mjs.map} +1 -1
- package/dist/work-queue-worker-19r0cpH4.mjs +14 -0
- package/dist/{work-queue-worker-B3nl8yyn.mjs → work-queue-worker-DXh_FnT5.mjs} +4 -4
- package/dist/{work-queue-worker-B3nl8yyn.mjs.map → work-queue-worker-DXh_FnT5.mjs.map} +1 -1
- package/dist/{zettelkasten-DDvKRm2V.mjs → zettelkasten-F0D_XfQ4.mjs} +3 -3
- package/dist/{zettelkasten-DDvKRm2V.mjs.map → zettelkasten-F0D_XfQ4.mjs.map} +1 -1
- package/docs/commands/worker.md +1 -1
- package/docs/merge-demo.md +1 -0
- package/docs/worker.md +40 -0
- package/package.json +1 -1
- package/plugins/productivity/hooks/hooks.json +5 -0
- package/src/hooks/ts/user-prompt/worker-supervision.ts +123 -0
- package/dist/chain-zMjCDW-R.mjs.map +0 -1
- package/dist/config-D1G9IFpn.mjs.map +0 -1
- package/dist/config-Dl8lT4Lu.mjs.map +0 -1
- package/dist/context-handover-cache-C6_rs7JT.mjs.map +0 -1
- package/dist/daemon-BSL9sXxm.mjs +0 -22
- package/dist/daemon-CpdVUnl_.mjs.map +0 -1
- package/dist/detector-BLVOCiJ_.mjs +0 -5
- package/dist/factory-C7ua_3ik.mjs +0 -3
- package/dist/fallback-CgQ_x4I7.mjs.map +0 -1
- package/dist/main-resolver-CM1IHbuu.mjs.map +0 -1
- package/dist/main-resolver-E5WlilhM.mjs +0 -6
- package/dist/query-feedback-CGcEiGtY.mjs +0 -3
- package/dist/router-CAtyVQud.mjs +0 -3
- package/dist/server-BOuAOj9b.mjs.map +0 -1
- package/dist/work-queue-worker-BM1GcAAq.mjs +0 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-feedback-
|
|
1
|
+
{"version":3,"file":"query-feedback-Dqz-HC__.mjs","names":[],"sources":["../src/zettelkasten/query-feedback.ts"],"sourcesContent":["/**\n * Query feedback loop — persist search queries and results as markdown files.\n *\n * When memory_search or zettel_converse returns results, the query + result\n * metadata is saved to ~/.config/pai/queries/ as a markdown file with YAML\n * frontmatter. The daemon indexer picks these up on the next cycle and indexes\n * them into federation.db, creating a self-reinforcing feedback loop: past\n * queries become searchable context for future queries.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nexport interface QueryRecord {\n /** The original query string. */\n query: string;\n /** Timestamp of the query. */\n timestamp: number;\n /** Tool that produced the result: 'memory_search' | 'zettel_converse'. */\n source: string;\n /** Slugs of the top result paths (for linking back). */\n sourceSlugs: string[];\n /** Preview of the answer/result (first 500 chars). */\n answerPreview: string;\n /** Number of results returned. */\n resultCount: number;\n}\n\nconst QUERIES_DIR = join(homedir(), \".config\", \"pai\", \"queries\");\n\n/**\n * Ensure the queries directory exists.\n */\nfunction ensureQueriesDir(): void {\n if (!existsSync(QUERIES_DIR)) {\n mkdirSync(QUERIES_DIR, { recursive: true });\n }\n}\n\n/**\n * Generate a filename-safe slug from a query string.\n */\nfunction querySlug(query: string, timestamp: number): string {\n const slug = query\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .slice(0, 60);\n const ts = new Date(timestamp).toISOString().slice(0, 10);\n const shortHash = timestamp.toString(36).slice(-4);\n return `${ts}-${slug}-${shortHash}`;\n}\n\n/**\n * Save a query + result record as a markdown file with YAML frontmatter.\n *\n * The file is written to ~/.config/pai/queries/ and will be picked up by\n * the daemon indexer on the next cycle.\n */\nexport function saveQueryResult(record: QueryRecord): string | null {\n try {\n ensureQueriesDir();\n\n const filename = querySlug(record.query, record.timestamp) + \".md\";\n const filepath = join(QUERIES_DIR, filename);\n\n // Don't overwrite if the exact file already exists\n if (existsSync(filepath)) return filepath;\n\n const frontmatter = [\n \"---\",\n `query: \"${record.query.replace(/\"/g, '\\\\\"')}\"`,\n `timestamp: ${new Date(record.timestamp).toISOString()}`,\n `source: ${record.source}`,\n `result_count: ${record.resultCount}`,\n `source_slugs:`,\n ...record.sourceSlugs.map((s) => ` - \"${s}\"`),\n \"---\",\n ].join(\"\\n\");\n\n const body = [\n \"\",\n `# Query: ${record.query}`,\n \"\",\n `**Source:** ${record.source} `,\n `**Date:** ${new Date(record.timestamp).toISOString().slice(0, 19).replace(\"T\", \" \")} `,\n `**Results:** ${record.resultCount}`,\n \"\",\n \"## Answer Preview\",\n \"\",\n record.answerPreview,\n \"\",\n \"## Source Paths\",\n \"\",\n ...record.sourceSlugs.map((s) => `- \\`${s}\\``),\n \"\",\n ].join(\"\\n\");\n\n writeFileSync(filepath, frontmatter + body, \"utf8\");\n return filepath;\n } catch {\n // Non-critical — don't crash the parent tool if query logging fails\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AA6BA,MAAM,cAAc,KAAK,SAAS,EAAE,WAAW,OAAO,UAAU;;;;AAKhE,SAAS,mBAAyB;AAChC,KAAI,CAAC,WAAW,YAAY,CAC1B,WAAU,aAAa,EAAE,WAAW,MAAM,CAAC;;;;;AAO/C,SAAS,UAAU,OAAe,WAA2B;CAC3D,MAAM,OAAO,MACV,aAAa,CACb,QAAQ,gBAAgB,GAAG,CAC3B,QAAQ,QAAQ,IAAI,CACpB,MAAM,GAAG,GAAG;AAGf,QAAO,GAFI,IAAI,KAAK,UAAU,CAAC,aAAa,CAAC,MAAM,GAAG,GAAG,CAE5C,GAAG,KAAK,GADH,UAAU,SAAS,GAAG,CAAC,MAAM,GAAG;;;;;;;;AAUpD,SAAgB,gBAAgB,QAAoC;AAClE,KAAI;AACF,oBAAkB;EAGlB,MAAM,WAAW,KAAK,aADL,UAAU,OAAO,OAAO,OAAO,UAAU,GAAG,MACjB;AAG5C,MAAI,WAAW,SAAS,CAAE,QAAO;AA+BjC,gBAAc,UA7BM;GAClB;GACA,WAAW,OAAO,MAAM,QAAQ,MAAM,OAAM,CAAC;GAC7C,cAAc,IAAI,KAAK,OAAO,UAAU,CAAC,aAAa;GACtD,WAAW,OAAO;GAClB,iBAAiB,OAAO;GACxB;GACA,GAAG,OAAO,YAAY,KAAK,MAAM,QAAQ,EAAE,GAAG;GAC9C;GACD,CAAC,KAAK,KAAK,GAEC;GACX;GACA,YAAY,OAAO;GACnB;GACA,eAAe,OAAO,OAAO;GAC7B,aAAa,IAAI,KAAK,OAAO,UAAU,CAAC,aAAa,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC;GACrF,gBAAgB,OAAO;GACvB;GACA;GACA;GACA,OAAO;GACP;GACA;GACA;GACA,GAAG,OAAO,YAAY,KAAK,MAAM,OAAO,EAAE,IAAI;GAC9C;GACD,CAAC,KAAK,KAAK,EAEgC,OAAO;AACnD,SAAO;SACD;AAEN,SAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router-DHracTUC.mjs","names":[],"sources":["../src/notifications/providers/ntfy.ts","../src/notifications/providers/whatsapp.ts","../src/notifications/providers/macos.ts","../src/notifications/providers/cli.ts","../src/notifications/router.ts"],"sourcesContent":["/**\n * ntfy.ts — ntfy.sh notification provider\n *\n * Sends notifications to a configured ntfy.sh topic via HTTP.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class NtfyProvider implements NotificationProvider {\n readonly channelId = \"ntfy\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.ntfy;\n if (!cfg.enabled || !cfg.url) return false;\n\n try {\n const headers: Record<string, string> = {\n \"Content-Type\": \"text/plain; charset=utf-8\",\n };\n\n if (payload.title) {\n headers[\"Title\"] = payload.title;\n }\n\n if (cfg.priority && cfg.priority !== \"default\") {\n headers[\"Priority\"] = cfg.priority;\n }\n\n const response = await fetch(cfg.url, {\n method: \"POST\",\n headers,\n body: payload.message,\n });\n\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n","/**\n * whatsapp.ts — WhatsApp notification provider (via the AIBroker hub)\n *\n * Sends through AIBroker's hub socket, which proxies to the whazaa adapter.\n *\n * It used to dial /tmp/whazaa.sock and call a method named `whatsapp_send`.\n * Both were wrong, and had been since AIBroker became the runtime hub and\n * adapters became thin transports: Whazaa no longer owns an IPC socket of its\n * own (it registers with the hub, currently at /tmp/whazaa-watcher.sock, which\n * is the hub's business and not ours), and `whatsapp_send` is an MCP TOOL name\n * — the adapter itself takes `send` and `tts`.\n *\n * So every WhatsApp notification failed. Silently, because a failed channel\n * writes one line to stderr and the router has no fallback: on 2026-08-04 four\n * task-bus escalations about a job that had not run for nine hours reached\n * /tmp/pai-scheduler.log and nowhere else, while the user had no idea.\n *\n * Routing through the hub rather than at the adapter directly is deliberate:\n * the hub knows where its adapters are, and that is exactly the knowledge whose\n * absence broke this. Connect-per-call, no persistent state.\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\n/**\n * AIBroker's hub socket.\n *\n * Hardcoded rather than imported: PAI must not depend on AIBroker. This is a\n * protocol constant between them, like the agent mark in tasks/poller.ts.\n */\nconst HUB_SOCKET = \"/tmp/aibroker.sock\";\nconst HUB_TIMEOUT_MS = 10_000;\n\n/**\n * Send a single IPC call to the hub.\n * Returns true on success, false if the hub is not available or errors.\n */\nfunction callHub(\n method: string,\n params: Record<string, unknown>\n): Promise<boolean> {\n return new Promise((resolve) => {\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(ok: boolean): void {\n if (done) return;\n done = true;\n if (timer) { clearTimeout(timer); timer = null; }\n try { socket?.destroy(); } catch { /* ignore */ }\n resolve(ok);\n }\n\n const socket = connect(HUB_SOCKET, () => {\n const request = {\n jsonrpc: \"2.0\",\n id: randomUUID(),\n method,\n params,\n };\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString();\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n try {\n const resp = JSON.parse(buffer.slice(0, nl)) as { error?: unknown; ok?: boolean };\n // The hub reports failure as {ok:false,error}; JSON-RPC reports {error}.\n // Checking only `error` counted an {ok:false} reply as a delivered\n // notification, which is the one mistake this file must never make.\n finish(!resp.error && resp.ok !== false);\n } catch {\n finish(false);\n }\n });\n\n socket.on(\"error\", () => finish(false));\n socket.on(\"end\", () => finish(false));\n\n timer = setTimeout(() => finish(false), HUB_TIMEOUT_MS);\n });\n}\n\nexport class WhatsAppProvider implements NotificationProvider {\n readonly channelId = \"whatsapp\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.whatsapp;\n if (!cfg.enabled) return false;\n\n const isVoiceMode = config.mode === \"voice\" || config.channels.voice.enabled;\n const asVoice = isVoiceMode && config.mode === \"voice\";\n\n // The adapter's own vocabulary is `send` and `tts`, reached through the\n // hub's `adapter_call`. `whatsapp_send` — what this used to ask for — is the\n // name of the MCP TOOL that wraps it, and the adapter has never answered to\n // it. The two vocabularies are easy to confuse because the MCP tool exists\n // and works; it just is not this interface.\n const inner: Record<string, unknown> = asVoice\n ? { text: payload.message, voice: config.channels.voice.voiceName ?? \"bm_george\" }\n : { message: payload.message };\n\n if (cfg.recipient) {\n // `send` addresses by `recipient`, `tts` by `jid` — same destination,\n // different key, per the adapter's interface.\n inner[asVoice ? \"jid\" : \"recipient\"] = cfg.recipient;\n }\n\n return callHub(\"adapter_call\", {\n adapter: \"whazaa\",\n method: asVoice ? \"tts\" : \"send\",\n params: inner,\n });\n }\n}\n","/**\n * macos.ts — macOS notification provider\n *\n * Uses the `osascript` command to display a macOS system notification.\n * Non-blocking: spawns the process and returns success without waiting.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class MacOsProvider implements NotificationProvider {\n readonly channelId = \"macos\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.macos;\n if (!cfg.enabled) return false;\n\n try {\n const title = payload.title ?? \"PAI\";\n // Escape single quotes in title and message for AppleScript\n const safeTitle = title.replace(/'/g, \"\\\\'\");\n const safeMessage = payload.message.replace(/'/g, \"\\\\'\");\n\n const script = `display notification \"${safeMessage}\" with title \"${safeTitle}\"`;\n\n return new Promise((resolve) => {\n const child = spawn(\"osascript\", [\"-e\", script], {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n\n // Give the process a moment to start, then assume success.\n // osascript is always present on macOS.\n child.on(\"error\", () => resolve(false));\n\n // Resolve after a short timeout — osascript exits quickly\n setTimeout(() => resolve(true), 200);\n });\n } catch {\n return false;\n }\n }\n}\n","/**\n * cli.ts — CLI notification provider\n *\n * Writes notifications to the PAI daemon log (stderr).\n * Always succeeds — it's the fallback channel.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class CliProvider implements NotificationProvider {\n readonly channelId = \"cli\" as const;\n\n async send(\n payload: NotificationPayload,\n _config: NotificationConfig\n ): Promise<boolean> {\n const prefix = `[pai-notify:${payload.event}]`;\n const title = payload.title ? ` ${payload.title}:` : \"\";\n process.stderr.write(`${prefix}${title} ${payload.message}\\n`);\n return true;\n }\n}\n","/**\n * router.ts — Notification router\n *\n * Routes notification events to the appropriate channels based on the\n * current mode and per-event routing config.\n *\n * Channel providers are instantiated lazily and cached.\n */\n\nimport type {\n NotificationPayload,\n NotificationConfig,\n NotificationProvider,\n ChannelId,\n SendResult,\n NotificationMode,\n} from \"./types.js\";\nimport { NtfyProvider } from \"./providers/ntfy.js\";\nimport { WhatsAppProvider } from \"./providers/whatsapp.js\";\nimport { MacOsProvider } from \"./providers/macos.js\";\nimport { CliProvider } from \"./providers/cli.js\";\n\n// ---------------------------------------------------------------------------\n// Provider registry (singletons — stateless, safe to reuse)\n// ---------------------------------------------------------------------------\n\nconst PROVIDERS: Record<ChannelId, NotificationProvider> = {\n ntfy: new NtfyProvider(),\n whatsapp: new WhatsAppProvider(),\n macos: new MacOsProvider(),\n voice: new WhatsAppProvider(), // Voice uses WhatsApp TTS; handled in WhatsAppProvider\n cli: new CliProvider(),\n};\n\n// ---------------------------------------------------------------------------\n// Channel resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Given the current config, resolve which channels should receive a\n * notification for the given event type.\n *\n * Mode overrides:\n * \"off\" → no channels\n * \"auto\" → use routing table, filtered by enabled channels\n * \"voice\" → whatsapp (TTS enabled in provider)\n * \"whatsapp\" → whatsapp\n * \"ntfy\" → ntfy\n * \"macos\" → macos\n * \"cli\" → cli\n */\nfunction resolveChannels(\n config: NotificationConfig,\n event: NotificationPayload[\"event\"]\n): ChannelId[] {\n const { mode, channels, routing } = config;\n\n if (mode === \"off\") return [];\n\n // Non-auto modes: force a single channel\n const modeToChannel: Partial<Record<NotificationMode, ChannelId>> = {\n voice: \"whatsapp\", // WhatsAppProvider checks mode === \"voice\" for TTS\n whatsapp: \"whatsapp\",\n ntfy: \"ntfy\",\n macos: \"macos\",\n cli: \"cli\",\n };\n\n if (mode !== \"auto\") {\n const ch = modeToChannel[mode];\n if (!ch) return [];\n // Check the channel is enabled\n const cfg = channels[ch];\n if (cfg && !cfg.enabled) return [ch]; // Still send — mode override bypasses enabled check\n return [ch];\n }\n\n // Auto mode: use routing table, filter to enabled channels\n const candidates = routing[event] ?? [];\n return candidates.filter((ch) => {\n const cfg = channels[ch];\n // \"voice\" channel is virtual — it overlaps with whatsapp.\n // Skip \"voice\" as an independent channel; voice is handled by checking config.mode.\n if (ch === \"voice\") return false;\n return cfg?.enabled === true;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Router\n// ---------------------------------------------------------------------------\n\n/**\n * Route a notification to the appropriate channels.\n *\n * Sends to all resolved channels in parallel.\n * Individual channel failures are non-fatal and logged to stderr.\n *\n * @param payload The notification to send\n * @param config The current notification config (from daemon state)\n */\nexport async function routeNotification(\n payload: NotificationPayload,\n config: NotificationConfig\n): Promise<SendResult> {\n const channels = resolveChannels(config, payload.event);\n\n if (channels.length === 0) {\n return {\n channelsAttempted: [],\n channelsSucceeded: [],\n channelsFailed: [],\n mode: config.mode,\n };\n }\n\n const results = await Promise.allSettled(\n channels.map(async (ch) => {\n const provider = PROVIDERS[ch];\n const ok = await provider.send(payload, config);\n if (!ok) {\n process.stderr.write(\n `[pai-notify] Channel ${ch} failed for event ${payload.event}\\n`\n );\n }\n return { ch, ok };\n })\n );\n\n const succeeded: ChannelId[] = [];\n const failed: ChannelId[] = [];\n\n for (const r of results) {\n if (r.status === \"fulfilled\") {\n if (r.value.ok) {\n succeeded.push(r.value.ch);\n } else {\n failed.push(r.value.ch);\n }\n } else {\n // Provider threw — treat as failure\n failed.push(channels[results.indexOf(r)]);\n }\n }\n\n return {\n channelsAttempted: channels,\n channelsSucceeded: succeeded,\n channelsFailed: failed,\n mode: config.mode,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"router-DD6D4j31.mjs","names":[],"sources":["../src/notifications/providers/ntfy.ts","../src/notifications/providers/whatsapp.ts","../src/notifications/providers/macos.ts","../src/notifications/providers/cli.ts","../src/notifications/router.ts"],"sourcesContent":["/**\n * ntfy.ts — ntfy.sh notification provider\n *\n * Sends notifications to a configured ntfy.sh topic via HTTP.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class NtfyProvider implements NotificationProvider {\n readonly channelId = \"ntfy\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.ntfy;\n if (!cfg.enabled || !cfg.url) return false;\n\n try {\n const headers: Record<string, string> = {\n \"Content-Type\": \"text/plain; charset=utf-8\",\n };\n\n if (payload.title) {\n headers[\"Title\"] = payload.title;\n }\n\n if (cfg.priority && cfg.priority !== \"default\") {\n headers[\"Priority\"] = cfg.priority;\n }\n\n const response = await fetch(cfg.url, {\n method: \"POST\",\n headers,\n body: payload.message,\n });\n\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n","/**\n * whatsapp.ts — WhatsApp notification provider (via the AIBroker hub)\n *\n * Sends through AIBroker's hub socket, which proxies to the whazaa adapter.\n *\n * It used to dial /tmp/whazaa.sock and call a method named `whatsapp_send`.\n * Both were wrong, and had been since AIBroker became the runtime hub and\n * adapters became thin transports: Whazaa no longer owns an IPC socket of its\n * own (it registers with the hub, currently at /tmp/whazaa-watcher.sock, which\n * is the hub's business and not ours), and `whatsapp_send` is an MCP TOOL name\n * — the adapter itself takes `send` and `tts`.\n *\n * So every WhatsApp notification failed. Silently, because a failed channel\n * writes one line to stderr and the router has no fallback: on 2026-08-04 four\n * task-bus escalations about a job that had not run for nine hours reached\n * /tmp/pai-scheduler.log and nowhere else, while the user had no idea.\n *\n * Routing through the hub rather than at the adapter directly is deliberate:\n * the hub knows where its adapters are, and that is exactly the knowledge whose\n * absence broke this. Connect-per-call, no persistent state.\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\n/**\n * AIBroker's hub socket.\n *\n * Hardcoded rather than imported: PAI must not depend on AIBroker. This is a\n * protocol constant between them, like the agent mark in tasks/poller.ts.\n */\nconst HUB_SOCKET = \"/tmp/aibroker.sock\";\nconst HUB_TIMEOUT_MS = 10_000;\n\n/**\n * Send a single IPC call to the hub.\n * Returns true on success, false if the hub is not available or errors.\n */\nfunction callHub(\n method: string,\n params: Record<string, unknown>\n): Promise<boolean> {\n return new Promise((resolve) => {\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(ok: boolean): void {\n if (done) return;\n done = true;\n if (timer) { clearTimeout(timer); timer = null; }\n try { socket?.destroy(); } catch { /* ignore */ }\n resolve(ok);\n }\n\n const socket = connect(HUB_SOCKET, () => {\n const request = {\n jsonrpc: \"2.0\",\n id: randomUUID(),\n method,\n params,\n };\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString();\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n try {\n const resp = JSON.parse(buffer.slice(0, nl)) as { error?: unknown; ok?: boolean };\n // The hub reports failure as {ok:false,error}; JSON-RPC reports {error}.\n // Checking only `error` counted an {ok:false} reply as a delivered\n // notification, which is the one mistake this file must never make.\n finish(!resp.error && resp.ok !== false);\n } catch {\n finish(false);\n }\n });\n\n socket.on(\"error\", () => finish(false));\n socket.on(\"end\", () => finish(false));\n\n timer = setTimeout(() => finish(false), HUB_TIMEOUT_MS);\n });\n}\n\nexport class WhatsAppProvider implements NotificationProvider {\n readonly channelId = \"whatsapp\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.whatsapp;\n if (!cfg.enabled) return false;\n\n const isVoiceMode = config.mode === \"voice\" || config.channels.voice.enabled;\n const asVoice = isVoiceMode && config.mode === \"voice\";\n\n // The adapter's own vocabulary is `send` and `tts`, reached through the\n // hub's `adapter_call`. `whatsapp_send` — what this used to ask for — is the\n // name of the MCP TOOL that wraps it, and the adapter has never answered to\n // it. The two vocabularies are easy to confuse because the MCP tool exists\n // and works; it just is not this interface.\n const inner: Record<string, unknown> = asVoice\n ? { text: payload.message, voice: config.channels.voice.voiceName ?? \"bm_george\" }\n : { message: payload.message };\n\n if (cfg.recipient) {\n // `send` addresses by `recipient`, `tts` by `jid` — same destination,\n // different key, per the adapter's interface.\n inner[asVoice ? \"jid\" : \"recipient\"] = cfg.recipient;\n }\n\n return callHub(\"adapter_call\", {\n adapter: \"whazaa\",\n method: asVoice ? \"tts\" : \"send\",\n params: inner,\n });\n }\n}\n","/**\n * macos.ts — macOS notification provider\n *\n * Uses the `osascript` command to display a macOS system notification.\n * Non-blocking: spawns the process and returns success without waiting.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class MacOsProvider implements NotificationProvider {\n readonly channelId = \"macos\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.macos;\n if (!cfg.enabled) return false;\n\n try {\n const title = payload.title ?? \"PAI\";\n // Escape single quotes in title and message for AppleScript\n const safeTitle = title.replace(/'/g, \"\\\\'\");\n const safeMessage = payload.message.replace(/'/g, \"\\\\'\");\n\n const script = `display notification \"${safeMessage}\" with title \"${safeTitle}\"`;\n\n return new Promise((resolve) => {\n const child = spawn(\"osascript\", [\"-e\", script], {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n\n // Give the process a moment to start, then assume success.\n // osascript is always present on macOS.\n child.on(\"error\", () => resolve(false));\n\n // Resolve after a short timeout — osascript exits quickly\n setTimeout(() => resolve(true), 200);\n });\n } catch {\n return false;\n }\n }\n}\n","/**\n * cli.ts — CLI notification provider\n *\n * Writes notifications to the PAI daemon log (stderr).\n * Always succeeds — it's the fallback channel.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class CliProvider implements NotificationProvider {\n readonly channelId = \"cli\" as const;\n\n async send(\n payload: NotificationPayload,\n _config: NotificationConfig\n ): Promise<boolean> {\n const prefix = `[pai-notify:${payload.event}]`;\n const title = payload.title ? ` ${payload.title}:` : \"\";\n process.stderr.write(`${prefix}${title} ${payload.message}\\n`);\n return true;\n }\n}\n","/**\n * router.ts — Notification router\n *\n * Routes notification events to the appropriate channels based on the\n * current mode and per-event routing config.\n *\n * Channel providers are instantiated lazily and cached.\n */\n\nimport type {\n NotificationPayload,\n NotificationConfig,\n NotificationProvider,\n ChannelId,\n SendResult,\n NotificationMode,\n} from \"./types.js\";\nimport { NtfyProvider } from \"./providers/ntfy.js\";\nimport { WhatsAppProvider } from \"./providers/whatsapp.js\";\nimport { MacOsProvider } from \"./providers/macos.js\";\nimport { CliProvider } from \"./providers/cli.js\";\n\n// ---------------------------------------------------------------------------\n// Provider registry (singletons — stateless, safe to reuse)\n// ---------------------------------------------------------------------------\n\nconst PROVIDERS: Record<ChannelId, NotificationProvider> = {\n ntfy: new NtfyProvider(),\n whatsapp: new WhatsAppProvider(),\n macos: new MacOsProvider(),\n voice: new WhatsAppProvider(), // Voice uses WhatsApp TTS; handled in WhatsAppProvider\n cli: new CliProvider(),\n};\n\n// ---------------------------------------------------------------------------\n// Channel resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Given the current config, resolve which channels should receive a\n * notification for the given event type.\n *\n * Mode overrides:\n * \"off\" → no channels\n * \"auto\" → use routing table, filtered by enabled channels\n * \"voice\" → whatsapp (TTS enabled in provider)\n * \"whatsapp\" → whatsapp\n * \"ntfy\" → ntfy\n * \"macos\" → macos\n * \"cli\" → cli\n */\nfunction resolveChannels(\n config: NotificationConfig,\n event: NotificationPayload[\"event\"]\n): ChannelId[] {\n const { mode, channels, routing } = config;\n\n if (mode === \"off\") return [];\n\n // Non-auto modes: force a single channel\n const modeToChannel: Partial<Record<NotificationMode, ChannelId>> = {\n voice: \"whatsapp\", // WhatsAppProvider checks mode === \"voice\" for TTS\n whatsapp: \"whatsapp\",\n ntfy: \"ntfy\",\n macos: \"macos\",\n cli: \"cli\",\n };\n\n if (mode !== \"auto\") {\n const ch = modeToChannel[mode];\n if (!ch) return [];\n // Check the channel is enabled\n const cfg = channels[ch];\n if (cfg && !cfg.enabled) return [ch]; // Still send — mode override bypasses enabled check\n return [ch];\n }\n\n // Auto mode: use routing table, filter to enabled channels\n const candidates = routing[event] ?? [];\n return candidates.filter((ch) => {\n const cfg = channels[ch];\n // \"voice\" channel is virtual — it overlaps with whatsapp.\n // Skip \"voice\" as an independent channel; voice is handled by checking config.mode.\n if (ch === \"voice\") return false;\n return cfg?.enabled === true;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Router\n// ---------------------------------------------------------------------------\n\n/**\n * Route a notification to the appropriate channels.\n *\n * Sends to all resolved channels in parallel.\n * Individual channel failures are non-fatal and logged to stderr.\n *\n * @param payload The notification to send\n * @param config The current notification config (from daemon state)\n */\nexport async function routeNotification(\n payload: NotificationPayload,\n config: NotificationConfig\n): Promise<SendResult> {\n const channels = resolveChannels(config, payload.event);\n\n if (channels.length === 0) {\n return {\n channelsAttempted: [],\n channelsSucceeded: [],\n channelsFailed: [],\n mode: config.mode,\n };\n }\n\n const results = await Promise.allSettled(\n channels.map(async (ch) => {\n const provider = PROVIDERS[ch];\n const ok = await provider.send(payload, config);\n if (!ok) {\n process.stderr.write(\n `[pai-notify] Channel ${ch} failed for event ${payload.event}\\n`\n );\n }\n return { ch, ok };\n })\n );\n\n const succeeded: ChannelId[] = [];\n const failed: ChannelId[] = [];\n\n for (const r of results) {\n if (r.status === \"fulfilled\") {\n if (r.value.ok) {\n succeeded.push(r.value.ch);\n } else {\n failed.push(r.value.ch);\n }\n } else {\n // Provider threw — treat as failure\n failed.push(channels[results.indexOf(r)]);\n }\n }\n\n return {\n channelsAttempted: channels,\n channelsSucceeded: succeeded,\n channelsFailed: failed,\n mode: config.mode,\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/runtime-paths.ts
|
|
2
|
+
/**
|
|
3
|
+
* runtime-paths.ts — the shared files PAI talks to the running daemon through.
|
|
4
|
+
*
|
|
5
|
+
* Sockets, logs and pidfiles live at fixed absolute paths under /tmp, and every
|
|
6
|
+
* one of them is shared with a *live* daemon. That makes them the same hazard
|
|
7
|
+
* as any other piece of real user state a test can reach: writing one from a
|
|
8
|
+
* test does not fail, it corrupts something already running, silently.
|
|
9
|
+
*
|
|
10
|
+
* The home guard in test/setup-home-guard.ts does not cover them. It works by
|
|
11
|
+
* redirecting HOME, so it protects paths *derived* from the home directory and
|
|
12
|
+
* nothing else — a hardcoded "/tmp/pai.sock" walks straight past it. Verified
|
|
13
|
+
* on 2026-08-01 by snapshotting all five and running the suite: nothing writes
|
|
14
|
+
* to them today. But "no test does this yet" is not a property, and the next
|
|
15
|
+
* daemon test to be written would clobber the live socket with nothing to stop
|
|
16
|
+
* it.
|
|
17
|
+
*
|
|
18
|
+
* So each path is read from an environment variable with the historical /tmp
|
|
19
|
+
* value as its default. Behaviour is unchanged for every real invocation; the
|
|
20
|
+
* test setup overrides the variables and the whole class becomes unreachable
|
|
21
|
+
* rather than merely absent, the same way the home guard works.
|
|
22
|
+
*
|
|
23
|
+
* Read at call time, not at module load: the test setup file runs before any
|
|
24
|
+
* module is imported, but resolving these eagerly would still bake in whatever
|
|
25
|
+
* the environment held at import and make the override order matter.
|
|
26
|
+
*/
|
|
27
|
+
/** IPC socket for the PAI daemon. */
|
|
28
|
+
function paiSocketPath() {
|
|
29
|
+
return process.env.PAI_SOCKET_PATH ?? "/tmp/pai.sock";
|
|
30
|
+
}
|
|
31
|
+
/** IPC socket for the AIBroker daemon — read by PAI, owned by AIBroker. */
|
|
32
|
+
function aibrokerSocketPath() {
|
|
33
|
+
return process.env.PAI_AIBROKER_SOCKET_PATH ?? "/tmp/aibroker.sock";
|
|
34
|
+
}
|
|
35
|
+
/** Where the daemon's stdout and stderr are collected. */
|
|
36
|
+
function daemonLogPath() {
|
|
37
|
+
return process.env.PAI_DAEMON_LOG_PATH ?? "/tmp/pai-daemon.log";
|
|
38
|
+
}
|
|
39
|
+
/** Pidfile for the running daemon. */
|
|
40
|
+
function daemonPidPath() {
|
|
41
|
+
return process.env.PAI_DAEMON_PID_PATH ?? "/tmp/pai-daemon.pid";
|
|
42
|
+
}
|
|
43
|
+
/** Where the launchd scheduler tick writes its output. */
|
|
44
|
+
function schedulerLogPath() {
|
|
45
|
+
return process.env.PAI_SCHEDULER_LOG_PATH ?? "/tmp/pai-scheduler.log";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { schedulerLogPath as a, paiSocketPath as i, daemonLogPath as n, daemonPidPath as r, aibrokerSocketPath as t };
|
|
50
|
+
//# sourceMappingURL=runtime-paths-rni52zHX.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-paths-rni52zHX.mjs","names":[],"sources":["../src/runtime-paths.ts"],"sourcesContent":["/**\n * runtime-paths.ts — the shared files PAI talks to the running daemon through.\n *\n * Sockets, logs and pidfiles live at fixed absolute paths under /tmp, and every\n * one of them is shared with a *live* daemon. That makes them the same hazard\n * as any other piece of real user state a test can reach: writing one from a\n * test does not fail, it corrupts something already running, silently.\n *\n * The home guard in test/setup-home-guard.ts does not cover them. It works by\n * redirecting HOME, so it protects paths *derived* from the home directory and\n * nothing else — a hardcoded \"/tmp/pai.sock\" walks straight past it. Verified\n * on 2026-08-01 by snapshotting all five and running the suite: nothing writes\n * to them today. But \"no test does this yet\" is not a property, and the next\n * daemon test to be written would clobber the live socket with nothing to stop\n * it.\n *\n * So each path is read from an environment variable with the historical /tmp\n * value as its default. Behaviour is unchanged for every real invocation; the\n * test setup overrides the variables and the whole class becomes unreachable\n * rather than merely absent, the same way the home guard works.\n *\n * Read at call time, not at module load: the test setup file runs before any\n * module is imported, but resolving these eagerly would still bake in whatever\n * the environment held at import and make the override order matter.\n */\n\n/** IPC socket for the PAI daemon. */\nexport function paiSocketPath(): string {\n return process.env.PAI_SOCKET_PATH ?? \"/tmp/pai.sock\";\n}\n\n/** IPC socket for the AIBroker daemon — read by PAI, owned by AIBroker. */\nexport function aibrokerSocketPath(): string {\n return process.env.PAI_AIBROKER_SOCKET_PATH ?? \"/tmp/aibroker.sock\";\n}\n\n/** Where the daemon's stdout and stderr are collected. */\nexport function daemonLogPath(): string {\n return process.env.PAI_DAEMON_LOG_PATH ?? \"/tmp/pai-daemon.log\";\n}\n\n/** Pidfile for the running daemon. */\nexport function daemonPidPath(): string {\n return process.env.PAI_DAEMON_PID_PATH ?? \"/tmp/pai-daemon.pid\";\n}\n\n/** Where the launchd scheduler tick writes its output. */\nexport function schedulerLogPath(): string {\n return process.env.PAI_SCHEDULER_LOG_PATH ?? \"/tmp/pai-scheduler.log\";\n}\n\n/**\n * Every runtime path, for the test guard to redirect in one place.\n *\n * Listed here rather than in the guard so that adding a path and forgetting to\n * protect it is not possible: the guard iterates this.\n */\nexport const RUNTIME_PATH_ENV_VARS = [\n \"PAI_SOCKET_PATH\",\n \"PAI_AIBROKER_SOCKET_PATH\",\n \"PAI_DAEMON_LOG_PATH\",\n \"PAI_DAEMON_PID_PATH\",\n \"PAI_SCHEDULER_LOG_PATH\",\n] as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,gBAAwB;AACtC,QAAO,QAAQ,IAAI,mBAAmB;;;AAIxC,SAAgB,qBAA6B;AAC3C,QAAO,QAAQ,IAAI,4BAA4B;;;AAIjD,SAAgB,gBAAwB;AACtC,QAAO,QAAQ,IAAI,uBAAuB;;;AAI5C,SAAgB,gBAAwB;AACtC,QAAO,QAAQ,IAAI,uBAAuB;;;AAI5C,SAAgB,mBAA2B;AACzC,QAAO,QAAQ,IAAI,0BAA0B"}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { g as statusPath } from "./server-DvN-IFIV.mjs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/workers/args.ts
|
|
7
|
+
function parseRunnerArgs(argv) {
|
|
8
|
+
let prompt = null;
|
|
9
|
+
let outputFormat = "text";
|
|
10
|
+
const rest = [];
|
|
11
|
+
let headless = false;
|
|
12
|
+
let callerModel = false;
|
|
13
|
+
let callerMcpConfig = false;
|
|
14
|
+
let callerSystemPrompt = false;
|
|
15
|
+
const mcp = [];
|
|
16
|
+
const allowedTools = [];
|
|
17
|
+
let i = 0;
|
|
18
|
+
while (i < argv.length) {
|
|
19
|
+
const a = argv[i];
|
|
20
|
+
if (a === "-p" || a === "--print") {
|
|
21
|
+
headless = true;
|
|
22
|
+
rest.push(a);
|
|
23
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
24
|
+
prompt = argv[i + 1];
|
|
25
|
+
rest.push(prompt);
|
|
26
|
+
i += 1;
|
|
27
|
+
}
|
|
28
|
+
} else if (a === "--output-format") {
|
|
29
|
+
const v = argv[i + 1];
|
|
30
|
+
if (v === "json" || v === "stream-json") outputFormat = v;
|
|
31
|
+
i += 1;
|
|
32
|
+
} else if (a.startsWith("--output-format=")) {
|
|
33
|
+
const v = a.slice(16);
|
|
34
|
+
if (v === "json" || v === "stream-json") outputFormat = v;
|
|
35
|
+
} else if (a === "--verbose") {} else if (a === "--mcp") {
|
|
36
|
+
const v = argv[i + 1];
|
|
37
|
+
if (v !== void 0 && !v.startsWith("-")) {
|
|
38
|
+
mcp.push(v);
|
|
39
|
+
i += 1;
|
|
40
|
+
}
|
|
41
|
+
} else if (a.startsWith("--mcp=")) mcp.push(a.slice(6));
|
|
42
|
+
else if (a === "--allowedTools") {
|
|
43
|
+
rest.push(a);
|
|
44
|
+
const v = argv[i + 1];
|
|
45
|
+
if (v !== void 0 && !v.startsWith("-")) {
|
|
46
|
+
allowedTools.push(v);
|
|
47
|
+
rest.push(v);
|
|
48
|
+
i += 1;
|
|
49
|
+
}
|
|
50
|
+
} else if (a.startsWith("--allowedTools=")) {
|
|
51
|
+
allowedTools.push(a.slice(15));
|
|
52
|
+
rest.push(a);
|
|
53
|
+
} else {
|
|
54
|
+
if (a === "--model") callerModel = true;
|
|
55
|
+
if (a.startsWith("--model=")) callerModel = true;
|
|
56
|
+
if (a === "--mcp-config") callerMcpConfig = true;
|
|
57
|
+
if (a.startsWith("--mcp-config=")) callerMcpConfig = true;
|
|
58
|
+
if (a === "--append-system-prompt") callerSystemPrompt = true;
|
|
59
|
+
if (a.startsWith("--append-system-prompt=")) callerSystemPrompt = true;
|
|
60
|
+
if (prompt === null && !a.startsWith("-") && rest.length > 0 && (rest[rest.length - 1] === "-p" || rest[rest.length - 1] === "--print")) prompt = a;
|
|
61
|
+
rest.push(a);
|
|
62
|
+
}
|
|
63
|
+
i += 1;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
prompt,
|
|
67
|
+
outputFormat,
|
|
68
|
+
rest,
|
|
69
|
+
headless,
|
|
70
|
+
callerModel,
|
|
71
|
+
callerMcpConfig,
|
|
72
|
+
callerSystemPrompt,
|
|
73
|
+
mcp,
|
|
74
|
+
allowedTools
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Turn `-p "<prompt>"` into bare `-p` (same for --print): for stream-json
|
|
79
|
+
* stdin runs the prompt moves to the first user message on stdin, so the
|
|
80
|
+
* value must not stay on the command line. Only prompt values directly
|
|
81
|
+
* following the flag are touched; everything else passes through.
|
|
82
|
+
*/
|
|
83
|
+
function stripPromptValues(argv) {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (let i = 0; i < argv.length; i++) {
|
|
86
|
+
const a = argv[i];
|
|
87
|
+
const isPrint = a === "-p" || a === "--print";
|
|
88
|
+
out.push(a);
|
|
89
|
+
if (isPrint && i + 1 < argv.length && !argv[i + 1].startsWith("-")) i += 1;
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/** Collapse whitespace and cut to n chars with an ellipsis (label rendering). */
|
|
94
|
+
function shortText(s, n) {
|
|
95
|
+
const t = String(s ?? "").split(/\s+/).filter(Boolean).join(" ");
|
|
96
|
+
return t.length <= n ? t : t.slice(0, n - 1) + "…";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/workers/status.ts
|
|
101
|
+
/**
|
|
102
|
+
* status.ts — the live per-worker status file.
|
|
103
|
+
*
|
|
104
|
+
* One JSON file per run, rewritten atomically on every turn, read by `ps`,
|
|
105
|
+
* `follow`, the status line and the pane logic. Same field set the Python
|
|
106
|
+
* runner wrote, plus `provider` (routing is multi-provider now) and `session`
|
|
107
|
+
* (the AIBroker identity of the launching session, see scope.ts).
|
|
108
|
+
*/
|
|
109
|
+
/** Label a worker gets when launched with neither --label nor a prompt. */
|
|
110
|
+
const UNLABELED = "unlabeled";
|
|
111
|
+
/**
|
|
112
|
+
* Whether a status is the terminal's interactive chat pane, not a spawned
|
|
113
|
+
* task worker. `origin` is the real discriminator; the fallback catches
|
|
114
|
+
* running entries from before the flag existed (unlabeled, no turns yet).
|
|
115
|
+
* Removable once every pane runs code that writes `origin`.
|
|
116
|
+
*/
|
|
117
|
+
function isChatPane(s) {
|
|
118
|
+
return s.origin === "chat" || !s.origin && (s.label === UNLABELED || s.label === "(no prompt)") && s.turns === 0;
|
|
119
|
+
}
|
|
120
|
+
/** Context-meter percentage 0–100, null when the numbers are missing. */
|
|
121
|
+
function contextPercent(s) {
|
|
122
|
+
if (!s.contextTokens || !s.contextWindow) return null;
|
|
123
|
+
return Math.round(s.contextTokens / s.contextWindow * 100);
|
|
124
|
+
}
|
|
125
|
+
/** Compact token count: 84k, 200k, 900. */
|
|
126
|
+
function fmtContextK(n) {
|
|
127
|
+
return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The one context-meter label, shared by every renderer so identical
|
|
131
|
+
* numbers always render identically: `ctx 84k/200k (42%)` — used-style
|
|
132
|
+
* percent. Empty when the numbers are missing (callers drop the part).
|
|
133
|
+
*/
|
|
134
|
+
function contextLabel(s) {
|
|
135
|
+
const pct = contextPercent(s);
|
|
136
|
+
if (pct === null) return "";
|
|
137
|
+
return `ctx ${fmtContextK(s.contextTokens ?? 0)}/${fmtContextK(s.contextWindow ?? 0)} (${pct}%)`;
|
|
138
|
+
}
|
|
139
|
+
/** Timestamp format shared by status files and the ledger. */
|
|
140
|
+
function nowStamp(d = /* @__PURE__ */ new Date()) {
|
|
141
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
142
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
143
|
+
}
|
|
144
|
+
let lastId = "";
|
|
145
|
+
let idSeq = 0;
|
|
146
|
+
function newWorkerId(d = /* @__PURE__ */ new Date(), pid = process.pid) {
|
|
147
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
148
|
+
const base = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${pid}`;
|
|
149
|
+
if (base === lastId) {
|
|
150
|
+
idSeq += 1;
|
|
151
|
+
return `${base}-${String(idSeq).padStart(2, "0")}`;
|
|
152
|
+
}
|
|
153
|
+
lastId = base;
|
|
154
|
+
idSeq = 0;
|
|
155
|
+
return base;
|
|
156
|
+
}
|
|
157
|
+
/** Write status atomically (temp + rename) and stamp `updated`. */
|
|
158
|
+
function saveStatus(logDir, status, d = /* @__PURE__ */ new Date()) {
|
|
159
|
+
status.updated = nowStamp(d);
|
|
160
|
+
const path = statusPath(logDir, status.id);
|
|
161
|
+
const tmp = `${path}.tmp`;
|
|
162
|
+
writeFileSync(tmp, JSON.stringify(status), "utf8");
|
|
163
|
+
renameSync(tmp, path);
|
|
164
|
+
}
|
|
165
|
+
/** Load every status file in the logDir, oldest id first, skipping damage. */
|
|
166
|
+
function loadStatuses(logDir) {
|
|
167
|
+
if (!existsSync(logDir)) return [];
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const name of readdirSync(logDir).sort()) {
|
|
170
|
+
if (!name.endsWith(".status")) continue;
|
|
171
|
+
try {
|
|
172
|
+
out.push(JSON.parse(readFileSync(join(logDir, name), "utf8")));
|
|
173
|
+
} catch {}
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
function loadStatus(logDir, id) {
|
|
178
|
+
const path = statusPath(logDir, id);
|
|
179
|
+
if (!existsSync(path)) return null;
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function alive(pid) {
|
|
187
|
+
if (!pid || pid <= 0) return false;
|
|
188
|
+
try {
|
|
189
|
+
process.kill(pid, 0);
|
|
190
|
+
return true;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** "42s" under 90s, "7m" above — the coarse age the table and bar show. */
|
|
196
|
+
function ageOf(ts, now = /* @__PURE__ */ new Date()) {
|
|
197
|
+
const t = Date.parse(ts.replace(" ", "T"));
|
|
198
|
+
if (Number.isNaN(t)) return "?";
|
|
199
|
+
const s = Math.max(0, Math.floor((now.getTime() - t) / 1e3));
|
|
200
|
+
return s < 90 ? `${s}s` : `${Math.floor(s / 60)}m`;
|
|
201
|
+
}
|
|
202
|
+
/** One-line description of a tool call, e.g. "Bash: npm test". */
|
|
203
|
+
function describeTool(name, inp) {
|
|
204
|
+
if (typeof inp !== "object" || inp === null) return name;
|
|
205
|
+
const i = inp;
|
|
206
|
+
const get = (k) => typeof i[k] === "string" ? i[k] : "";
|
|
207
|
+
if (name === "Bash") return `Bash: ${shortText(get("command"), 70)}`;
|
|
208
|
+
if (name === "Read" || name === "Edit" || name === "Write" || name === "MultiEdit") return `${name}: ${get("file_path").split("/").pop() ?? ""}`;
|
|
209
|
+
if (name === "Grep" || name === "Glob") return `${name}: ${shortText(get("pattern"), 50)}`;
|
|
210
|
+
if (name === "WebSearch") return `${name}: ${shortText(get("query"), 50)}`;
|
|
211
|
+
if (name === "WebFetch") return `${name}: ${shortText(get("url"), 60)}`;
|
|
212
|
+
return name;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/workers/scope.ts
|
|
217
|
+
/**
|
|
218
|
+
* scope.ts — which workers "belong" to the terminal asking about them.
|
|
219
|
+
*
|
|
220
|
+
* Two identities, in priority order:
|
|
221
|
+
*
|
|
222
|
+
* 1. AIBroker's session registry (~/.aibroker/session-names.json): iTerm
|
|
223
|
+
* session UUID → session name. The launching session's UUID is stored in
|
|
224
|
+
* each worker's status file as `session.id`, so every pane of a named
|
|
225
|
+
* session sees its workers — regardless of tab layout.
|
|
226
|
+
* 2. The iTerm tab key (`w<window>t<tab>` prefix of ITERM_SESSION_ID): the
|
|
227
|
+
* original heuristic, kept as the fallback when no registry entry
|
|
228
|
+
* matches (AIBroker absent, unnamed session, non-iTerm terminal).
|
|
229
|
+
*
|
|
230
|
+
* If neither is available, viewers fall back to "all workers".
|
|
231
|
+
*
|
|
232
|
+
* A third case: workers spawned from a Claude Code Bash tool (the
|
|
233
|
+
* orchestrator pattern) have neither — Claude Code exports no terminal or
|
|
234
|
+
* session identity to its Bash children. Those record `spawnerSession`, the
|
|
235
|
+
* orchestrator's claude session id bridged through the status line's
|
|
236
|
+
* session map (see below), so their tab still claims them.
|
|
237
|
+
*/
|
|
238
|
+
const AIBROKER_REGISTRY = join(homedir(), ".aibroker", "session-names.json");
|
|
239
|
+
/**
|
|
240
|
+
* `w<window>t<tab>` prefix of an iTerm session id, "" if empty or malformed.
|
|
241
|
+
* Panes split from the same tab share this prefix and differ only in `p<n>`.
|
|
242
|
+
*/
|
|
243
|
+
function tabKey(term) {
|
|
244
|
+
if (!term) return "";
|
|
245
|
+
const head = term.split("p", 1)[0];
|
|
246
|
+
const parts = head.slice(1).split("t");
|
|
247
|
+
if (head.startsWith("w") && parts.length === 2 && parts.every((p) => /^\d+$/.test(p))) return head;
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
/** Tab key of the iTerm tab this process runs in, "" if not inside iTerm2. */
|
|
251
|
+
function currentTabKey(env = process.env) {
|
|
252
|
+
return tabKey(env.ITERM_SESSION_ID ?? "");
|
|
253
|
+
}
|
|
254
|
+
/** The iTerm UUID part of an ITERM_SESSION_ID (after the last colon). */
|
|
255
|
+
function itermUuid(term) {
|
|
256
|
+
if (!term) return "";
|
|
257
|
+
return term.split(":").pop() ?? "";
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Resolve the AIBroker session for an ITERM_SESSION_ID. Reads the persistent
|
|
261
|
+
* name registry (the same store `aibroker_rename` writes); returns null when
|
|
262
|
+
* AIBroker is absent or the session is not in it — callers then use the tab
|
|
263
|
+
* key, which is the pre-AIBroker behaviour.
|
|
264
|
+
*/
|
|
265
|
+
function resolveSession(term, registryPath = AIBROKER_REGISTRY) {
|
|
266
|
+
const uuid = itermUuid(term);
|
|
267
|
+
if (!uuid) return null;
|
|
268
|
+
let names;
|
|
269
|
+
try {
|
|
270
|
+
if (!existsSync(registryPath)) return null;
|
|
271
|
+
names = JSON.parse(readFileSync(registryPath, "utf8"));
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const name = names[uuid];
|
|
276
|
+
if (typeof name !== "string" || !name) return null;
|
|
277
|
+
return {
|
|
278
|
+
id: uuid,
|
|
279
|
+
name
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* True when `worker` was launched from `term` (the terminal asking about it).
|
|
284
|
+
* Session-id match first; the tab key only when no registry entry matched —
|
|
285
|
+
* a session without a name keeps the tab-scoped behaviour it always had.
|
|
286
|
+
*/
|
|
287
|
+
function workerInScope(worker, term) {
|
|
288
|
+
if (!term) return false;
|
|
289
|
+
const uuid = itermUuid(term);
|
|
290
|
+
if (worker.session?.id && uuid) return worker.session.id === uuid;
|
|
291
|
+
return tabKey(worker.term) === tabKey(term) && tabKey(term) !== "";
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Registry key for pane files: the session id when AIBroker knows this
|
|
295
|
+
* terminal, else the tab key, else the raw iTerm UUID.
|
|
296
|
+
*/
|
|
297
|
+
function scopeKey(term) {
|
|
298
|
+
const session = resolveSession(term);
|
|
299
|
+
if (session) return session.id;
|
|
300
|
+
return currentTabKey() || itermUuid(term);
|
|
301
|
+
}
|
|
302
|
+
/** `[Name]` when the worker has an AIBroker session name, else "". */
|
|
303
|
+
function sessionTag(worker) {
|
|
304
|
+
return worker.session?.name ? `[${worker.session.name}]` : "";
|
|
305
|
+
}
|
|
306
|
+
/** How old a map entry may be for a spawn to adopt it (status lines refresh constantly while a session lives). */
|
|
307
|
+
const SPAWNER_SESSION_TTL_MS = 10 * 6e4;
|
|
308
|
+
/** Map entries not refreshed within this window are pruned on write. */
|
|
309
|
+
const SESSION_MAP_PRUNE_MS = 60 * 6e4;
|
|
310
|
+
function sessionMapPath(logDir) {
|
|
311
|
+
return join(logDir, "claude-session-map.json");
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The claude session a new run was spawned by, when it can be known: a run
|
|
315
|
+
* inside another worker inherits its spawner (chain stages keep the
|
|
316
|
+
* orchestrator's tab that way); otherwise a fresh map entry for `cwd`.
|
|
317
|
+
*/
|
|
318
|
+
function resolveSpawnerSession(logDir, cwd, env = process.env, now = Date.now()) {
|
|
319
|
+
const parentId = env.PAI_WORKER_ID;
|
|
320
|
+
if (parentId) {
|
|
321
|
+
const inherited = loadStatus(logDir, parentId)?.spawnerSession;
|
|
322
|
+
if (inherited) return inherited;
|
|
323
|
+
}
|
|
324
|
+
const path = sessionMapPath(logDir);
|
|
325
|
+
if (!existsSync(path) || !cwd) return null;
|
|
326
|
+
try {
|
|
327
|
+
const entry = JSON.parse(readFileSync(path, "utf8"))[cwd];
|
|
328
|
+
if (entry && entry.session && now - entry.ts < SPAWNER_SESSION_TTL_MS) return entry.session;
|
|
329
|
+
} catch {}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* The iTerm UUID a claude session last rendered its status line in, when the
|
|
334
|
+
* map knows one: the freshest entry naming that session. This is the reverse
|
|
335
|
+
* of resolveSpawnerSession — worker → orchestrator there, orchestrator →
|
|
336
|
+
* terminal here — and it exists so daemon-side pushes (supervision) can reach
|
|
337
|
+
* a session that has no iTerm identity of its own in any worker status.
|
|
338
|
+
*/
|
|
339
|
+
function itermForClaudeSession(logDir, claudeSession, now = Date.now()) {
|
|
340
|
+
const path = sessionMapPath(logDir);
|
|
341
|
+
if (!existsSync(path) || !claudeSession) return null;
|
|
342
|
+
let map;
|
|
343
|
+
try {
|
|
344
|
+
map = JSON.parse(readFileSync(path, "utf8"));
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
let best = null;
|
|
349
|
+
for (const e of Object.values(map)) {
|
|
350
|
+
if (e.session !== claudeSession || !e.term) continue;
|
|
351
|
+
if (now - e.ts >= SESSION_MAP_PRUNE_MS) continue;
|
|
352
|
+
if (!best || e.ts > best.ts) best = e;
|
|
353
|
+
}
|
|
354
|
+
const term = best?.term;
|
|
355
|
+
return term ? itermUuid(term) : null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
//#endregion
|
|
359
|
+
export { stripPromptValues as C, shortText as S, loadStatuses as _, resolveSpawnerSession as a, saveStatus as b, workerInScope as c, alive as d, contextLabel as f, loadStatus as g, isChatPane as h, resolveSession as i, UNLABELED as l, describeTool as m, itermForClaudeSession as n, scopeKey as o, contextPercent as p, itermUuid as r, sessionTag as s, currentTabKey as t, ageOf as u, newWorkerId as v, parseRunnerArgs as x, nowStamp as y };
|
|
360
|
+
//# sourceMappingURL=scope-CfYnjy5R.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-CfYnjy5R.mjs","names":[],"sources":["../src/workers/args.ts","../src/workers/status.ts","../src/workers/scope.ts"],"sourcesContent":["/**\n * args.ts — parse the claude-args tail that `pai worker run` receives.\n *\n * The runner needs a few things out of the caller's argument vector: the\n * prompt (for the default label), the requested --output-format (so the final\n * print matches what plain `claude -p` would have produced), whether the\n * caller already chose a --model or an --mcp-config (both suppress the\n * defaults the runner would otherwise force), any --mcp allowlist names, any\n * --allowedTools grants (their mcp__server__… entries decide which servers\n * load), and whether they appended their own system prompt (the worker\n * contract is then added alongside, not instead). Everything is passed\n * through untouched — the runner never rewrites the caller's task.\n */\n\nexport interface ParsedRunnerArgs {\n /** Prompt string after -p/--print, when the run is headless. */\n prompt: string | null;\n /** Caller's --output-format: text (default), json, or stream-json. */\n outputFormat: \"text\" | \"json\" | \"stream-json\";\n /** Args to hand to claude (minus --output-format/--verbose, which we add). */\n rest: string[];\n /** Headless: a -p/--print flag is present. */\n headless: boolean;\n /** Caller passed --model (or --model=…): do not force the provider model. */\n callerModel: boolean;\n /** Caller passed --mcp-config (or --mcp-config=…): keep their MCP setup. */\n callerMcpConfig: boolean;\n /** Caller passed --append-system-prompt: the contract is added alongside. */\n callerSystemPrompt: boolean;\n /** --mcp values (repeatable, comma-separated inside one flag). */\n mcp: string[];\n /** --allowedTools values (repeatable, comma-separated inside one flag). */\n allowedTools: string[];\n}\n\nexport function parseRunnerArgs(argv: string[]): ParsedRunnerArgs {\n let prompt: string | null = null;\n let outputFormat: ParsedRunnerArgs[\"outputFormat\"] = \"text\";\n const rest: string[] = [];\n let headless = false;\n let callerModel = false;\n let callerMcpConfig = false;\n let callerSystemPrompt = false;\n const mcp: string[] = [];\n const allowedTools: string[] = [];\n\n let i = 0;\n while (i < argv.length) {\n const a = argv[i];\n if (a === \"-p\" || a === \"--print\") {\n headless = true;\n rest.push(a);\n if (i + 1 < argv.length && !argv[i + 1].startsWith(\"-\")) {\n prompt = argv[i + 1];\n rest.push(prompt);\n i += 1;\n }\n } else if (a === \"--output-format\") {\n const v = argv[i + 1];\n if (v === \"json\" || v === \"stream-json\") outputFormat = v;\n i += 1; // dropped; the runner prints the result in this format itself\n } else if (a.startsWith(\"--output-format=\")) {\n const v = a.slice(\"--output-format=\".length);\n if (v === \"json\" || v === \"stream-json\") outputFormat = v;\n } else if (a === \"--verbose\") {\n // dropped; re-added by the runner\n } else if (a === \"--mcp\") {\n const v = argv[i + 1];\n if (v !== undefined && !v.startsWith(\"-\")) {\n mcp.push(v);\n i += 1;\n }\n } else if (a.startsWith(\"--mcp=\")) {\n mcp.push(a.slice(\"--mcp=\".length));\n } else if (a === \"--allowedTools\") {\n // captured for the MCP derivation, but still claude's flag: it passes through\n rest.push(a);\n const v = argv[i + 1];\n if (v !== undefined && !v.startsWith(\"-\")) {\n allowedTools.push(v);\n rest.push(v);\n i += 1;\n }\n } else if (a.startsWith(\"--allowedTools=\")) {\n allowedTools.push(a.slice(\"--allowedTools=\".length));\n rest.push(a);\n } else {\n if (a === \"--model\") callerModel = true;\n if (a.startsWith(\"--model=\")) callerModel = true;\n if (a === \"--mcp-config\") callerMcpConfig = true;\n if (a.startsWith(\"--mcp-config=\")) callerMcpConfig = true;\n if (a === \"--append-system-prompt\") callerSystemPrompt = true;\n if (a.startsWith(\"--append-system-prompt=\")) callerSystemPrompt = true;\n if (\n prompt === null && !a.startsWith(\"-\") && rest.length > 0 &&\n (rest[rest.length - 1] === \"-p\" || rest[rest.length - 1] === \"--print\")\n ) {\n prompt = a;\n }\n rest.push(a);\n }\n i += 1;\n }\n\n return {\n prompt,\n outputFormat,\n rest,\n headless,\n callerModel,\n callerMcpConfig,\n callerSystemPrompt,\n mcp,\n allowedTools,\n };\n}\n\n/**\n * Turn `-p \"<prompt>\"` into bare `-p` (same for --print): for stream-json\n * stdin runs the prompt moves to the first user message on stdin, so the\n * value must not stay on the command line. Only prompt values directly\n * following the flag are touched; everything else passes through.\n */\nexport function stripPromptValues(argv: string[]): string[] {\n const out: string[] = [];\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n const isPrint = a === \"-p\" || a === \"--print\";\n out.push(a);\n if (isPrint && i + 1 < argv.length && !argv[i + 1].startsWith(\"-\")) {\n i += 1; // drop the prompt value; the flag itself stays\n }\n }\n return out;\n}\n\n/** Collapse whitespace and cut to n chars with an ellipsis (label rendering). */\nexport function shortText(s: unknown, n: number): string {\n const t = String(s ?? \"\").split(/\\s+/).filter(Boolean).join(\" \");\n return t.length <= n ? t : t.slice(0, n - 1) + \"…\";\n}\n","/**\n * status.ts — the live per-worker status file.\n *\n * One JSON file per run, rewritten atomically on every turn, read by `ps`,\n * `follow`, the status line and the pane logic. Same field set the Python\n * runner wrote, plus `provider` (routing is multi-provider now) and `session`\n * (the AIBroker identity of the launching session, see scope.ts).\n */\n\nimport { existsSync, readFileSync, renameSync, writeFileSync, readdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { shortText } from \"./args.js\";\nimport { statusPath } from \"./paths.js\";\n\nexport interface WorkerSessionRef {\n /** AIBroker/iTerm session id (the iTerm UUID). */\n id: string;\n /** AIBroker name of that session, when it has one. */\n name: string;\n}\n\nexport interface WorkerStatus {\n id: string;\n pid: number;\n label: string;\n cwd: string;\n /** iTerm session id of the launching terminal (\"\" outside iTerm). */\n term: string;\n provider: string;\n model: string;\n state: \"running\" | \"done\" | \"failed\" | \"killed\" | \"lost\";\n started: string;\n updated: string;\n turns: number;\n tools: number;\n last: string;\n rc: number | null;\n secs: number | null;\n /** Launching session resolved through AIBroker, when it was. */\n session?: WorkerSessionRef | null;\n /** Claude Code session id (system/init) — what `resume` continues. */\n claudeSession?: string | null;\n /**\n * Claude Code session id of the orchestrator whose Bash launched this run,\n * when the status line's session map knew it (see scope.ts). Distinct from\n * claudeSession, which is the run's own session.\n */\n spawnerSession?: string | null;\n /** Context meter: tokens of the last assistant turn (input+cache+output). */\n contextTokens?: number | null;\n /** Context meter: window size (init model info or provider default). */\n contextWindow?: number | null;\n /** Chain this stage belongs to (the chain id), when it is a chain stage. */\n parent?: string;\n /** Class name of the chain stage (\"draft\", \"implement\", …). */\n stage?: string;\n /** Worktree this run executed in, when it got one (see worktree.ts). */\n worktreeDir?: string | null;\n /** Branch the worker committed on (worker/<id>), set on worktree runs. */\n branch?: string | null;\n /** Commit the branch started from (worktree base) for the commits count. */\n worktreeBase?: string | null;\n /** Commits the worker made on its branch; set with `branch` on success. */\n commits?: number | null;\n /** Set by `pai worker merge` once the branch landed in the original checkout. */\n merged?: boolean;\n /**\n * How the run came to be: \"spawn\" (a `pai worker run` subagent) or \"chat\"\n * (the terminal's interactive pane itself, tracked like a worker). Absent\n * on statuses written before the flag existed — read as a spawn.\n */\n origin?: \"spawn\" | \"chat\";\n}\n\n/** Label a worker gets when launched with neither --label nor a prompt. */\nexport const UNLABELED = \"unlabeled\";\n\n/**\n * Whether a status is the terminal's interactive chat pane, not a spawned\n * task worker. `origin` is the real discriminator; the fallback catches\n * running entries from before the flag existed (unlabeled, no turns yet).\n * Removable once every pane runs code that writes `origin`.\n */\nexport function isChatPane(s: Pick<WorkerStatus, \"origin\" | \"label\" | \"turns\">): boolean {\n return (\n s.origin === \"chat\" ||\n (!s.origin && (s.label === UNLABELED || s.label === \"(no prompt)\") && s.turns === 0)\n );\n}\n\n/** Context-meter percentage 0–100, null when the numbers are missing. */\nexport function contextPercent(s: Pick<WorkerStatus, \"contextTokens\" | \"contextWindow\">): number | null {\n if (!s.contextTokens || !s.contextWindow) return null;\n return Math.round((s.contextTokens / s.contextWindow) * 100);\n}\n\n/** Compact token count: 84k, 200k, 900. */\nexport function fmtContextK(n: number): string {\n return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);\n}\n\n/**\n * The one context-meter label, shared by every renderer so identical\n * numbers always render identically: `ctx 84k/200k (42%)` — used-style\n * percent. Empty when the numbers are missing (callers drop the part).\n */\nexport function contextLabel(\n s: Pick<WorkerStatus, \"contextTokens\" | \"contextWindow\">\n): string {\n const pct = contextPercent(s);\n if (pct === null) return \"\";\n return `ctx ${fmtContextK(s.contextTokens ?? 0)}/${fmtContextK(s.contextWindow ?? 0)} (${pct}%)`;\n}\n\n/** Timestamp format shared by status files and the ledger. */\nexport function nowStamp(d: Date = new Date()): string {\n const p = (n: number) => String(n).padStart(2, \"0\");\n return (\n `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ` +\n `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`\n );\n}\n\n// second-resolution ids repeat when two workers/chains start together in one\n// process; a repeat gets a monotonic suffix so files never collide\nlet lastId = \"\";\nlet idSeq = 0;\n\nexport function newWorkerId(d: Date = new Date(), pid = process.pid): string {\n const p = (n: number) => String(n).padStart(2, \"0\");\n const base = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${pid}`;\n if (base === lastId) {\n idSeq += 1;\n return `${base}-${String(idSeq).padStart(2, \"0\")}`;\n }\n lastId = base;\n idSeq = 0;\n return base;\n}\n\n/** Write status atomically (temp + rename) and stamp `updated`. */\nexport function saveStatus(logDir: string, status: WorkerStatus, d: Date = new Date()): void {\n status.updated = nowStamp(d);\n const path = statusPath(logDir, status.id);\n const tmp = `${path}.tmp`;\n writeFileSync(tmp, JSON.stringify(status), \"utf8\");\n renameSync(tmp, path);\n}\n\n/** Load every status file in the logDir, oldest id first, skipping damage. */\nexport function loadStatuses(logDir: string): WorkerStatus[] {\n if (!existsSync(logDir)) return [];\n const out: WorkerStatus[] = [];\n for (const name of readdirSync(logDir).sort()) {\n if (!name.endsWith(\".status\")) continue;\n try {\n out.push(JSON.parse(readFileSync(join(logDir, name), \"utf8\")) as WorkerStatus);\n } catch {\n // a half-written or damaged status file is not worth a crash\n }\n }\n return out;\n}\n\nexport function loadStatus(logDir: string, id: string): WorkerStatus | null {\n const path = statusPath(logDir, id);\n if (!existsSync(path)) return null;\n try {\n return JSON.parse(readFileSync(path, \"utf8\")) as WorkerStatus;\n } catch {\n return null;\n }\n}\n\nexport function alive(pid: number | null | undefined): boolean {\n if (!pid || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/** \"42s\" under 90s, \"7m\" above — the coarse age the table and bar show. */\nexport function ageOf(ts: string, now: Date = new Date()): string {\n const t = Date.parse(ts.replace(\" \", \"T\"));\n if (Number.isNaN(t)) return \"?\";\n const s = Math.max(0, Math.floor((now.getTime() - t) / 1000));\n return s < 90 ? `${s}s` : `${Math.floor(s / 60)}m`;\n}\n\n/** One-line description of a tool call, e.g. \"Bash: npm test\". */\nexport function describeTool(name: string, inp: unknown): string {\n if (typeof inp !== \"object\" || inp === null) return name;\n const i = inp as Record<string, unknown>;\n const get = (k: string) => (typeof i[k] === \"string\" ? (i[k] as string) : \"\");\n if (name === \"Bash\") return `Bash: ${shortText(get(\"command\"), 70)}`;\n if (name === \"Read\" || name === \"Edit\" || name === \"Write\" || name === \"MultiEdit\") {\n const file = get(\"file_path\").split(\"/\").pop() ?? \"\";\n return `${name}: ${file}`;\n }\n if (name === \"Grep\" || name === \"Glob\") return `${name}: ${shortText(get(\"pattern\"), 50)}`;\n if (name === \"WebSearch\") return `${name}: ${shortText(get(\"query\"), 50)}`;\n if (name === \"WebFetch\") return `${name}: ${shortText(get(\"url\"), 60)}`;\n return name;\n}\n","/**\n * scope.ts — which workers \"belong\" to the terminal asking about them.\n *\n * Two identities, in priority order:\n *\n * 1. AIBroker's session registry (~/.aibroker/session-names.json): iTerm\n * session UUID → session name. The launching session's UUID is stored in\n * each worker's status file as `session.id`, so every pane of a named\n * session sees its workers — regardless of tab layout.\n * 2. The iTerm tab key (`w<window>t<tab>` prefix of ITERM_SESSION_ID): the\n * original heuristic, kept as the fallback when no registry entry\n * matches (AIBroker absent, unnamed session, non-iTerm terminal).\n *\n * If neither is available, viewers fall back to \"all workers\".\n *\n * A third case: workers spawned from a Claude Code Bash tool (the\n * orchestrator pattern) have neither — Claude Code exports no terminal or\n * session identity to its Bash children. Those record `spawnerSession`, the\n * orchestrator's claude session id bridged through the status line's\n * session map (see below), so their tab still claims them.\n */\n\nimport { existsSync, readFileSync, renameSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { loadStatus, type WorkerStatus } from \"./status.js\";\n\nexport const AIBROKER_REGISTRY = join(homedir(), \".aibroker\", \"session-names.json\");\n\n/**\n * `w<window>t<tab>` prefix of an iTerm session id, \"\" if empty or malformed.\n * Panes split from the same tab share this prefix and differ only in `p<n>`.\n */\nexport function tabKey(term: string): string {\n if (!term) return \"\";\n const head = term.split(\"p\", 1)[0];\n const parts = head.slice(1).split(\"t\");\n if (head.startsWith(\"w\") && parts.length === 2 && parts.every((p) => /^\\d+$/.test(p))) {\n return head;\n }\n return \"\";\n}\n\n/** Tab key of the iTerm tab this process runs in, \"\" if not inside iTerm2. */\nexport function currentTabKey(env: NodeJS.ProcessEnv = process.env): string {\n return tabKey(env.ITERM_SESSION_ID ?? \"\");\n}\n\n/** The iTerm UUID part of an ITERM_SESSION_ID (after the last colon). */\nexport function itermUuid(term: string): string {\n if (!term) return \"\";\n return term.split(\":\").pop() ?? \"\";\n}\n\nexport interface SessionIdentity {\n id: string;\n name: string;\n}\n\n/**\n * Resolve the AIBroker session for an ITERM_SESSION_ID. Reads the persistent\n * name registry (the same store `aibroker_rename` writes); returns null when\n * AIBroker is absent or the session is not in it — callers then use the tab\n * key, which is the pre-AIBroker behaviour.\n */\nexport function resolveSession(\n term: string,\n registryPath: string = AIBROKER_REGISTRY\n): SessionIdentity | null {\n const uuid = itermUuid(term);\n if (!uuid) return null;\n let names: Record<string, unknown>;\n try {\n if (!existsSync(registryPath)) return null;\n names = JSON.parse(readFileSync(registryPath, \"utf8\")) as Record<string, unknown>;\n } catch {\n return null;\n }\n const name = names[uuid];\n if (typeof name !== \"string\" || !name) return null;\n return { id: uuid, name };\n}\n\n/**\n * True when `worker` was launched from `term` (the terminal asking about it).\n * Session-id match first; the tab key only when no registry entry matched —\n * a session without a name keeps the tab-scoped behaviour it always had.\n */\nexport function workerInScope(worker: WorkerStatus, term: string): boolean {\n if (!term) return false;\n const uuid = itermUuid(term);\n if (worker.session?.id && uuid) return worker.session.id === uuid;\n return tabKey(worker.term) === tabKey(term) && tabKey(term) !== \"\";\n}\n\n/**\n * Registry key for pane files: the session id when AIBroker knows this\n * terminal, else the tab key, else the raw iTerm UUID.\n */\nexport function scopeKey(term: string): string {\n const session = resolveSession(term);\n if (session) return session.id;\n return currentTabKey() || itermUuid(term);\n}\n\n/** `[Name]` when the worker has an AIBroker session name, else \"\". */\nexport function sessionTag(worker: { session?: { name?: string } | null }): string {\n return worker.session?.name ? `[${worker.session.name}]` : \"\";\n}\n\n// ---------------------------------------------------------------------------\n// spawner sessions — attribution for workers launched from a Claude Code Bash\n// ---------------------------------------------------------------------------\n\n/**\n * Claude Code exports neither ITERM_SESSION_ID nor its own session id to the\n * Bash tool, so a `pai worker run` from an orchestrator has no terminal to\n * record and its status would be unattributable. The status line is the one\n * process that sees both identities at once — the payload's session_id and\n * the tab's ITERM_SESSION_ID — so it bridges them: every refresh writes this\n * cwd-keyed map, and the runner reads it back at spawn time.\n */\nexport interface SessionMapEntry {\n session: string;\n ts: number;\n /**\n * The tab's ITERM_SESSION_ID, when the status line could see one — the\n * bridge back from a claude session id to the terminal it runs in\n * (supervision uses it to push worker events into that terminal).\n * Absent on entries written before the field existed.\n */\n term?: string;\n}\n\n/** How old a map entry may be for a spawn to adopt it (status lines refresh constantly while a session lives). */\nexport const SPAWNER_SESSION_TTL_MS = 10 * 60_000;\n\n/** Map entries not refreshed within this window are pruned on write. */\nconst SESSION_MAP_PRUNE_MS = 60 * 60_000;\n\nexport function sessionMapPath(logDir: string): string {\n return join(logDir, \"claude-session-map.json\");\n}\n\n/**\n * Record that the claude session `session` renders its status line in `cwd`\n * (and, when known, in iTerm session `term`). Never throws — a broken map\n * must not break the bar. Prunes stale entries; skips the write when the\n * entry is unchanged and fresh.\n */\nexport function recordSessionMapEntry(\n logDir: string,\n cwd: string,\n session: string,\n term?: string,\n now: number = Date.now()\n): void {\n if (!cwd || !session) return;\n const path = sessionMapPath(logDir);\n let map: Record<string, SessionMapEntry> = {};\n try {\n if (existsSync(path)) {\n map = JSON.parse(readFileSync(path, \"utf8\")) as Record<string, SessionMapEntry>;\n }\n } catch {\n map = {}; // a damaged map is rewritten, never fatal\n }\n const prev = map[cwd];\n if (\n prev &&\n prev.session === session &&\n (prev.term ?? \"\") === (term ?? \"\") &&\n now - prev.ts < 60_000\n ) {\n return;\n }\n const pruned: Record<string, SessionMapEntry> = {};\n for (const [dir, e] of Object.entries(map)) {\n if (now - e.ts < SESSION_MAP_PRUNE_MS) pruned[dir] = e;\n }\n pruned[cwd] = { session, ts: now, ...(term ? { term } : {}) };\n try {\n mkdirSync(logDir, { recursive: true });\n const tmp = `${path}.tmp`;\n writeFileSync(tmp, JSON.stringify(pruned), \"utf8\");\n renameSync(tmp, path);\n } catch {\n // unwritable log dir: no attribution this round, nothing else breaks\n }\n}\n\n/**\n * The claude session a new run was spawned by, when it can be known: a run\n * inside another worker inherits its spawner (chain stages keep the\n * orchestrator's tab that way); otherwise a fresh map entry for `cwd`.\n */\nexport function resolveSpawnerSession(\n logDir: string,\n cwd: string,\n env: NodeJS.ProcessEnv = process.env,\n now: number = Date.now()\n): string | null {\n const parentId = env.PAI_WORKER_ID;\n if (parentId) {\n const inherited = loadStatus(logDir, parentId)?.spawnerSession;\n if (inherited) return inherited;\n }\n const path = sessionMapPath(logDir);\n if (!existsSync(path) || !cwd) return null;\n try {\n const entry = (JSON.parse(readFileSync(path, \"utf8\")) as Record<string, SessionMapEntry>)[cwd];\n if (entry && entry.session && now - entry.ts < SPAWNER_SESSION_TTL_MS) return entry.session;\n } catch {\n // a damaged map simply attributes nothing\n }\n return null;\n}\n\n/**\n * The iTerm UUID a claude session last rendered its status line in, when the\n * map knows one: the freshest entry naming that session. This is the reverse\n * of resolveSpawnerSession — worker → orchestrator there, orchestrator →\n * terminal here — and it exists so daemon-side pushes (supervision) can reach\n * a session that has no iTerm identity of its own in any worker status.\n */\nexport function itermForClaudeSession(\n logDir: string,\n claudeSession: string,\n now: number = Date.now()\n): string | null {\n const path = sessionMapPath(logDir);\n if (!existsSync(path) || !claudeSession) return null;\n let map: Record<string, SessionMapEntry>;\n try {\n map = JSON.parse(readFileSync(path, \"utf8\")) as Record<string, SessionMapEntry>;\n } catch {\n return null;\n }\n let best: SessionMapEntry | null = null;\n for (const e of Object.values(map)) {\n if (e.session !== claudeSession || !e.term) continue;\n // a stale entry names a tab the session left; freshness orders them\n if (now - e.ts >= SESSION_MAP_PRUNE_MS) continue;\n if (!best || e.ts > best.ts) best = e;\n }\n const term = best?.term;\n return term ? itermUuid(term) : null;\n}\n"],"mappings":"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"}
|