@tekmidian/pai 0.64.0 → 0.65.0
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/README.md +19 -5
- package/dist/{aibroker-client-BpfuGPe5.mjs → aibroker-client-B8c42Lh8.mjs} +1 -1
- package/dist/{aibroker-client-DRld7g6z.mjs → aibroker-client-C5Fw7DNz.mjs} +10 -4
- package/dist/{aibroker-client-DRld7g6z.mjs.map → aibroker-client-C5Fw7DNz.mjs.map} +1 -1
- package/dist/{async-CY_cd_8j.mjs → async-CNn36gm4.mjs} +2 -2
- package/dist/{async-CY_cd_8j.mjs.map → async-CNn36gm4.mjs.map} +1 -1
- package/dist/{auto-route-CPlam4iv.mjs → auto-route-sLMU-NnM.mjs} +2 -2
- package/dist/{auto-route-CPlam4iv.mjs.map → auto-route-sLMU-NnM.mjs.map} +1 -1
- package/dist/{chain-o-g8AgE3.mjs → chain-C8QO8gwj.mjs} +4 -4
- package/dist/{chain-o-g8AgE3.mjs.map → chain-C8QO8gwj.mjs.map} +1 -1
- package/dist/cli/index.mjs +19 -19
- package/dist/cli/program.d.mts.map +1 -1
- package/dist/cli/program.mjs +19 -19
- package/dist/{clusters-DsMf20PP.mjs → clusters-PgIUYT_v.mjs} +1 -1
- package/dist/{clusters-DsMf20PP.mjs.map → clusters-PgIUYT_v.mjs.map} +1 -1
- package/dist/{config-2wkz744W.mjs → config-BbLFD7Uf.mjs} +3 -3
- package/dist/{config-2wkz744W.mjs.map → config-BbLFD7Uf.mjs.map} +1 -1
- package/dist/{config-CUTg6VDq.mjs → config-YinjgXEJ.mjs} +1 -1
- package/dist/{context-handover-cache-BOpjLsKe.mjs → context-handover-cache-mGxq2-8f.mjs} +11 -29
- package/dist/context-handover-cache-mGxq2-8f.mjs.map +1 -0
- package/dist/daemon/index.mjs +16 -16
- package/dist/daemon-CX9JomIJ.mjs +19 -0
- package/dist/{daemon-BYieXduQ.mjs → daemon-D2r1AQqE.mjs} +30 -30
- package/dist/{daemon-BYieXduQ.mjs.map → daemon-D2r1AQqE.mjs.map} +1 -1
- package/dist/daemon-mcp/index.mjs +17 -17
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/detector-C3Q7mxQU.mjs +3 -0
- package/dist/{detector-BAlmrLQ0.mjs → detector-CIHEXKsV.mjs} +1 -1
- package/dist/{detector-BAlmrLQ0.mjs.map → detector-CIHEXKsV.mjs.map} +1 -1
- package/dist/{embeddings-DK9XfQic.mjs → embeddings-BbNVXa_0.mjs} +1 -1
- package/dist/{embeddings-DK9XfQic.mjs.map → embeddings-BbNVXa_0.mjs.map} +1 -1
- package/dist/{embeddings-Xg8XLf2K.mjs → embeddings-CDfM63uC.mjs} +1 -1
- package/dist/{factory-BMK0tC1b.mjs → factory-CaswxuJ0.mjs} +1 -1
- package/dist/{factory-C4We3xR_.mjs → factory-y36qGegI.mjs} +13 -13
- package/dist/{factory-C4We3xR_.mjs.map → factory-y36qGegI.mjs.map} +1 -1
- package/dist/{fallback-ECiqCuh6.mjs → fallback-CWDQYmJi.mjs} +1205 -1204
- package/dist/fallback-CWDQYmJi.mjs.map +1 -0
- package/dist/{federation-db-CUgy0WYs.mjs → federation-db-BTyoufBh.mjs} +2 -2
- package/dist/{federation-db-CUgy0WYs.mjs.map → federation-db-BTyoufBh.mjs.map} +1 -1
- package/dist/federation-db-HfIFc7FG.mjs +3 -0
- package/dist/hooks/block-sleep-poll.mjs +36 -11
- package/dist/hooks/block-sleep-poll.mjs.map +2 -2
- package/dist/hooks/capture-all-events.mjs +3 -17
- package/dist/hooks/capture-all-events.mjs.map +3 -3
- package/dist/hooks/capture-session-summary.mjs +3 -17
- package/dist/hooks/capture-session-summary.mjs.map +3 -3
- package/dist/hooks/capture-tool-output.mjs +3 -17
- package/dist/hooks/capture-tool-output.mjs.map +3 -3
- package/dist/hooks/cleanup-session-files.mjs +3 -17
- package/dist/hooks/cleanup-session-files.mjs.map +2 -2
- package/dist/hooks/context-compression-hook.mjs +3 -17
- package/dist/hooks/context-compression-hook.mjs.map +3 -3
- package/dist/hooks/initialize-session.mjs +3 -17
- package/dist/hooks/initialize-session.mjs.map +2 -2
- package/dist/hooks/inject-observations.mjs +3 -17
- package/dist/hooks/inject-observations.mjs.map +2 -2
- package/dist/hooks/load-core-context.mjs +4 -22
- package/dist/hooks/load-core-context.mjs.map +2 -2
- package/dist/hooks/load-project-context.mjs +3 -17
- package/dist/hooks/load-project-context.mjs.map +3 -3
- package/dist/hooks/mcp-deferred-gate.mjs +3 -17
- package/dist/hooks/mcp-deferred-gate.mjs.map +2 -2
- package/dist/hooks/observe.mjs +3 -17
- package/dist/hooks/observe.mjs.map +2 -2
- package/dist/hooks/security-validator.mjs +3 -17
- package/dist/hooks/security-validator.mjs.map +3 -3
- package/dist/hooks/stop-hook.mjs +3 -17
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/hooks/sync-todo-to-md.mjs +3 -17
- package/dist/hooks/sync-todo-to-md.mjs.map +2 -2
- package/dist/hooks/worker-guard.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +4 -4
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/{indexer-backend-CJ0RGgMz.mjs → indexer-backend-Cnc7Tf5C.mjs} +2 -2
- package/dist/{ipc-client-EhWY8XL0.mjs → ipc-client-D16Xw6Uo.mjs} +2 -2
- package/dist/{ipc-client-EhWY8XL0.mjs.map → ipc-client-D16Xw6Uo.mjs.map} +1 -1
- package/dist/{kg-entity-1uqCnk4u.mjs → kg-entity-COTUj1ZC.mjs} +1 -1
- package/dist/{kg-entity-1uqCnk4u.mjs.map → kg-entity-COTUj1ZC.mjs.map} +1 -1
- package/dist/{latent-ideas-CSuKfiq3.mjs → latent-ideas-ByvMjVcb.mjs} +3 -3
- package/dist/{latent-ideas-CSuKfiq3.mjs.map → latent-ideas-ByvMjVcb.mjs.map} +1 -1
- package/dist/{link-boost-BtjzfE3c.mjs → link-boost-UiE-uooT.mjs} +1 -1
- package/dist/{link-boost-BtjzfE3c.mjs.map → link-boost-UiE-uooT.mjs.map} +1 -1
- package/dist/{main-resolver-DyOrwNYw.mjs → main-resolver-CDe7DCso.mjs} +6 -6
- package/dist/{main-resolver-DyOrwNYw.mjs.map → main-resolver-CDe7DCso.mjs.map} +1 -1
- package/dist/main-resolver-DI-A7lSO.mjs +7 -0
- package/dist/merge-2gqRPFu2.mjs +3 -0
- package/dist/{merge-D58n_-LZ.mjs → merge-DgU9OgZy.mjs} +1 -1
- package/dist/{merge-D58n_-LZ.mjs.map → merge-DgU9OgZy.mjs.map} +1 -1
- package/dist/module-paths-DdRzbkUI.mjs +44 -0
- package/dist/module-paths-DdRzbkUI.mjs.map +1 -0
- package/dist/{neighborhood-DNuRelRB.mjs → neighborhood-CvRHlqdR.mjs} +2 -2
- package/dist/{neighborhood-DNuRelRB.mjs.map → neighborhood-CvRHlqdR.mjs.map} +1 -1
- package/dist/{note-context-D9JZ4-7o.mjs → note-context-BrbfUIoP.mjs} +1 -1
- package/dist/{note-context-D9JZ4-7o.mjs.map → note-context-BrbfUIoP.mjs.map} +1 -1
- package/dist/{pai-home-UncxWxlX.mjs → pai-home-Cm9rcJgX.mjs} +1 -1
- package/dist/{pai-home-UncxWxlX.mjs.map → pai-home-Cm9rcJgX.mjs.map} +1 -1
- package/dist/{planner-DgC3oSvb.mjs → planner-8-shIa8t.mjs} +7 -7
- package/dist/{planner-DgC3oSvb.mjs.map → planner-8-shIa8t.mjs.map} +1 -1
- package/dist/{postgres-CcsRKir-.mjs → postgres-DYtZg7J7.mjs} +5 -13
- package/dist/{postgres-CcsRKir-.mjs.map → postgres-DYtZg7J7.mjs.map} +1 -1
- package/dist/{program-JhF2GgJY.mjs → program-DiktbiWa.mjs} +225 -245
- package/dist/program-DiktbiWa.mjs.map +1 -0
- package/dist/{query-feedback-BPa0dISE.mjs → query-feedback-BIaZTTFO.mjs} +2 -2
- package/dist/{query-feedback-BPa0dISE.mjs.map → query-feedback-BIaZTTFO.mjs.map} +1 -1
- package/dist/query-feedback-B_iigYj-.mjs +3 -0
- package/dist/{registry-db-DCzdI4sC.mjs → registry-db-C7voqML9.mjs} +2 -2
- package/dist/{registry-db-DCzdI4sC.mjs.map → registry-db-C7voqML9.mjs.map} +1 -1
- package/dist/registry-db-JHPhA8vF.mjs +3 -0
- package/dist/{registry-postgres-BUnkHKYs.mjs → registry-postgres-DSrkxqfF.mjs} +2 -2
- package/dist/{registry-postgres-BUnkHKYs.mjs.map → registry-postgres-DSrkxqfF.mjs.map} +1 -1
- package/dist/{registry-sqlite-CKYNwkUv.mjs → registry-sqlite-B2436JgX.mjs} +2 -2
- package/dist/{registry-sqlite-CKYNwkUv.mjs.map → registry-sqlite-B2436JgX.mjs.map} +1 -1
- package/dist/router-S6C5BxzZ.mjs +3 -0
- package/dist/{router-CGZATlvm.mjs → router-aqLMvNMg.mjs} +1 -1
- package/dist/{router-CGZATlvm.mjs.map → router-aqLMvNMg.mjs.map} +1 -1
- package/dist/{run-W2rV_9j0.mjs → run-DUZRSnIT.mjs} +120 -42
- package/dist/run-DUZRSnIT.mjs.map +1 -0
- package/dist/{run-env-Br4Bc7Vz.mjs → run-env-BHWnXqle.mjs} +3 -3
- package/dist/{run-env-Br4Bc7Vz.mjs.map → run-env-BHWnXqle.mjs.map} +1 -1
- package/dist/{runtime-paths-QGQJAekd.mjs → runtime-paths-CHTg3ywb.mjs} +1 -1
- package/dist/{runtime-paths-QGQJAekd.mjs.map → runtime-paths-CHTg3ywb.mjs.map} +1 -1
- package/dist/{server-B71rem4q.mjs → server-DlL1QI2k.mjs} +4 -5
- package/dist/server-DlL1QI2k.mjs.map +1 -0
- package/dist/{session-keepalive-Dgil9hjw.mjs → session-keepalive-CZUwp5IQ.mjs} +7 -7
- package/dist/{session-keepalive-Dgil9hjw.mjs.map → session-keepalive-CZUwp5IQ.mjs.map} +1 -1
- package/dist/skills/Art/SKILL.md +1 -1
- package/dist/skills/Observability/SKILL.md +4 -4
- package/dist/skills/Research/SKILL.md +1 -1
- package/dist/{sources-LcGptLA-.mjs → sources-DX4ElmrE.mjs} +1 -1
- package/dist/{sources-LcGptLA-.mjs.map → sources-DX4ElmrE.mjs.map} +1 -1
- package/dist/{sqlite-CAJcw2zL.mjs → sqlite-BrEu3avy.mjs} +3 -3
- package/dist/{sqlite-CAJcw2zL.mjs.map → sqlite-BrEu3avy.mjs.map} +1 -1
- package/dist/{state-S9wlKarB.mjs → state-DW8zdweW.mjs} +1 -1
- package/dist/{state-CHltNjXI.mjs → state-HyjqTihC.mjs} +1 -1
- package/dist/{state-CHltNjXI.mjs.map → state-HyjqTihC.mjs.map} +1 -1
- package/dist/{themes-D1FQFthd.mjs → themes-BgqahYVM.mjs} +2 -2
- package/dist/{themes-D1FQFthd.mjs.map → themes-BgqahYVM.mjs.map} +1 -1
- package/dist/{tools-BW7OXf-N.mjs → tools-BbqQIHPe.mjs} +4 -4
- package/dist/{tools-BFC-113F.mjs → tools-gMGDIpJ9.mjs} +18 -18
- package/dist/{tools-BFC-113F.mjs.map → tools-gMGDIpJ9.mjs.map} +1 -1
- package/dist/{trace-IDKK1VFs.mjs → trace-CmAB7iJZ.mjs} +1 -1
- package/dist/{trace-IDKK1VFs.mjs.map → trace-CmAB7iJZ.mjs.map} +1 -1
- package/dist/{vault-indexer-y6YC2-mh.mjs → vault-indexer-Ddq51X20.mjs} +1 -1
- package/dist/{vault-indexer-y6YC2-mh.mjs.map → vault-indexer-Ddq51X20.mjs.map} +1 -1
- package/dist/{wakeup-CNZQZzsY.mjs → wakeup-CZw88uXf.mjs} +3 -3
- package/dist/{wakeup-CNZQZzsY.mjs.map → wakeup-CZw88uXf.mjs.map} +1 -1
- package/dist/{work-queue-worker-Dy5FT2ak.mjs → work-queue-worker-DCzfH0d1.mjs} +4 -4
- package/dist/{work-queue-worker-Dy5FT2ak.mjs.map → work-queue-worker-DCzfH0d1.mjs.map} +1 -1
- package/dist/work-queue-worker-Taba_vcA.mjs +11 -0
- package/dist/{zettelkasten-Dx-63BEk.mjs → zettelkasten-Dwj67e4p.mjs} +4 -4
- package/dist/{zettelkasten-Dx-63BEk.mjs.map → zettelkasten-Dwj67e4p.mjs.map} +1 -1
- package/docker/docker-compose.yml +39 -0
- package/docs/commands/README.md +3 -3
- package/docs/commands/config.md +3 -3
- package/docs/commands/setup.md +9 -2
- package/docs/commands/worker.md +2 -2
- package/package.json +3 -2
- package/scripts/build-hooks.mjs +6 -6
- package/src/hooks/ts/lib/pai-paths-case.test.ts +12 -0
- package/src/hooks/ts/lib/pai-paths-import.test.ts +25 -0
- package/src/hooks/ts/lib/pai-paths.ts +5 -28
- package/src/hooks/ts/lib/sleep-poll-gate.test.ts +17 -1
- package/src/hooks/ts/lib/sleep-poll-gate.ts +21 -9
- package/src/hooks/ts/session-start/load-core-context.ts +2 -6
- package/src/hooks/ts/session-start/session-start-worker-guard.test.ts +24 -1
- package/dist/context-handover-cache-BOpjLsKe.mjs.map +0 -1
- package/dist/daemon-q3xjHwa4.mjs +0 -19
- package/dist/detector-C-YmZsOi.mjs +0 -3
- package/dist/fallback-ECiqCuh6.mjs.map +0 -1
- package/dist/federation-db-BZ8PyxFe.mjs +0 -3
- package/dist/main-resolver-vN09bkXv.mjs +0 -7
- package/dist/merge-CXGz5wXz.mjs +0 -3
- package/dist/program-JhF2GgJY.mjs.map +0 -1
- package/dist/query-feedback-BPHFFSu1.mjs +0 -3
- package/dist/registry-db-Hphlj3H4.mjs +0 -3
- package/dist/router-CoA8Uy1m.mjs +0 -3
- package/dist/run-W2rV_9j0.mjs.map +0 -1
- package/dist/server-B71rem4q.mjs.map +0 -1
- package/dist/work-queue-worker-CkShUa0z.mjs +0 -11
- /package/dist/{indexer-backend-DotpxHJd.mjs → indexer-backend-isSLg6yE.mjs} +0 -0
package/README.md
CHANGED
|
@@ -30,18 +30,31 @@ bun run build
|
|
|
30
30
|
### 2. Run the setup wizard
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
pai setup
|
|
33
|
+
pai setup # interactive
|
|
34
|
+
pai setup --yes # unattended: every prompt takes its default
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
The wizard walks you through: storage mode (SQLite or PostgreSQL), project directories, Obsidian vault path, MCP server registration, CLAUDE.md template, and daemon configuration. It's idempotent — safe to re-run anytime.
|
|
37
38
|
|
|
39
|
+
#### Linux, from zero (Ubuntu, apt Node, no Docker)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
sudo apt install -y nodejs npm tmux
|
|
43
|
+
npm i -g @anthropic-ai/claude-code && claude login
|
|
44
|
+
npm i -g @tekmidian/pai # set `npm config set prefix ~/.npm-global` first to avoid sudo
|
|
45
|
+
pai setup --yes --storage sqlite # skips macOS-only steps; installs the systemd user unit when systemd runs
|
|
46
|
+
pai worker on # route subagents to `pai worker run`
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Run `loginctl enable-linger "$USER"` so the daemon survives logout. Inside tmux, `pai worker run` opens its follow pane as a tmux split; elsewhere use `pai worker follow <id>`. Where systemd is absent (containers), start the daemon with `pai daemon serve`.
|
|
50
|
+
|
|
38
51
|
### 3. Start the daemon
|
|
39
52
|
|
|
40
53
|
```bash
|
|
41
54
|
pai daemon start
|
|
42
55
|
```
|
|
43
56
|
|
|
44
|
-
The daemon runs in the background via launchd, indexing your sessions and serving the MCP tools. It starts automatically on login.
|
|
57
|
+
The daemon runs in the background via launchd (macOS) or a systemd user unit (Linux), indexing your sessions and serving the MCP tools. It starts automatically on login.
|
|
45
58
|
|
|
46
59
|
### 4. Verify
|
|
47
60
|
|
|
@@ -874,7 +887,7 @@ Give Claude Code this prompt and it handles everything:
|
|
|
874
887
|
|
|
875
888
|
PAI offers two modes, and the setup wizard asks which you prefer.
|
|
876
889
|
|
|
877
|
-
**Simple mode (SQLite)** — Zero dependencies beyond
|
|
890
|
+
**Simple mode (SQLite)** — Zero dependencies beyond Node. Keyword search only. Great for trying it out or for systems without Docker.
|
|
878
891
|
|
|
879
892
|
**Full mode (PostgreSQL + pgvector)** — Adds semantic search and vector embeddings. Finds things by meaning, not just exact words. "How does the reconnection logic work?" finds the right session even if it never used those exact words. Requires Docker.
|
|
880
893
|
|
|
@@ -882,10 +895,11 @@ PAI offers two modes, and the setup wizard asks which you prefer.
|
|
|
882
895
|
|
|
883
896
|
## Prerequisites
|
|
884
897
|
|
|
885
|
-
- [
|
|
898
|
+
- [Node.js](https://nodejs.org) 20 or newer (22 from apt works) — the installed `pai` runs on Node
|
|
899
|
+
- [Bun](https://bun.sh) — only to build from a git checkout (development)
|
|
886
900
|
- [Docker](https://docs.docker.com/get-docker/) — only for full mode
|
|
887
901
|
- [Claude Code](https://claude.ai/code)
|
|
888
|
-
- macOS or Linux
|
|
902
|
+
- macOS or Linux (tmux for worker panes on Linux; iTerm2 on macOS)
|
|
889
903
|
|
|
890
904
|
---
|
|
891
905
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as switchToSession, i as sendToSession, n as fetchLiveSessions, r as revealItermSession, t as callAiBroker } from "./aibroker-client-
|
|
1
|
+
import { a as switchToSession, i as sendToSession, n as fetchLiveSessions, r as revealItermSession, t as callAiBroker } from "./aibroker-client-C5Fw7DNz.mjs";
|
|
2
2
|
|
|
3
3
|
export { callAiBroker, sendToSession };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as aibrokerSocketPath } from "./runtime-paths-
|
|
1
|
+
import { t as aibrokerSocketPath } from "./runtime-paths-CHTg3ywb.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { connect } from "node:net";
|
|
@@ -163,8 +163,10 @@ async function sendToSession(sessionId, text, timeoutMs = SEND_TIMEOUT_MS, opts)
|
|
|
163
163
|
async function switchToSession(target) {
|
|
164
164
|
try {
|
|
165
165
|
await callAiBroker("switch", { target });
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
if (process.platform === "darwin") {
|
|
167
|
+
const { spawnSync } = await import("node:child_process");
|
|
168
|
+
spawnSync("osascript", ["-e", "tell application \"iTerm\" to activate"], { stdio: "ignore" });
|
|
169
|
+
}
|
|
168
170
|
return { ok: true };
|
|
169
171
|
} catch (e) {
|
|
170
172
|
return {
|
|
@@ -183,6 +185,10 @@ async function switchToSession(target) {
|
|
|
183
185
|
* iTerm), this actually reveals the tab.
|
|
184
186
|
*/
|
|
185
187
|
function revealItermSession(itermSessionId) {
|
|
188
|
+
if (process.platform !== "darwin") return {
|
|
189
|
+
ok: false,
|
|
190
|
+
error: "iTerm2 is macOS only"
|
|
191
|
+
};
|
|
186
192
|
const script = `tell application "iTerm2"
|
|
187
193
|
activate
|
|
188
194
|
repeat with w in windows
|
|
@@ -220,4 +226,4 @@ end tell`;
|
|
|
220
226
|
|
|
221
227
|
//#endregion
|
|
222
228
|
export { switchToSession as a, sendToSession as i, fetchLiveSessions as n, revealItermSession as r, callAiBroker as t };
|
|
223
|
-
//# sourceMappingURL=aibroker-client-
|
|
229
|
+
//# sourceMappingURL=aibroker-client-C5Fw7DNz.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aibroker-client-DRld7g6z.mjs","names":[],"sources":["../src/cli/lib/aibroker-client.ts"],"sourcesContent":["/**\n * aibroker-client.ts — Lightweight IPC client for AIBroker daemon.\n *\n * Connects to the AIBroker Unix Domain Socket, sends a JSON-RPC request,\n * and reads a single newline-terminated JSON response. No class needed —\n * just a thin async function matching the WatcherClient protocol.\n *\n * Socket path: /tmp/aibroker.sock (default; override via AIBROKER_SOCKET env).\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport { spawnSync } from \"node:child_process\";\nimport { aibrokerSocketPath } from \"../../runtime-paths.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Lightweight session metadata returned by the AIBroker `sessions` IPC method.\n * Does NOT include scrollback content — use `session_content` for that.\n */\nexport interface AiBrokerSessionMeta {\n index: number;\n sessionId: string;\n /** iTerm2 tab title or profile name */\n name: string;\n /** PAI session name set via /Name; null for bare shells */\n paiName: string | null;\n atPrompt: boolean;\n /** \"claude\" for Claude Code panes, \"shell\" for bare terminals */\n kind: \"claude\" | \"shell\";\n /** Whether this is the currently focused pane */\n active: boolean;\n /** Last user prompt seen in scrollback (only populated by fetchLiveSessionsWithPrompts) */\n lastPrompt?: string;\n}\n\ninterface AiBrokerSessionsResult {\n sessions: AiBrokerSessionMeta[];\n}\n\n// ---------------------------------------------------------------------------\n// Core call\n// ---------------------------------------------------------------------------\n\n// AIBROKER_SOCKET stays first: it is AIBroker's own variable and a user who has\n// set it means it. The fallback goes through runtime-paths so the test guard\n// can redirect it — writing to a live socket from a test is the same hazard as\n// writing to live user state, and this one is not even ours.\nconst DEFAULT_SOCKET = process.env.AIBROKER_SOCKET ?? aibrokerSocketPath();\n\n/**\n * How long to wait for `send_to_session`, which must exceed the server's own\n * ack window or the caller gives up while the callee is still working.\n *\n * AIBroker waits up to SEND_ACK_TIMEOUT_MS (15s) for the submit confirmation —\n * the text leaving the input line — before answering. The generic client\n * timeout is 8s. So every send needing 8-15s to confirm reported\n * \"AIBroker IPC call timed out\" while succeeding, and the handler's eventual\n * `delivered: true` was written into a socket nobody was reading any more.\n *\n * Measured on `pai pause all` across 15 sessions: 9 reported failed, 8 of them\n * verifiably paused. A caller's deadline shorter than the callee's is not a\n * tuning question, it is a guaranteed false negative on every slow success.\n */\nconst SEND_TIMEOUT_MS = 30_000;\n\n/**\n * Call an AIBroker IPC method and return the result.\n *\n * Resolves with the `result` field of a successful response.\n * Rejects if the socket is not available, the call times out, or the\n * daemon returns an error.\n *\n * @param method IPC method name (e.g. \"session_content\", \"send_to_session\")\n * @param params Method parameters object\n * @param timeoutMs Connection + response timeout in milliseconds (default: 8 000)\n */\nexport function callAiBroker(\n method: string,\n params: Record<string, unknown> = {},\n timeoutMs = 8_000\n): Promise<Record<string, unknown>> {\n return new Promise((resolve, reject) => {\n const socketPath = DEFAULT_SOCKET;\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(err: Error | null, value?: Record<string, unknown>): void {\n if (done) return;\n done = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n try {\n socket.destroy();\n } catch {\n /* ignore */\n }\n if (err) reject(err);\n else resolve(value!);\n }\n\n const socket = connect(socketPath, () => {\n const request = {\n id: randomUUID(),\n sessionId: process.env.TERM_SESSION_ID ?? \"pai-cli\",\n method,\n params,\n };\n const itermId = process.env.ITERM_SESSION_ID;\n if (itermId) Object.assign(request, { itermSessionId: itermId });\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString(\"utf8\");\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n const line = buffer.slice(0, nl);\n\n let response: { ok: boolean; result?: Record<string, unknown>; error?: string };\n try {\n response = JSON.parse(line);\n } catch {\n finish(new Error(`AIBroker IPC parse error: ${line.slice(0, 120)}`));\n return;\n }\n\n if (!response.ok) {\n finish(new Error(response.error ?? \"AIBroker IPC call failed\"));\n } else {\n finish(null, response.result ?? {});\n }\n });\n\n socket.on(\"error\", (e: NodeJS.ErrnoException) => {\n if (e.code === \"ENOENT\" || e.code === \"ECONNREFUSED\") {\n finish(new Error(\"AIBroker not running (socket not found).\"));\n } else {\n finish(e);\n }\n });\n\n socket.on(\"end\", () => {\n if (!done) finish(new Error(\"AIBroker IPC connection closed before response.\"));\n });\n\n timer = setTimeout(\n () => finish(new Error(\"AIBroker IPC call timed out.\")),\n timeoutMs\n );\n });\n}\n\n// ---------------------------------------------------------------------------\n// Typed helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch all live iTerm2 session metadata from AIBroker via the `sessions` method.\n * Returns an empty array if AIBroker is not running.\n *\n * This is metadata-only (no scrollback). It is faster than `session_content`\n * and the correct source for listing/routing purposes.\n */\nexport async function fetchLiveSessions(): Promise<AiBrokerSessionMeta[]> {\n try {\n const result = await callAiBroker(\"sessions\", {});\n const sessions = (result as unknown as AiBrokerSessionsResult).sessions;\n if (!Array.isArray(sessions)) return [];\n return sessions;\n } catch {\n return [];\n }\n}\n\n/**\n * Fetch live sessions WITH the last user prompt extracted from terminal scrollback.\n *\n * Heavier than `fetchLiveSessions` (one IPC call returning full content for\n * every session), but yields a `lastPrompt` field useful for the unified\n * listing.\n */\nexport async function fetchLiveSessionsWithPrompts(): Promise<\n (AiBrokerSessionMeta & { lastPrompt?: string })[]\n> {\n // Get the basic metadata first (always works, fast).\n const metas = await fetchLiveSessions();\n if (metas.length === 0) return [];\n\n // Then enrich with last prompts via session_content (single call, all sessions).\n try {\n const contentResult = (await callAiBroker(\"session_content\", { lines: 60 })) as {\n sessions?: Array<{ sessionId: string; content?: string }>;\n };\n const contentMap = new Map<string, string>();\n for (const s of contentResult.sessions ?? []) {\n if (s.sessionId && typeof s.content === \"string\") {\n contentMap.set(s.sessionId, s.content);\n }\n }\n return metas.map((m) => {\n const content = contentMap.get(m.sessionId);\n const lastPrompt = content ? extractLastUserPrompt(content) : undefined;\n return { ...m, lastPrompt };\n });\n } catch {\n return metas.map((m) => ({ ...m }));\n }\n}\n\n/**\n * Extract the most-recent user prompt from a terminal scrollback string.\n *\n * Claude Code's TUI shows user input lines prefixed with `❯ `. We scan from\n * the bottom up, skipping the active input box (between the two horizontal\n * rule lines) and the statusline footer.\n */\nfunction extractLastUserPrompt(content: string): string | undefined {\n const lines = content.split(\"\\n\");\n // Walk bottom-up looking for the most recent user-typed line.\n // Claude Code marks user prompts with one of these prefixes:\n // ❯ <text> (current/recent prompt in the input box or scrollback)\n // > <text> (older variant)\n // The line MUST have non-empty content after the prompt symbol.\n // Skip the active input box (often empty `❯`) and statusline (👋 PAI CC...).\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n // Match ❯ or > (followed by space) then content\n const m = line.match(/^[❯>]\\s+(.+?)\\s*$/);\n if (!m) continue;\n let text = m[1].trim();\n if (!text) continue;\n // Skip lines that are just box-drawing or known UI noise\n if (/^[─━═]+/.test(text)) continue;\n if (text.startsWith(\"👋\")) continue;\n // Strip ANSI escape sequences that may leak in\n text = text.replace(/\\x1B\\[[0-9;]*[A-Za-z]/g, \"\").trim();\n if (text) return text;\n }\n return undefined;\n}\n\n/**\n * Send text to a specific AIBroker session by its iTerm2 sessionId.\n *\n * The wire keys are `target` and `message`, and neither is negotiable — the\n * handler rejects anything else with \"target is required\" before it looks at\n * the rest. This was written as `{ target: sessionId, message: text }`, which is the shape of\n * THIS function's own parameters rather than the shape of the IPC, so every\n * call failed identically. `pai pause all` was the only caller, so the live\n * path had never once executed: its --dry-run branch returns before sending,\n * and that was the only branch anyone had exercised.\n *\n * The caller must NOT terminate `text` with a newline. The transport sends with\n * `enter: true` and appends the Enter itself, so a trailing \\n submits twice —\n * once for the text and once for an empty prompt.\n */\nexport async function sendToSession(\n sessionId: string,\n text: string,\n timeoutMs = SEND_TIMEOUT_MS,\n opts?: { noReply?: boolean }\n): Promise<{ ok: boolean; error?: string; timedOut?: boolean }> {\n try {\n const params: Record<string, unknown> = { target: sessionId, message: text };\n if (opts?.noReply) params.noReply = true;\n await callAiBroker(\"send_to_session\", params, timeoutMs);\n return { ok: true };\n } catch (e) {\n const error = String(e);\n const timedOut = /timed out/i.test(error);\n // A timeout is NOT a delivery failure, and conflating the two is expensive.\n // The handler deposits into the target's mailbox BEFORE it waits for the\n // submit ack, so a send that times out has still been delivered — only the\n // confirmation is missing. Reported as a plain failure, the natural response\n // is to send again, which is how `pai pause all` produced nested\n // \"carried forward\" blocks in checkpoints that had already been written.\n // With noReply there is no mailbox deposit at all, so a timeout there really\n // is an unknown/failed delivery, not just a missing confirmation.\n if (opts?.noReply && timedOut) return { ok: false, timedOut: true, error };\n return { ok: false, error, timedOut };\n }\n}\n\n/**\n * Switch iTerm2 focus to the session identified by `target`.\n *\n * `target` can be a sessionId, paiName, or tab index number (as string).\n * After switching, activates the iTerm2 application itself so the window\n * comes to the foreground.\n *\n * Returns { ok: true } if AIBroker confirmed the switch, or\n * { ok: false, error } if AIBroker is not running or the session was not found.\n */\nexport async function switchToSession(target: string): Promise<{ ok: boolean; error?: string }> {\n try {\n await callAiBroker(\"switch\", { target });\n // Bring iTerm2 itself to the foreground (the IPC only selects the tab)\n const { spawnSync } = await import(\"node:child_process\");\n spawnSync(\"osascript\", [\"-e\", 'tell application \"iTerm\" to activate'], {\n stdio: \"ignore\",\n });\n return { ok: true };\n } catch (e) {\n return { ok: false, error: String(e) };\n }\n}\n\n/**\n * Bring the iTerm2 tab containing a specific session to the front.\n *\n * This is the mechanism AIBroker's screenshot path uses: match the session by\n * its iTerm2 session id (the `sessionId` returned by `fetchLiveSessions`, which\n * is iTerm's own `id of session`), then `select` its window, tab, and session.\n * Unlike the `switch` IPC (which only flips an internal index and never touches\n * iTerm), this actually reveals the tab.\n */\nexport function revealItermSession(itermSessionId: string): { ok: boolean; error?: string } {\n // Strip any \"iterm:\" style prefix, matching AIBroker's stripItermPrefix.\n const id = itermSessionId.replace(/^iterm:/i, \"\").trim();\n const script = `tell application \"iTerm2\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if id of s is \"${id}\" then\n select w\n select t\n select s\n return \"ok\"\n end if\n end repeat\n end repeat\n end repeat\n return \"not-found\"\nend tell`;\n try {\n const r = spawnSync(\"osascript\", [\"-e\", script], { encoding: \"utf8\" });\n if (r.status !== 0) {\n return { ok: false, error: (r.stderr || \"osascript failed\").trim() };\n }\n if ((r.stdout ?? \"\").trim() === \"ok\") return { ok: true };\n return { ok: false, error: \"session not found in any iTerm2 window\" };\n } catch (e) {\n return { ok: false, error: String(e) };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"aibroker-client-C5Fw7DNz.mjs","names":[],"sources":["../src/cli/lib/aibroker-client.ts"],"sourcesContent":["/**\n * aibroker-client.ts — Lightweight IPC client for AIBroker daemon.\n *\n * Connects to the AIBroker Unix Domain Socket, sends a JSON-RPC request,\n * and reads a single newline-terminated JSON response. No class needed —\n * just a thin async function matching the WatcherClient protocol.\n *\n * Socket path: /tmp/aibroker.sock (default; override via AIBROKER_SOCKET env).\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport { spawnSync } from \"node:child_process\";\nimport { aibrokerSocketPath } from \"../../runtime-paths.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Lightweight session metadata returned by the AIBroker `sessions` IPC method.\n * Does NOT include scrollback content — use `session_content` for that.\n */\nexport interface AiBrokerSessionMeta {\n index: number;\n sessionId: string;\n /** iTerm2 tab title or profile name */\n name: string;\n /** PAI session name set via /Name; null for bare shells */\n paiName: string | null;\n atPrompt: boolean;\n /** \"claude\" for Claude Code panes, \"shell\" for bare terminals */\n kind: \"claude\" | \"shell\";\n /** Whether this is the currently focused pane */\n active: boolean;\n /** Last user prompt seen in scrollback (only populated by fetchLiveSessionsWithPrompts) */\n lastPrompt?: string;\n}\n\ninterface AiBrokerSessionsResult {\n sessions: AiBrokerSessionMeta[];\n}\n\n// ---------------------------------------------------------------------------\n// Core call\n// ---------------------------------------------------------------------------\n\n// AIBROKER_SOCKET stays first: it is AIBroker's own variable and a user who has\n// set it means it. The fallback goes through runtime-paths so the test guard\n// can redirect it — writing to a live socket from a test is the same hazard as\n// writing to live user state, and this one is not even ours.\nconst DEFAULT_SOCKET = process.env.AIBROKER_SOCKET ?? aibrokerSocketPath();\n\n/**\n * How long to wait for `send_to_session`, which must exceed the server's own\n * ack window or the caller gives up while the callee is still working.\n *\n * AIBroker waits up to SEND_ACK_TIMEOUT_MS (15s) for the submit confirmation —\n * the text leaving the input line — before answering. The generic client\n * timeout is 8s. So every send needing 8-15s to confirm reported\n * \"AIBroker IPC call timed out\" while succeeding, and the handler's eventual\n * `delivered: true` was written into a socket nobody was reading any more.\n *\n * Measured on `pai pause all` across 15 sessions: 9 reported failed, 8 of them\n * verifiably paused. A caller's deadline shorter than the callee's is not a\n * tuning question, it is a guaranteed false negative on every slow success.\n */\nconst SEND_TIMEOUT_MS = 30_000;\n\n/**\n * Call an AIBroker IPC method and return the result.\n *\n * Resolves with the `result` field of a successful response.\n * Rejects if the socket is not available, the call times out, or the\n * daemon returns an error.\n *\n * @param method IPC method name (e.g. \"session_content\", \"send_to_session\")\n * @param params Method parameters object\n * @param timeoutMs Connection + response timeout in milliseconds (default: 8 000)\n */\nexport function callAiBroker(\n method: string,\n params: Record<string, unknown> = {},\n timeoutMs = 8_000\n): Promise<Record<string, unknown>> {\n return new Promise((resolve, reject) => {\n const socketPath = DEFAULT_SOCKET;\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(err: Error | null, value?: Record<string, unknown>): void {\n if (done) return;\n done = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n try {\n socket.destroy();\n } catch {\n /* ignore */\n }\n if (err) reject(err);\n else resolve(value!);\n }\n\n const socket = connect(socketPath, () => {\n const request = {\n id: randomUUID(),\n sessionId: process.env.TERM_SESSION_ID ?? \"pai-cli\",\n method,\n params,\n };\n const itermId = process.env.ITERM_SESSION_ID;\n if (itermId) Object.assign(request, { itermSessionId: itermId });\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString(\"utf8\");\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n const line = buffer.slice(0, nl);\n\n let response: { ok: boolean; result?: Record<string, unknown>; error?: string };\n try {\n response = JSON.parse(line);\n } catch {\n finish(new Error(`AIBroker IPC parse error: ${line.slice(0, 120)}`));\n return;\n }\n\n if (!response.ok) {\n finish(new Error(response.error ?? \"AIBroker IPC call failed\"));\n } else {\n finish(null, response.result ?? {});\n }\n });\n\n socket.on(\"error\", (e: NodeJS.ErrnoException) => {\n if (e.code === \"ENOENT\" || e.code === \"ECONNREFUSED\") {\n finish(new Error(\"AIBroker not running (socket not found).\"));\n } else {\n finish(e);\n }\n });\n\n socket.on(\"end\", () => {\n if (!done) finish(new Error(\"AIBroker IPC connection closed before response.\"));\n });\n\n timer = setTimeout(\n () => finish(new Error(\"AIBroker IPC call timed out.\")),\n timeoutMs\n );\n });\n}\n\n// ---------------------------------------------------------------------------\n// Typed helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch all live iTerm2 session metadata from AIBroker via the `sessions` method.\n * Returns an empty array if AIBroker is not running.\n *\n * This is metadata-only (no scrollback). It is faster than `session_content`\n * and the correct source for listing/routing purposes.\n */\nexport async function fetchLiveSessions(): Promise<AiBrokerSessionMeta[]> {\n try {\n const result = await callAiBroker(\"sessions\", {});\n const sessions = (result as unknown as AiBrokerSessionsResult).sessions;\n if (!Array.isArray(sessions)) return [];\n return sessions;\n } catch {\n return [];\n }\n}\n\n/**\n * Fetch live sessions WITH the last user prompt extracted from terminal scrollback.\n *\n * Heavier than `fetchLiveSessions` (one IPC call returning full content for\n * every session), but yields a `lastPrompt` field useful for the unified\n * listing.\n */\nexport async function fetchLiveSessionsWithPrompts(): Promise<\n (AiBrokerSessionMeta & { lastPrompt?: string })[]\n> {\n // Get the basic metadata first (always works, fast).\n const metas = await fetchLiveSessions();\n if (metas.length === 0) return [];\n\n // Then enrich with last prompts via session_content (single call, all sessions).\n try {\n const contentResult = (await callAiBroker(\"session_content\", { lines: 60 })) as {\n sessions?: Array<{ sessionId: string; content?: string }>;\n };\n const contentMap = new Map<string, string>();\n for (const s of contentResult.sessions ?? []) {\n if (s.sessionId && typeof s.content === \"string\") {\n contentMap.set(s.sessionId, s.content);\n }\n }\n return metas.map((m) => {\n const content = contentMap.get(m.sessionId);\n const lastPrompt = content ? extractLastUserPrompt(content) : undefined;\n return { ...m, lastPrompt };\n });\n } catch {\n return metas.map((m) => ({ ...m }));\n }\n}\n\n/**\n * Extract the most-recent user prompt from a terminal scrollback string.\n *\n * Claude Code's TUI shows user input lines prefixed with `❯ `. We scan from\n * the bottom up, skipping the active input box (between the two horizontal\n * rule lines) and the statusline footer.\n */\nfunction extractLastUserPrompt(content: string): string | undefined {\n const lines = content.split(\"\\n\");\n // Walk bottom-up looking for the most recent user-typed line.\n // Claude Code marks user prompts with one of these prefixes:\n // ❯ <text> (current/recent prompt in the input box or scrollback)\n // > <text> (older variant)\n // The line MUST have non-empty content after the prompt symbol.\n // Skip the active input box (often empty `❯`) and statusline (👋 PAI CC...).\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i].trim();\n // Match ❯ or > (followed by space) then content\n const m = line.match(/^[❯>]\\s+(.+?)\\s*$/);\n if (!m) continue;\n let text = m[1].trim();\n if (!text) continue;\n // Skip lines that are just box-drawing or known UI noise\n if (/^[─━═]+/.test(text)) continue;\n if (text.startsWith(\"👋\")) continue;\n // Strip ANSI escape sequences that may leak in\n text = text.replace(/\\x1B\\[[0-9;]*[A-Za-z]/g, \"\").trim();\n if (text) return text;\n }\n return undefined;\n}\n\n/**\n * Send text to a specific AIBroker session by its iTerm2 sessionId.\n *\n * The wire keys are `target` and `message`, and neither is negotiable — the\n * handler rejects anything else with \"target is required\" before it looks at\n * the rest. This was written as `{ target: sessionId, message: text }`, which is the shape of\n * THIS function's own parameters rather than the shape of the IPC, so every\n * call failed identically. `pai pause all` was the only caller, so the live\n * path had never once executed: its --dry-run branch returns before sending,\n * and that was the only branch anyone had exercised.\n *\n * The caller must NOT terminate `text` with a newline. The transport sends with\n * `enter: true` and appends the Enter itself, so a trailing \\n submits twice —\n * once for the text and once for an empty prompt.\n */\nexport async function sendToSession(\n sessionId: string,\n text: string,\n timeoutMs = SEND_TIMEOUT_MS,\n opts?: { noReply?: boolean }\n): Promise<{ ok: boolean; error?: string; timedOut?: boolean }> {\n try {\n const params: Record<string, unknown> = { target: sessionId, message: text };\n if (opts?.noReply) params.noReply = true;\n await callAiBroker(\"send_to_session\", params, timeoutMs);\n return { ok: true };\n } catch (e) {\n const error = String(e);\n const timedOut = /timed out/i.test(error);\n // A timeout is NOT a delivery failure, and conflating the two is expensive.\n // The handler deposits into the target's mailbox BEFORE it waits for the\n // submit ack, so a send that times out has still been delivered — only the\n // confirmation is missing. Reported as a plain failure, the natural response\n // is to send again, which is how `pai pause all` produced nested\n // \"carried forward\" blocks in checkpoints that had already been written.\n // With noReply there is no mailbox deposit at all, so a timeout there really\n // is an unknown/failed delivery, not just a missing confirmation.\n if (opts?.noReply && timedOut) return { ok: false, timedOut: true, error };\n return { ok: false, error, timedOut };\n }\n}\n\n/**\n * Switch iTerm2 focus to the session identified by `target`.\n *\n * `target` can be a sessionId, paiName, or tab index number (as string).\n * After switching, activates the iTerm2 application itself so the window\n * comes to the foreground.\n *\n * Returns { ok: true } if AIBroker confirmed the switch, or\n * { ok: false, error } if AIBroker is not running or the session was not found.\n */\nexport async function switchToSession(target: string): Promise<{ ok: boolean; error?: string }> {\n try {\n await callAiBroker(\"switch\", { target });\n // Bring iTerm2 itself to the foreground (the IPC only selects the tab)\n if (process.platform === \"darwin\") {\n const { spawnSync } = await import(\"node:child_process\");\n spawnSync(\"osascript\", [\"-e\", 'tell application \"iTerm\" to activate'], {\n stdio: \"ignore\",\n });\n }\n return { ok: true };\n } catch (e) {\n return { ok: false, error: String(e) };\n }\n}\n\n/**\n * Bring the iTerm2 tab containing a specific session to the front.\n *\n * This is the mechanism AIBroker's screenshot path uses: match the session by\n * its iTerm2 session id (the `sessionId` returned by `fetchLiveSessions`, which\n * is iTerm's own `id of session`), then `select` its window, tab, and session.\n * Unlike the `switch` IPC (which only flips an internal index and never touches\n * iTerm), this actually reveals the tab.\n */\nexport function revealItermSession(itermSessionId: string): { ok: boolean; error?: string } {\n if (process.platform !== \"darwin\") return { ok: false, error: \"iTerm2 is macOS only\" };\n // Strip any \"iterm:\" style prefix, matching AIBroker's stripItermPrefix.\n const id = itermSessionId.replace(/^iterm:/i, \"\").trim();\n const script = `tell application \"iTerm2\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if id of s is \"${id}\" then\n select w\n select t\n select s\n return \"ok\"\n end if\n end repeat\n end repeat\n end repeat\n return \"not-found\"\nend tell`;\n try {\n const r = spawnSync(\"osascript\", [\"-e\", script], { encoding: \"utf8\" });\n if (r.status !== 0) {\n return { ok: false, error: (r.stderr || \"osascript failed\").trim() };\n }\n if ((r.stdout ?? \"\").trim() === \"ok\") return { ok: true };\n return { ok: false, error: \"session not found in any iTerm2 window\" };\n } catch (e) {\n return { ok: false, error: String(e) };\n }\n}\n"],"mappings":"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"}
|
|
@@ -223,7 +223,7 @@ const DEFAULT_MAX_MILLIS_PER_PASS = 12e4;
|
|
|
223
223
|
* Returns the number of newly embedded chunks.
|
|
224
224
|
*/
|
|
225
225
|
async function embedChunksWithBackend(backend, shouldStop, projectNames, options) {
|
|
226
|
-
const { generateEmbeddings, serializeEmbedding } = await import("./embeddings-
|
|
226
|
+
const { generateEmbeddings, serializeEmbedding } = await import("./embeddings-CDfM63uC.mjs");
|
|
227
227
|
const maxChunks = options?.maxChunks ?? DEFAULT_MAX_CHUNKS_PER_PASS;
|
|
228
228
|
const maxMillis = options?.maxMillis ?? DEFAULT_MAX_MILLIS_PER_PASS;
|
|
229
229
|
const deadline = Date.now() + maxMillis;
|
|
@@ -297,4 +297,4 @@ async function indexAllWithBackend(backend, registry) {
|
|
|
297
297
|
|
|
298
298
|
//#endregion
|
|
299
299
|
export { indexProjectWithBackend as i, indexAllWithBackend as n, indexFileWithBackend as r, embedChunksWithBackend as t };
|
|
300
|
-
//# sourceMappingURL=async-
|
|
300
|
+
//# sourceMappingURL=async-CNn36gm4.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async-CY_cd_8j.mjs","names":[],"sources":["../src/memory/indexer/async.ts"],"sourcesContent":["/**\n * Backend-aware async indexer for PAI federation memory.\n *\n * Provides the same functionality as sync.ts but writes through the\n * StorageBackend interface instead of directly to the SQLite driver.\n * Used when the daemon is configured with the Postgres backend.\n *\n * The SQLite path still uses sync.ts directly (which is faster for SQLite\n * due to synchronous transactions).\n */\n\nimport { readFileSync, statSync, existsSync } from \"node:fs\";\nimport { join, relative, basename } from \"node:path\";\nimport type { StorageBackend, ChunkRow } from \"../../storage/interface.js\";\nimport type { RegistryBackend } from \"../../storage/registry-interface.js\";\nimport { chunkMarkdown, fileContentHash } from \"../chunker.js\";\nimport {\n sha256File,\n chunkId,\n detectTier,\n walkMdFiles,\n walkContentFiles,\n isPathTooBroadForContentScan,\n parseSessionTitleChunk,\n yieldToEventLoop,\n INDEX_YIELD_EVERY,\n} from \"./helpers.js\";\nimport type { IndexResult } from \"./types.js\";\n\nexport type { IndexResult };\n\n// ---------------------------------------------------------------------------\n// Single-file indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\n/**\n * Index a single file through the StorageBackend interface.\n * Returns true if the file was re-indexed (changed or new), false if skipped.\n */\nexport async function indexFileWithBackend(\n backend: StorageBackend,\n projectId: number,\n rootPath: string,\n relativePath: string,\n source: string,\n tier: string,\n): Promise<boolean> {\n const absPath = join(rootPath, relativePath);\n\n let content: string;\n let stat: ReturnType<typeof statSync>;\n try {\n content = readFileSync(absPath, \"utf8\");\n stat = statSync(absPath);\n } catch {\n return false;\n }\n\n const hash = fileContentHash(content);\n const mtime = Math.floor(stat.mtimeMs);\n const size = stat.size;\n\n // Change detection\n const existingHash = await backend.getFileHash(projectId, relativePath);\n if (existingHash === hash) return false;\n\n // Delete old chunks\n await backend.deleteChunksForFile(projectId, relativePath);\n\n // Chunk the content\n const rawChunks = chunkMarkdown(content);\n const updatedAt = Date.now();\n\n const chunks: ChunkRow[] = rawChunks.map((c, i) => ({\n id: chunkId(projectId, relativePath, i, c.startLine, c.endLine),\n projectId,\n source,\n tier,\n path: relativePath,\n startLine: c.startLine,\n endLine: c.endLine,\n hash: c.hash,\n text: c.text,\n updatedAt,\n embedding: null,\n }));\n\n // Insert chunks + update file record\n await backend.insertChunks(chunks);\n await backend.upsertFile({ projectId, path: relativePath, source, tier, hash, mtime, size });\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Project-level indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\nexport async function indexProjectWithBackend(\n backend: StorageBackend,\n projectId: number,\n rootPath: string,\n claudeNotesDir?: string | null,\n): Promise<IndexResult> {\n const result: IndexResult = { filesProcessed: 0, chunksCreated: 0, filesSkipped: 0 };\n\n const filesToIndex: Array<{ absPath: string; rootBase: string; source: string; tier: string }> = [];\n\n const rootMemoryMd = join(rootPath, \"MEMORY.md\");\n if (existsSync(rootMemoryMd)) {\n filesToIndex.push({ absPath: rootMemoryMd, rootBase: rootPath, source: \"memory\", tier: \"evergreen\" });\n }\n\n const memoryDir = join(rootPath, \"memory\");\n for (const absPath of walkMdFiles(memoryDir)) {\n const relPath = relative(rootPath, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"memory\", tier });\n }\n\n const notesDir = join(rootPath, \"Notes\");\n for (const absPath of walkMdFiles(notesDir)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic session-title chunks for Notes files\n {\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(notesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(rootPath, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n const titleChunk: ChunkRow = {\n id, projectId, source: \"notes\", tier: \"session\",\n path: syntheticPath, startLine: 0, endLine: 0,\n hash, text, updatedAt, embedding: null,\n };\n try {\n await backend.insertChunks([titleChunk]);\n } catch {\n // Skip title chunks that cause backend errors\n }\n }\n }\n\n if (!isPathTooBroadForContentScan(rootPath)) {\n for (const absPath of walkContentFiles(rootPath)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"content\", tier: \"topic\" });\n }\n }\n\n if (claudeNotesDir && claudeNotesDir !== notesDir) {\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n filesToIndex.push({ absPath, rootBase: claudeNotesDir, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic title chunks for claude notes dir\n {\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(claudeNotesDir, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n const titleChunk: ChunkRow = {\n id, projectId, source: \"notes\", tier: \"session\",\n path: syntheticPath, startLine: 0, endLine: 0,\n hash, text, updatedAt, embedding: null,\n };\n try {\n await backend.insertChunks([titleChunk]);\n } catch {\n // Skip title chunks that cause backend errors\n }\n }\n }\n\n if (claudeNotesDir.endsWith(\"/Notes\")) {\n const claudeProjectDir = claudeNotesDir.slice(0, -\"/Notes\".length);\n const claudeMemoryMd = join(claudeProjectDir, \"MEMORY.md\");\n if (existsSync(claudeMemoryMd)) {\n filesToIndex.push({ absPath: claudeMemoryMd, rootBase: claudeProjectDir, source: \"memory\", tier: \"evergreen\" });\n }\n const claudeMemoryDir = join(claudeProjectDir, \"memory\");\n for (const absPath of walkMdFiles(claudeMemoryDir)) {\n const relPath = relative(claudeProjectDir, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: claudeProjectDir, source: \"memory\", tier });\n }\n }\n }\n\n await yieldToEventLoop();\n\n let filesSinceYield = 0;\n\n for (const { absPath, rootBase, source, tier } of filesToIndex) {\n if (filesSinceYield >= INDEX_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const relPath = relative(rootBase, absPath);\n try {\n const changed = await indexFileWithBackend(backend, projectId, rootBase, relPath, source, tier);\n\n if (changed) {\n const ids = await backend.getChunkIds(projectId, relPath);\n result.filesProcessed++;\n result.chunksCreated += ids.length;\n } else {\n result.filesSkipped++;\n }\n } catch {\n // Skip files that cause backend errors (e.g. null bytes in Postgres)\n result.filesSkipped++;\n }\n }\n\n // Prune stale paths\n const livePaths = new Set<string>();\n for (const { absPath, rootBase } of filesToIndex) {\n livePaths.add(relative(rootBase, absPath));\n }\n\n const dbChunkPaths = await backend.getDistinctChunkPaths(projectId);\n\n const stalePaths: string[] = [];\n for (const p of dbChunkPaths) {\n const basePath = p.endsWith(\"::title\") ? p.slice(0, -\"::title\".length) : p;\n if (!livePaths.has(basePath)) {\n stalePaths.push(p);\n }\n }\n\n if (stalePaths.length > 0) {\n await backend.deletePaths(projectId, stalePaths);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Embedding generation via StorageBackend\n// ---------------------------------------------------------------------------\n\nconst EMBED_BATCH_SIZE = 50;\nconst EMBED_YIELD_EVERY = 1;\n\n/** Default ceiling on one pass. See EmbedPassOptions. */\nconst DEFAULT_MAX_CHUNKS_PER_PASS = 5_000;\n/** Default wall-clock ceiling on one pass, in milliseconds. */\nconst DEFAULT_MAX_MILLIS_PER_PASS = 120_000;\n\nexport interface EmbedPassOptions {\n /**\n * Stop the pass after this many chunks. Unbounded passes are the reason a\n * six-figure backlog starves the indexer: the daemon serialises indexing and\n * embedding against each other, so a pass that runs for hours means nothing\n * new gets indexed for hours. Bounding the pass costs nothing — every\n * embedding is written as it is produced, so the next pass resumes exactly\n * where this one stopped.\n */\n maxChunks?: number;\n /** Stop the pass after this much wall-clock time, whichever bound hits first. */\n maxMillis?: number;\n}\n\n/**\n * Generate and store embeddings for unembedded chunks via the StorageBackend.\n *\n * The pass is deliberately bounded (see EmbedPassOptions) and resumable: it\n * takes a slice of the backlog, embeds it, and returns so the scheduler can run\n * an index pass before coming back. Draining the whole backlog in one call\n * starves indexing for as long as the call runs.\n *\n * The optional `shouldStop` callback is checked between every batch. When it\n * returns true the embed loop exits early so the caller (e.g. the daemon\n * shutdown handler) can close the pool without racing against active queries.\n *\n * Returns the number of newly embedded chunks.\n */\nexport async function embedChunksWithBackend(\n backend: StorageBackend,\n shouldStop?: () => boolean,\n projectNames?: Map<number, string>,\n options?: EmbedPassOptions,\n): Promise<number> {\n const { generateEmbeddings, serializeEmbedding } = await import(\"../embeddings.js\");\n\n const maxChunks = options?.maxChunks ?? DEFAULT_MAX_CHUNKS_PER_PASS;\n const maxMillis = options?.maxMillis ?? DEFAULT_MAX_MILLIS_PER_PASS;\n const deadline = Date.now() + maxMillis;\n\n const rows = await backend.getUnembeddedChunkIds(undefined, maxChunks);\n if (rows.length === 0) return 0;\n\n const total = rows.length;\n let embedded = 0;\n\n // Build a summary of what needs embedding: count chunks per project_id\n const projectChunkCounts = new Map<number, { count: number; samplePath: string }>();\n for (const row of rows) {\n const entry = projectChunkCounts.get(row.project_id);\n if (entry) {\n entry.count++;\n } else {\n projectChunkCounts.set(row.project_id, { count: 1, samplePath: row.path });\n }\n }\n const pName = (pid: number) => projectNames?.get(pid) ?? `project ${pid}`;\n const projectSummary = Array.from(projectChunkCounts.entries())\n .map(([pid, { count, samplePath }]) => ` ${pName(pid)}: ${count} chunks (e.g. ${samplePath})`)\n .join(\"\\n\");\n process.stderr.write(\n `[pai-daemon] Embed pass: ${total} unembedded chunks across ${projectChunkCounts.size} project(s)\\n${projectSummary}\\n`\n );\n\n // Track current project for transition logging\n let currentProjectId = -1;\n let projectEmbedded = 0;\n\n for (let i = 0; i < rows.length; i += EMBED_BATCH_SIZE) {\n // Check cancellation between every batch before touching the pool again\n if (shouldStop?.()) {\n process.stderr.write(\n `[pai-daemon] Embed pass cancelled after ${embedded}/${total} chunks (shutdown requested)\\n`\n );\n break;\n }\n\n // Yield the daemon back to the indexer rather than run past the deadline.\n // The remaining rows are simply picked up by the next scheduled pass.\n if (Date.now() >= deadline) {\n process.stderr.write(\n `[pai-daemon] Embed pass yielding after ${embedded}/${total} chunks (${maxMillis}ms budget spent); resuming next pass\\n`\n );\n break;\n }\n\n const batch = rows.slice(i, i + EMBED_BATCH_SIZE);\n\n // Keep IPC responsive: the forward pass below is synchronous inside the\n // model, so yield before entering it rather than between chunks.\n await yieldToEventLoop();\n\n const vecs = await generateEmbeddings(batch.map((r) => r.text));\n // Issue the writes together rather than one round-trip at a time. Measured\n // on this machine the model embeds 40-67 chunks/s in isolation while the\n // daemon managed ~5/s: the difference was one sequential UPDATE per chunk,\n // so the pass spent most of its budget waiting on the network, not working.\n // Concurrency is bounded by the batch size, which the pool handles; the\n // SQLite backend is synchronous and simply ignores the difference.\n await Promise.all(\n batch.map((row, j) => backend.updateEmbedding(row.id, serializeEmbedding(vecs[j])))\n );\n\n // Attribute the batch to projects only after it is durably stored, and walk\n // it in order so a batch straddling a project boundary credits each side\n // correctly. Rows arrive ordered by project, so a boundary is a real\n // transition, not the per-chunk flapping this logging used to produce.\n for (const { project_id, path } of batch) {\n if (project_id !== currentProjectId) {\n if (currentProjectId !== -1) {\n process.stderr.write(\n `[pai-daemon] Finished ${pName(currentProjectId)}: ${projectEmbedded} chunks embedded\\n`\n );\n }\n const info = projectChunkCounts.get(project_id);\n process.stderr.write(\n `[pai-daemon] Embedding ${pName(project_id)} (${info?.count ?? \"?\"} chunks, starting at ${path})\\n`\n );\n currentProjectId = project_id;\n projectEmbedded = 0;\n }\n projectEmbedded++;\n }\n\n embedded += batch.length;\n\n // Log progress with current file path for context\n const lastChunk = batch[batch.length - 1];\n process.stderr.write(\n `[pai-daemon] Embedded ${embedded}/${total} chunks (${pName(lastChunk.project_id)}: ${lastChunk.path})\\n`\n );\n }\n\n // Log final project completion\n if (currentProjectId !== -1) {\n process.stderr.write(\n `[pai-daemon] Finished ${pName(currentProjectId)}: ${projectEmbedded} chunks embedded\\n`\n );\n }\n\n return embedded;\n}\n\n// ---------------------------------------------------------------------------\n// Global indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\nexport async function indexAllWithBackend(\n backend: StorageBackend,\n registry: RegistryBackend,\n): Promise<{ projects: number; result: IndexResult }> {\n const projects = await registry.listProjects({ status: \"active\" });\n\n const totals: IndexResult = { filesProcessed: 0, chunksCreated: 0, filesSkipped: 0 };\n\n for (const project of projects) {\n await yieldToEventLoop();\n const r = await indexProjectWithBackend(backend, project.id, project.root_path, project.claude_notes_dir);\n totals.filesProcessed += r.filesProcessed;\n totals.chunksCreated += r.chunksCreated;\n totals.filesSkipped += r.filesSkipped;\n }\n\n return { projects: projects.length, result: totals };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"async-CNn36gm4.mjs","names":[],"sources":["../src/memory/indexer/async.ts"],"sourcesContent":["/**\n * Backend-aware async indexer for PAI federation memory.\n *\n * Provides the same functionality as sync.ts but writes through the\n * StorageBackend interface instead of directly to the SQLite driver.\n * Used when the daemon is configured with the Postgres backend.\n *\n * The SQLite path still uses sync.ts directly (which is faster for SQLite\n * due to synchronous transactions).\n */\n\nimport { readFileSync, statSync, existsSync } from \"node:fs\";\nimport { join, relative, basename } from \"node:path\";\nimport type { StorageBackend, ChunkRow } from \"../../storage/interface.js\";\nimport type { RegistryBackend } from \"../../storage/registry-interface.js\";\nimport { chunkMarkdown, fileContentHash } from \"../chunker.js\";\nimport {\n sha256File,\n chunkId,\n detectTier,\n walkMdFiles,\n walkContentFiles,\n isPathTooBroadForContentScan,\n parseSessionTitleChunk,\n yieldToEventLoop,\n INDEX_YIELD_EVERY,\n} from \"./helpers.js\";\nimport type { IndexResult } from \"./types.js\";\n\nexport type { IndexResult };\n\n// ---------------------------------------------------------------------------\n// Single-file indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\n/**\n * Index a single file through the StorageBackend interface.\n * Returns true if the file was re-indexed (changed or new), false if skipped.\n */\nexport async function indexFileWithBackend(\n backend: StorageBackend,\n projectId: number,\n rootPath: string,\n relativePath: string,\n source: string,\n tier: string,\n): Promise<boolean> {\n const absPath = join(rootPath, relativePath);\n\n let content: string;\n let stat: ReturnType<typeof statSync>;\n try {\n content = readFileSync(absPath, \"utf8\");\n stat = statSync(absPath);\n } catch {\n return false;\n }\n\n const hash = fileContentHash(content);\n const mtime = Math.floor(stat.mtimeMs);\n const size = stat.size;\n\n // Change detection\n const existingHash = await backend.getFileHash(projectId, relativePath);\n if (existingHash === hash) return false;\n\n // Delete old chunks\n await backend.deleteChunksForFile(projectId, relativePath);\n\n // Chunk the content\n const rawChunks = chunkMarkdown(content);\n const updatedAt = Date.now();\n\n const chunks: ChunkRow[] = rawChunks.map((c, i) => ({\n id: chunkId(projectId, relativePath, i, c.startLine, c.endLine),\n projectId,\n source,\n tier,\n path: relativePath,\n startLine: c.startLine,\n endLine: c.endLine,\n hash: c.hash,\n text: c.text,\n updatedAt,\n embedding: null,\n }));\n\n // Insert chunks + update file record\n await backend.insertChunks(chunks);\n await backend.upsertFile({ projectId, path: relativePath, source, tier, hash, mtime, size });\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Project-level indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\nexport async function indexProjectWithBackend(\n backend: StorageBackend,\n projectId: number,\n rootPath: string,\n claudeNotesDir?: string | null,\n): Promise<IndexResult> {\n const result: IndexResult = { filesProcessed: 0, chunksCreated: 0, filesSkipped: 0 };\n\n const filesToIndex: Array<{ absPath: string; rootBase: string; source: string; tier: string }> = [];\n\n const rootMemoryMd = join(rootPath, \"MEMORY.md\");\n if (existsSync(rootMemoryMd)) {\n filesToIndex.push({ absPath: rootMemoryMd, rootBase: rootPath, source: \"memory\", tier: \"evergreen\" });\n }\n\n const memoryDir = join(rootPath, \"memory\");\n for (const absPath of walkMdFiles(memoryDir)) {\n const relPath = relative(rootPath, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"memory\", tier });\n }\n\n const notesDir = join(rootPath, \"Notes\");\n for (const absPath of walkMdFiles(notesDir)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic session-title chunks for Notes files\n {\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(notesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(rootPath, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n const titleChunk: ChunkRow = {\n id, projectId, source: \"notes\", tier: \"session\",\n path: syntheticPath, startLine: 0, endLine: 0,\n hash, text, updatedAt, embedding: null,\n };\n try {\n await backend.insertChunks([titleChunk]);\n } catch {\n // Skip title chunks that cause backend errors\n }\n }\n }\n\n if (!isPathTooBroadForContentScan(rootPath)) {\n for (const absPath of walkContentFiles(rootPath)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"content\", tier: \"topic\" });\n }\n }\n\n if (claudeNotesDir && claudeNotesDir !== notesDir) {\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n filesToIndex.push({ absPath, rootBase: claudeNotesDir, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic title chunks for claude notes dir\n {\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(claudeNotesDir, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n const titleChunk: ChunkRow = {\n id, projectId, source: \"notes\", tier: \"session\",\n path: syntheticPath, startLine: 0, endLine: 0,\n hash, text, updatedAt, embedding: null,\n };\n try {\n await backend.insertChunks([titleChunk]);\n } catch {\n // Skip title chunks that cause backend errors\n }\n }\n }\n\n if (claudeNotesDir.endsWith(\"/Notes\")) {\n const claudeProjectDir = claudeNotesDir.slice(0, -\"/Notes\".length);\n const claudeMemoryMd = join(claudeProjectDir, \"MEMORY.md\");\n if (existsSync(claudeMemoryMd)) {\n filesToIndex.push({ absPath: claudeMemoryMd, rootBase: claudeProjectDir, source: \"memory\", tier: \"evergreen\" });\n }\n const claudeMemoryDir = join(claudeProjectDir, \"memory\");\n for (const absPath of walkMdFiles(claudeMemoryDir)) {\n const relPath = relative(claudeProjectDir, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: claudeProjectDir, source: \"memory\", tier });\n }\n }\n }\n\n await yieldToEventLoop();\n\n let filesSinceYield = 0;\n\n for (const { absPath, rootBase, source, tier } of filesToIndex) {\n if (filesSinceYield >= INDEX_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const relPath = relative(rootBase, absPath);\n try {\n const changed = await indexFileWithBackend(backend, projectId, rootBase, relPath, source, tier);\n\n if (changed) {\n const ids = await backend.getChunkIds(projectId, relPath);\n result.filesProcessed++;\n result.chunksCreated += ids.length;\n } else {\n result.filesSkipped++;\n }\n } catch {\n // Skip files that cause backend errors (e.g. null bytes in Postgres)\n result.filesSkipped++;\n }\n }\n\n // Prune stale paths\n const livePaths = new Set<string>();\n for (const { absPath, rootBase } of filesToIndex) {\n livePaths.add(relative(rootBase, absPath));\n }\n\n const dbChunkPaths = await backend.getDistinctChunkPaths(projectId);\n\n const stalePaths: string[] = [];\n for (const p of dbChunkPaths) {\n const basePath = p.endsWith(\"::title\") ? p.slice(0, -\"::title\".length) : p;\n if (!livePaths.has(basePath)) {\n stalePaths.push(p);\n }\n }\n\n if (stalePaths.length > 0) {\n await backend.deletePaths(projectId, stalePaths);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Embedding generation via StorageBackend\n// ---------------------------------------------------------------------------\n\nconst EMBED_BATCH_SIZE = 50;\nconst EMBED_YIELD_EVERY = 1;\n\n/** Default ceiling on one pass. See EmbedPassOptions. */\nconst DEFAULT_MAX_CHUNKS_PER_PASS = 5_000;\n/** Default wall-clock ceiling on one pass, in milliseconds. */\nconst DEFAULT_MAX_MILLIS_PER_PASS = 120_000;\n\nexport interface EmbedPassOptions {\n /**\n * Stop the pass after this many chunks. Unbounded passes are the reason a\n * six-figure backlog starves the indexer: the daemon serialises indexing and\n * embedding against each other, so a pass that runs for hours means nothing\n * new gets indexed for hours. Bounding the pass costs nothing — every\n * embedding is written as it is produced, so the next pass resumes exactly\n * where this one stopped.\n */\n maxChunks?: number;\n /** Stop the pass after this much wall-clock time, whichever bound hits first. */\n maxMillis?: number;\n}\n\n/**\n * Generate and store embeddings for unembedded chunks via the StorageBackend.\n *\n * The pass is deliberately bounded (see EmbedPassOptions) and resumable: it\n * takes a slice of the backlog, embeds it, and returns so the scheduler can run\n * an index pass before coming back. Draining the whole backlog in one call\n * starves indexing for as long as the call runs.\n *\n * The optional `shouldStop` callback is checked between every batch. When it\n * returns true the embed loop exits early so the caller (e.g. the daemon\n * shutdown handler) can close the pool without racing against active queries.\n *\n * Returns the number of newly embedded chunks.\n */\nexport async function embedChunksWithBackend(\n backend: StorageBackend,\n shouldStop?: () => boolean,\n projectNames?: Map<number, string>,\n options?: EmbedPassOptions,\n): Promise<number> {\n const { generateEmbeddings, serializeEmbedding } = await import(\"../embeddings.js\");\n\n const maxChunks = options?.maxChunks ?? DEFAULT_MAX_CHUNKS_PER_PASS;\n const maxMillis = options?.maxMillis ?? DEFAULT_MAX_MILLIS_PER_PASS;\n const deadline = Date.now() + maxMillis;\n\n const rows = await backend.getUnembeddedChunkIds(undefined, maxChunks);\n if (rows.length === 0) return 0;\n\n const total = rows.length;\n let embedded = 0;\n\n // Build a summary of what needs embedding: count chunks per project_id\n const projectChunkCounts = new Map<number, { count: number; samplePath: string }>();\n for (const row of rows) {\n const entry = projectChunkCounts.get(row.project_id);\n if (entry) {\n entry.count++;\n } else {\n projectChunkCounts.set(row.project_id, { count: 1, samplePath: row.path });\n }\n }\n const pName = (pid: number) => projectNames?.get(pid) ?? `project ${pid}`;\n const projectSummary = Array.from(projectChunkCounts.entries())\n .map(([pid, { count, samplePath }]) => ` ${pName(pid)}: ${count} chunks (e.g. ${samplePath})`)\n .join(\"\\n\");\n process.stderr.write(\n `[pai-daemon] Embed pass: ${total} unembedded chunks across ${projectChunkCounts.size} project(s)\\n${projectSummary}\\n`\n );\n\n // Track current project for transition logging\n let currentProjectId = -1;\n let projectEmbedded = 0;\n\n for (let i = 0; i < rows.length; i += EMBED_BATCH_SIZE) {\n // Check cancellation between every batch before touching the pool again\n if (shouldStop?.()) {\n process.stderr.write(\n `[pai-daemon] Embed pass cancelled after ${embedded}/${total} chunks (shutdown requested)\\n`\n );\n break;\n }\n\n // Yield the daemon back to the indexer rather than run past the deadline.\n // The remaining rows are simply picked up by the next scheduled pass.\n if (Date.now() >= deadline) {\n process.stderr.write(\n `[pai-daemon] Embed pass yielding after ${embedded}/${total} chunks (${maxMillis}ms budget spent); resuming next pass\\n`\n );\n break;\n }\n\n const batch = rows.slice(i, i + EMBED_BATCH_SIZE);\n\n // Keep IPC responsive: the forward pass below is synchronous inside the\n // model, so yield before entering it rather than between chunks.\n await yieldToEventLoop();\n\n const vecs = await generateEmbeddings(batch.map((r) => r.text));\n // Issue the writes together rather than one round-trip at a time. Measured\n // on this machine the model embeds 40-67 chunks/s in isolation while the\n // daemon managed ~5/s: the difference was one sequential UPDATE per chunk,\n // so the pass spent most of its budget waiting on the network, not working.\n // Concurrency is bounded by the batch size, which the pool handles; the\n // SQLite backend is synchronous and simply ignores the difference.\n await Promise.all(\n batch.map((row, j) => backend.updateEmbedding(row.id, serializeEmbedding(vecs[j])))\n );\n\n // Attribute the batch to projects only after it is durably stored, and walk\n // it in order so a batch straddling a project boundary credits each side\n // correctly. Rows arrive ordered by project, so a boundary is a real\n // transition, not the per-chunk flapping this logging used to produce.\n for (const { project_id, path } of batch) {\n if (project_id !== currentProjectId) {\n if (currentProjectId !== -1) {\n process.stderr.write(\n `[pai-daemon] Finished ${pName(currentProjectId)}: ${projectEmbedded} chunks embedded\\n`\n );\n }\n const info = projectChunkCounts.get(project_id);\n process.stderr.write(\n `[pai-daemon] Embedding ${pName(project_id)} (${info?.count ?? \"?\"} chunks, starting at ${path})\\n`\n );\n currentProjectId = project_id;\n projectEmbedded = 0;\n }\n projectEmbedded++;\n }\n\n embedded += batch.length;\n\n // Log progress with current file path for context\n const lastChunk = batch[batch.length - 1];\n process.stderr.write(\n `[pai-daemon] Embedded ${embedded}/${total} chunks (${pName(lastChunk.project_id)}: ${lastChunk.path})\\n`\n );\n }\n\n // Log final project completion\n if (currentProjectId !== -1) {\n process.stderr.write(\n `[pai-daemon] Finished ${pName(currentProjectId)}: ${projectEmbedded} chunks embedded\\n`\n );\n }\n\n return embedded;\n}\n\n// ---------------------------------------------------------------------------\n// Global indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\nexport async function indexAllWithBackend(\n backend: StorageBackend,\n registry: RegistryBackend,\n): Promise<{ projects: number; result: IndexResult }> {\n const projects = await registry.listProjects({ status: \"active\" });\n\n const totals: IndexResult = { filesProcessed: 0, chunksCreated: 0, filesSkipped: 0 };\n\n for (const project of projects) {\n await yieldToEventLoop();\n const r = await indexProjectWithBackend(backend, project.id, project.root_path, project.claude_notes_dir);\n totals.filesProcessed += r.filesProcessed;\n totals.chunksCreated += r.chunksCreated;\n totals.filesSkipped += r.filesSkipped;\n }\n\n return { projects: projects.length, result: totals };\n}\n"],"mappings":"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"}
|
|
@@ -25,7 +25,7 @@ async function autoRoute(registryBackend, federation, cwd, context) {
|
|
|
25
25
|
const markerResult = await findMarkerUpward(registryBackend, target);
|
|
26
26
|
if (markerResult) return markerResult;
|
|
27
27
|
if (context && context.trim().length > 0) {
|
|
28
|
-
const { detectTopicShift } = await import("./detector-
|
|
28
|
+
const { detectTopicShift } = await import("./detector-C3Q7mxQU.mjs");
|
|
29
29
|
const topicResult = await detectTopicShift(registryBackend, federation, {
|
|
30
30
|
context,
|
|
31
31
|
threshold: .5
|
|
@@ -97,4 +97,4 @@ function formatAutoRouteJson(result) {
|
|
|
97
97
|
|
|
98
98
|
//#endregion
|
|
99
99
|
export { autoRoute, formatAutoRouteJson };
|
|
100
|
-
//# sourceMappingURL=auto-route-
|
|
100
|
+
//# sourceMappingURL=auto-route-sLMU-NnM.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-route-
|
|
1
|
+
{"version":3,"file":"auto-route-sLMU-NnM.mjs","names":[],"sources":["../src/session/auto-route.ts"],"sourcesContent":["/**\n * Auto-route: automatic project routing suggestion on session start.\n *\n * Given a working directory (and optional conversation context), determine\n * which registered project the session belongs to.\n *\n * Strategy (in priority order):\n * 1. Path match — exact or parent-directory match in the project registry\n * 2. Marker walk — walk up from cwd looking for Notes/PAI.md, resolve slug\n * 3. Topic match — BM25 keyword search against memory (requires context text)\n *\n * The function is stateless and works directly against the registry/storage\n * backends (no daemon required), making it fast and safe to call during\n * session startup.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport type { RegistryBackend, Project } from \"../storage/registry-interface.js\";\nimport { resolve, dirname } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { readPaiMarker } from \"../registry/pai-marker.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type AutoRouteMethod = \"path\" | \"marker\" | \"topic\";\n\nexport interface AutoRouteResult {\n /** Project slug */\n slug: string;\n /** Human-readable project name */\n display_name: string;\n /** Absolute path to the project root */\n root_path: string;\n /** How the project was detected */\n method: AutoRouteMethod;\n /** Confidence [0,1]: 1.0 for path/marker matches, BM25 fraction for topic */\n confidence: number;\n}\n\n// ---------------------------------------------------------------------------\n// Core function\n// ---------------------------------------------------------------------------\n\n/**\n * Determine which project a session should be routed to.\n *\n * @param registryBackend Registry backend (projects, sessions, ...)\n * @param federation Memory storage backend (needed only for topic fallback)\n * @param cwd Working directory to detect from (defaults to process.cwd())\n * @param context Optional conversation text for topic-based fallback\n * @returns Best project match, or null if nothing matched\n */\nexport async function autoRoute(\n registryBackend: RegistryBackend,\n federation: StorageBackend,\n cwd?: string,\n context?: string\n): Promise<AutoRouteResult | null> {\n const target = resolve(cwd ?? process.cwd());\n\n // -------------------------------------------------------------------------\n // Strategy 1: Path match via registry\n // -------------------------------------------------------------------------\n\n const pathMatch = await findProjectByPath(registryBackend, target);\n\n if (pathMatch) {\n return {\n slug: pathMatch.slug,\n display_name: pathMatch.display_name,\n root_path: pathMatch.root_path,\n method: \"path\",\n confidence: 1.0,\n };\n }\n\n // -------------------------------------------------------------------------\n // Strategy 2: PAI.md marker file walk\n //\n // Walk up from cwd, checking <dir>/Notes/PAI.md at each level.\n // Once found, resolve the slug against the registry to get full project info.\n // -------------------------------------------------------------------------\n\n const markerResult = await findMarkerUpward(registryBackend, target);\n if (markerResult) {\n return markerResult;\n }\n\n // -------------------------------------------------------------------------\n // Strategy 3: Topic detection (requires context text)\n // -------------------------------------------------------------------------\n\n if (context && context.trim().length > 0) {\n // Lazy import to avoid bundler pulling in daemon/index.mjs at module load time\n const { detectTopicShift } = await import(\"../topics/detector.js\");\n const topicResult = await detectTopicShift(\n registryBackend,\n federation,\n {\n context,\n threshold: 0.5, // Lower threshold for initial routing (vs shift detection)\n }\n );\n\n if (topicResult.suggestedProject && topicResult.confidence > 0) {\n // Look up the full project info from the registry\n const projectRow = await registryBackend.getProjectBySlug(topicResult.suggestedProject);\n\n if (projectRow && projectRow.status !== \"archived\") {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"topic\",\n confidence: topicResult.confidence,\n };\n }\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Path-match helper\n// ---------------------------------------------------------------------------\n\n/**\n * Exact or parent-directory match against every non-archived project,\n * longest root_path wins. Mirrors src/cli/commands/detect.ts's detectProject()\n * matching logic (that file also enriches with session stats, which autoRoute\n * does not need).\n */\nasync function findProjectByPath(\n registryBackend: RegistryBackend,\n target: string\n): Promise<Project | null> {\n const projects = await registryBackend.listProjectsByPathLengthDesc({ excludeArchived: true });\n\n for (const p of projects) {\n const root = resolve(p.root_path);\n if (target === root) return p;\n if (target.startsWith(root + \"/\")) return p;\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Marker walk helper\n// ---------------------------------------------------------------------------\n\n/**\n * Walk up the directory tree from `startDir`, checking each level for a\n * `Notes/PAI.md` file. If found, read the slug and look up the project.\n *\n * Stops at the filesystem root or after 20 levels (safety guard).\n */\nasync function findMarkerUpward(\n registryBackend: RegistryBackend,\n startDir: string\n): Promise<AutoRouteResult | null> {\n let current = startDir;\n let depth = 0;\n\n while (depth < 20) {\n const markerPath = `${current}/Notes/PAI.md`;\n\n if (existsSync(markerPath)) {\n const marker = readPaiMarker(current);\n\n if (marker && marker.status !== \"archived\") {\n // Resolve slug to full project info in the registry\n const projectRow = await registryBackend.getProjectBySlug(marker.slug);\n\n if (projectRow && projectRow.status !== \"archived\") {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"marker\",\n confidence: 1.0,\n };\n }\n }\n }\n\n const parent = dirname(current);\n if (parent === current) break; // Reached filesystem root\n current = parent;\n depth++;\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Format helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format an AutoRouteResult as a human-readable string for CLI output.\n */\nexport function formatAutoRoute(result: AutoRouteResult): string {\n const lines: string[] = [\n `slug: ${result.slug}`,\n `display_name: ${result.display_name}`,\n `root_path: ${result.root_path}`,\n `method: ${result.method}`,\n `confidence: ${(result.confidence * 100).toFixed(0)}%`,\n ];\n return lines.join(\"\\n\");\n}\n\n/**\n * Format an AutoRouteResult as JSON for machine consumption.\n */\nexport function formatAutoRouteJson(result: AutoRouteResult): string {\n return JSON.stringify(result, null, 2);\n}\n"],"mappings":";;;;;;;;;;;;;;AAsDA,eAAsB,UACpB,iBACA,YACA,KACA,SACiC;CACjC,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,CAAC;CAM5C,MAAM,YAAY,MAAM,kBAAkB,iBAAiB,OAAO;AAElE,KAAI,UACF,QAAO;EACL,MAAM,UAAU;EAChB,cAAc,UAAU;EACxB,WAAW,UAAU;EACrB,QAAQ;EACR,YAAY;EACb;CAUH,MAAM,eAAe,MAAM,iBAAiB,iBAAiB,OAAO;AACpE,KAAI,aACF,QAAO;AAOT,KAAI,WAAW,QAAQ,MAAM,CAAC,SAAS,GAAG;EAExC,MAAM,EAAE,qBAAqB,MAAM,OAAO;EAC1C,MAAM,cAAc,MAAM,iBACxB,iBACA,YACA;GACE;GACA,WAAW;GACZ,CACF;AAED,MAAI,YAAY,oBAAoB,YAAY,aAAa,GAAG;GAE9D,MAAM,aAAa,MAAM,gBAAgB,iBAAiB,YAAY,iBAAiB;AAEvF,OAAI,cAAc,WAAW,WAAW,WACtC,QAAO;IACL,MAAM,WAAW;IACjB,cAAc,WAAW;IACzB,WAAW,WAAW;IACtB,QAAQ;IACR,YAAY,YAAY;IACzB;;;AAKP,QAAO;;;;;;;;AAaT,eAAe,kBACb,iBACA,QACyB;CACzB,MAAM,WAAW,MAAM,gBAAgB,6BAA6B,EAAE,iBAAiB,MAAM,CAAC;AAE9F,MAAK,MAAM,KAAK,UAAU;EACxB,MAAM,OAAO,QAAQ,EAAE,UAAU;AACjC,MAAI,WAAW,KAAM,QAAO;AAC5B,MAAI,OAAO,WAAW,OAAO,IAAI,CAAE,QAAO;;AAG5C,QAAO;;;;;;;;AAaT,eAAe,iBACb,iBACA,UACiC;CACjC,IAAI,UAAU;CACd,IAAI,QAAQ;AAEZ,QAAO,QAAQ,IAAI;AAGjB,MAAI,WAFe,GAAG,QAAQ,eAEJ,EAAE;GAC1B,MAAM,SAAS,cAAc,QAAQ;AAErC,OAAI,UAAU,OAAO,WAAW,YAAY;IAE1C,MAAM,aAAa,MAAM,gBAAgB,iBAAiB,OAAO,KAAK;AAEtE,QAAI,cAAc,WAAW,WAAW,WACtC,QAAO;KACL,MAAM,WAAW;KACjB,cAAc,WAAW;KACzB,WAAW,WAAW;KACtB,QAAQ;KACR,YAAY;KACb;;;EAKP,MAAM,SAAS,QAAQ,QAAQ;AAC/B,MAAI,WAAW,QAAS;AACxB,YAAU;AACV;;AAGF,QAAO;;;;;AAwBT,SAAgB,oBAAoB,QAAiC;AACnE,QAAO,KAAK,UAAU,QAAQ,MAAM,EAAE"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { E as readWorkersSection } from "./run-env-
|
|
2
|
-
import { V as newWorkerId, Y as shortText, k as appendLedger, n as runWorker } from "./run-
|
|
3
|
-
import { h as ledgerPath, v as workersLogDir } from "./server-
|
|
1
|
+
import { E as readWorkersSection } from "./run-env-BHWnXqle.mjs";
|
|
2
|
+
import { V as newWorkerId, Y as shortText, k as appendLedger, n as runWorker } from "./run-DUZRSnIT.mjs";
|
|
3
|
+
import { h as ledgerPath, v as workersLogDir } from "./server-DlL1QI2k.mjs";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
|
|
@@ -162,4 +162,4 @@ async function runChain(opts, deps = {}) {
|
|
|
162
162
|
|
|
163
163
|
//#endregion
|
|
164
164
|
export { swapPromptArg as n, runChain as t };
|
|
165
|
-
//# sourceMappingURL=chain-
|
|
165
|
+
//# sourceMappingURL=chain-C8QO8gwj.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chain-o-g8AgE3.mjs","names":[],"sources":["../src/workers/chain.ts"],"sourcesContent":["/**\n * chain.ts — draft-then-implement chains: `--chain draft,implement[,review]`.\n *\n * The chain runs each stage as its own worker (own id, own pane, `parent` set\n * to the chain id), so `ps` shows the chain as a tree and every stage can be\n * followed, replayed and said to like any other worker:\n *\n * - draft turns the operator's brief into a full spec file under\n * <logDir>/specs/<chain id>.md (goal, constraints, files likely\n * touched, acceptance checks, verification commands);\n * - any other stage (implement, plan, …) runs with that spec as its prompt\n * and the original brief attached;\n * - review reads the spec and the working-tree diff and produces the\n * structured report.\n *\n * A stage that fails (or a draft that produces no spec file) stops the chain;\n * the caller then writes the spec itself and re-runs without the draft stage.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { readWorkersSection } from \"./config.js\";\nimport { appendLedger } from \"./ledger.js\";\nimport { ledgerPath, workersLogDir } from \"./paths.js\";\nimport { newWorkerId } from \"./status.js\";\nimport { shortText } from \"./args.js\";\nimport { runWorker, type RunOptions } from \"./run.js\";\n\n/** Where a chain's spec file lives: <logDir>/specs/<chain id>.md. */\nexport function specPathFor(logDir: string, chainId: string): string {\n return join(logDir, \"specs\", `${chainId}.md`);\n}\n\n/**\n * Replace the caller's -p value with `prompt`, dropping every existing\n * -p/--print pair first (two -p flags on one claude command line are an error,\n * so the chain must never leave the brief in place when swapping prompts).\n */\nexport function swapPromptArg(claudeArgs: string[], prompt: string): string[] {\n const out: string[] = [];\n for (let i = 0; i < claudeArgs.length; i++) {\n const a = claudeArgs[i];\n if (a === \"-p\" || a === \"--print\") {\n if (i + 1 < claudeArgs.length && !claudeArgs[i + 1].startsWith(\"-\")) i += 1;\n continue;\n }\n out.push(a);\n }\n out.push(\"-p\", prompt);\n return out;\n}\n\nexport function draftPrompt(brief: string, specPath: string): string {\n return [\n \"You are the DRAFT stage of a worker chain. Turn the operator's brief below into a full implementation spec and write it to\",\n specPath,\n \"with the Write tool.\",\n \"\",\n \"Use exactly these five sections as Markdown headings:\",\n \"# Goal\",\n \"# Constraints\",\n \"# Files likely touched\",\n \"# Acceptance checks\",\n \"# Verification commands\",\n \"\",\n \"Read the repository first (Glob/Grep/Read) so the spec names real files and real commands. Do not implement anything.\",\n \"\",\n \"## Operator brief\",\n \"\",\n brief,\n ].join(\"\\n\");\n}\n\nexport function implementPrompt(brief: string, specPath: string | null, spec: string | null): string {\n const head = spec\n ? [\n \"You are the IMPLEMENT stage of a worker chain. The draft stage wrote the spec below (also at \" +\n specPath +\n \"). Implement it exactly, then run the spec's verification commands before finishing.\",\n \"\",\n \"## Spec\",\n \"\",\n spec,\n ]\n : [\"Implement the operator's brief below.\"];\n return [...head, \"\", \"## Operator brief\", \"\", brief].join(\"\\n\");\n}\n\nexport function reviewPrompt(brief: string, specPath: string | null, spec: string | null): string {\n const specPart = spec\n ? [\n \"\",\n \"## Spec (also at \" + specPath + \")\",\n \"\",\n spec,\n ]\n : [];\n return [\n \"You are the REVIEW stage of a worker chain. The implement stage just ran. Read the repository's diff (run `git diff` and `git status`; use `git diff --stat` for the overview) and check it against the spec's acceptance checks and verification commands — run the checks when they are cheap. Do not fix anything you find; report it.\",\n ...specPart,\n \"\",\n \"## Operator brief\",\n \"\",\n brief,\n ].join(\"\\n\");\n}\n\nexport interface ChainOptions {\n /** Class names, run in order: e.g. [\"draft\", \"implement\", \"review\"]. */\n stages: string[];\n /** Overrides the class of every stage when given (--class with --chain). */\n className?: string;\n providerFlag?: string;\n modelFlag?: string;\n label?: string;\n noPane?: boolean;\n mcpFlag?: string;\n /** --spec value, resolved for display/recording (\"-\" for stdin). */\n specPath?: string;\n /** The operator's brief — the -p value of the run. */\n brief: string;\n /** The caller's claude args (allowedTools etc.); the -p value is swapped. */\n claudeArgs: string[];\n cwd?: string;\n /** Internal: notified with the chain id once it exists (worker_run uses it). */\n onChainStart?: (chainId: string) => void;\n /** Internal: suppress result printing (the MCP shim's stdout is the RPC channel). */\n quiet?: boolean;\n}\n\nexport interface ChainDeps {\n /** Stage runner; tests inject a mock, production uses runWorker. */\n runStage?: (opts: RunOptions) => Promise<number>;\n /** logDir override for tests; default: the configured workers logDir. */\n logDir?: string;\n}\n\n/** Run a chain of stages; returns the exit code of the first failed stage, 0 when all pass. */\nexport async function runChain(opts: ChainOptions, deps: ChainDeps = {}): Promise<number> {\n const stages = opts.stages.map((s) => s.trim()).filter(Boolean);\n if (!stages.length) throw new Error(\"--chain needs at least one class, e.g. --chain draft,implement\");\n const runStage = deps.runStage ?? runWorker;\n const logDir = deps.logDir ?? workersLogDir(readWorkersSection().workers);\n const chainId = newWorkerId();\n const specPath = specPathFor(logDir, chainId);\n mkdirSync(join(logDir, \"specs\"), { recursive: true }); // the draft stage writes into it\n const baseLabel = opts.label ?? shortText(opts.brief, 40);\n\n appendLedger(ledgerPath(logDir), \"WORKER-CHAIN\", {\n chain: chainId,\n stages: stages.join(\",\"),\n label: baseLabel,\n });\n opts.onChainStart?.(chainId);\n\n let spec: string | null = null;\n for (let i = 0; i < stages.length; i++) {\n const stage = stages[i];\n if (stage === \"draft\") {\n spec = null; // a chain may legally start over; the draft rewrites it\n } else if (spec === null && existsSync(specPath)) {\n spec = readFileSync(specPath, \"utf8\");\n }\n const prompt =\n stage === \"draft\"\n ? draftPrompt(opts.brief, specPath)\n : stage === \"review\"\n ? reviewPrompt(opts.brief, spec ? specPath : null, spec)\n : implementPrompt(opts.brief, spec ? specPath : null, spec);\n process.stderr.write(\n `chain ${chainId}: stage ${i + 1}/${stages.length} ${stage} (spec: ${specPath})\\n`\n );\n const rc = await runStage({\n className: opts.className ?? stage,\n providerFlag: opts.providerFlag,\n modelFlag: opts.modelFlag,\n label: `${baseLabel} · ${stage}`,\n noPane: opts.noPane,\n mcpFlag: opts.mcpFlag,\n specPath: opts.specPath,\n claudeArgs: swapPromptArg(opts.claudeArgs, prompt),\n cwd: opts.cwd,\n parent: chainId,\n stage,\n quiet: opts.quiet,\n });\n if (stage === \"draft\") {\n if (!existsSync(specPath)) {\n process.stderr.write(\n `chain ${chainId}: draft stage produced no spec at ${specPath} — stopping. ` +\n `Write the spec yourself and re-run without the draft stage.\\n`\n );\n appendLedger(ledgerPath(logDir), \"WORKER-CHAIN-END\", {\n chain: chainId,\n rc: rc !== 0 ? rc : 1,\n failed: \"draft\",\n });\n return rc !== 0 ? rc : 1;\n }\n spec = readFileSync(specPath, \"utf8\");\n }\n if (rc !== 0) {\n appendLedger(ledgerPath(logDir), \"WORKER-CHAIN-END\", {\n chain: chainId,\n rc,\n failed: stage,\n });\n return rc;\n }\n }\n appendLedger(ledgerPath(logDir), \"WORKER-CHAIN-END\", { chain: chainId, rc: 0 });\n return 0;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chain-C8QO8gwj.mjs","names":[],"sources":["../src/workers/chain.ts"],"sourcesContent":["/**\n * chain.ts — draft-then-implement chains: `--chain draft,implement[,review]`.\n *\n * The chain runs each stage as its own worker (own id, own pane, `parent` set\n * to the chain id), so `ps` shows the chain as a tree and every stage can be\n * followed, replayed and said to like any other worker:\n *\n * - draft turns the operator's brief into a full spec file under\n * <logDir>/specs/<chain id>.md (goal, constraints, files likely\n * touched, acceptance checks, verification commands);\n * - any other stage (implement, plan, …) runs with that spec as its prompt\n * and the original brief attached;\n * - review reads the spec and the working-tree diff and produces the\n * structured report.\n *\n * A stage that fails (or a draft that produces no spec file) stops the chain;\n * the caller then writes the spec itself and re-runs without the draft stage.\n */\n\nimport { existsSync, mkdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { readWorkersSection } from \"./config.js\";\nimport { appendLedger } from \"./ledger.js\";\nimport { ledgerPath, workersLogDir } from \"./paths.js\";\nimport { newWorkerId } from \"./status.js\";\nimport { shortText } from \"./args.js\";\nimport { runWorker, type RunOptions } from \"./run.js\";\n\n/** Where a chain's spec file lives: <logDir>/specs/<chain id>.md. */\nexport function specPathFor(logDir: string, chainId: string): string {\n return join(logDir, \"specs\", `${chainId}.md`);\n}\n\n/**\n * Replace the caller's -p value with `prompt`, dropping every existing\n * -p/--print pair first (two -p flags on one claude command line are an error,\n * so the chain must never leave the brief in place when swapping prompts).\n */\nexport function swapPromptArg(claudeArgs: string[], prompt: string): string[] {\n const out: string[] = [];\n for (let i = 0; i < claudeArgs.length; i++) {\n const a = claudeArgs[i];\n if (a === \"-p\" || a === \"--print\") {\n if (i + 1 < claudeArgs.length && !claudeArgs[i + 1].startsWith(\"-\")) i += 1;\n continue;\n }\n out.push(a);\n }\n out.push(\"-p\", prompt);\n return out;\n}\n\nexport function draftPrompt(brief: string, specPath: string): string {\n return [\n \"You are the DRAFT stage of a worker chain. Turn the operator's brief below into a full implementation spec and write it to\",\n specPath,\n \"with the Write tool.\",\n \"\",\n \"Use exactly these five sections as Markdown headings:\",\n \"# Goal\",\n \"# Constraints\",\n \"# Files likely touched\",\n \"# Acceptance checks\",\n \"# Verification commands\",\n \"\",\n \"Read the repository first (Glob/Grep/Read) so the spec names real files and real commands. Do not implement anything.\",\n \"\",\n \"## Operator brief\",\n \"\",\n brief,\n ].join(\"\\n\");\n}\n\nexport function implementPrompt(brief: string, specPath: string | null, spec: string | null): string {\n const head = spec\n ? [\n \"You are the IMPLEMENT stage of a worker chain. The draft stage wrote the spec below (also at \" +\n specPath +\n \"). Implement it exactly, then run the spec's verification commands before finishing.\",\n \"\",\n \"## Spec\",\n \"\",\n spec,\n ]\n : [\"Implement the operator's brief below.\"];\n return [...head, \"\", \"## Operator brief\", \"\", brief].join(\"\\n\");\n}\n\nexport function reviewPrompt(brief: string, specPath: string | null, spec: string | null): string {\n const specPart = spec\n ? [\n \"\",\n \"## Spec (also at \" + specPath + \")\",\n \"\",\n spec,\n ]\n : [];\n return [\n \"You are the REVIEW stage of a worker chain. The implement stage just ran. Read the repository's diff (run `git diff` and `git status`; use `git diff --stat` for the overview) and check it against the spec's acceptance checks and verification commands — run the checks when they are cheap. Do not fix anything you find; report it.\",\n ...specPart,\n \"\",\n \"## Operator brief\",\n \"\",\n brief,\n ].join(\"\\n\");\n}\n\nexport interface ChainOptions {\n /** Class names, run in order: e.g. [\"draft\", \"implement\", \"review\"]. */\n stages: string[];\n /** Overrides the class of every stage when given (--class with --chain). */\n className?: string;\n providerFlag?: string;\n modelFlag?: string;\n label?: string;\n noPane?: boolean;\n mcpFlag?: string;\n /** --spec value, resolved for display/recording (\"-\" for stdin). */\n specPath?: string;\n /** The operator's brief — the -p value of the run. */\n brief: string;\n /** The caller's claude args (allowedTools etc.); the -p value is swapped. */\n claudeArgs: string[];\n cwd?: string;\n /** Internal: notified with the chain id once it exists (worker_run uses it). */\n onChainStart?: (chainId: string) => void;\n /** Internal: suppress result printing (the MCP shim's stdout is the RPC channel). */\n quiet?: boolean;\n}\n\nexport interface ChainDeps {\n /** Stage runner; tests inject a mock, production uses runWorker. */\n runStage?: (opts: RunOptions) => Promise<number>;\n /** logDir override for tests; default: the configured workers logDir. */\n logDir?: string;\n}\n\n/** Run a chain of stages; returns the exit code of the first failed stage, 0 when all pass. */\nexport async function runChain(opts: ChainOptions, deps: ChainDeps = {}): Promise<number> {\n const stages = opts.stages.map((s) => s.trim()).filter(Boolean);\n if (!stages.length) throw new Error(\"--chain needs at least one class, e.g. --chain draft,implement\");\n const runStage = deps.runStage ?? runWorker;\n const logDir = deps.logDir ?? workersLogDir(readWorkersSection().workers);\n const chainId = newWorkerId();\n const specPath = specPathFor(logDir, chainId);\n mkdirSync(join(logDir, \"specs\"), { recursive: true }); // the draft stage writes into it\n const baseLabel = opts.label ?? shortText(opts.brief, 40);\n\n appendLedger(ledgerPath(logDir), \"WORKER-CHAIN\", {\n chain: chainId,\n stages: stages.join(\",\"),\n label: baseLabel,\n });\n opts.onChainStart?.(chainId);\n\n let spec: string | null = null;\n for (let i = 0; i < stages.length; i++) {\n const stage = stages[i];\n if (stage === \"draft\") {\n spec = null; // a chain may legally start over; the draft rewrites it\n } else if (spec === null && existsSync(specPath)) {\n spec = readFileSync(specPath, \"utf8\");\n }\n const prompt =\n stage === \"draft\"\n ? draftPrompt(opts.brief, specPath)\n : stage === \"review\"\n ? reviewPrompt(opts.brief, spec ? specPath : null, spec)\n : implementPrompt(opts.brief, spec ? specPath : null, spec);\n process.stderr.write(\n `chain ${chainId}: stage ${i + 1}/${stages.length} ${stage} (spec: ${specPath})\\n`\n );\n const rc = await runStage({\n className: opts.className ?? stage,\n providerFlag: opts.providerFlag,\n modelFlag: opts.modelFlag,\n label: `${baseLabel} · ${stage}`,\n noPane: opts.noPane,\n mcpFlag: opts.mcpFlag,\n specPath: opts.specPath,\n claudeArgs: swapPromptArg(opts.claudeArgs, prompt),\n cwd: opts.cwd,\n parent: chainId,\n stage,\n quiet: opts.quiet,\n });\n if (stage === \"draft\") {\n if (!existsSync(specPath)) {\n process.stderr.write(\n `chain ${chainId}: draft stage produced no spec at ${specPath} — stopping. ` +\n `Write the spec yourself and re-run without the draft stage.\\n`\n );\n appendLedger(ledgerPath(logDir), \"WORKER-CHAIN-END\", {\n chain: chainId,\n rc: rc !== 0 ? rc : 1,\n failed: \"draft\",\n });\n return rc !== 0 ? rc : 1;\n }\n spec = readFileSync(specPath, \"utf8\");\n }\n if (rc !== 0) {\n appendLedger(ledgerPath(logDir), \"WORKER-CHAIN-END\", {\n chain: chainId,\n rc,\n failed: stage,\n });\n return rc;\n }\n }\n appendLedger(ledgerPath(logDir), \"WORKER-CHAIN-END\", { chain: chainId, rc: 0 });\n return 0;\n}\n"],"mappings":"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"}
|
package/dist/cli/index.mjs
CHANGED
|
@@ -2,25 +2,25 @@
|
|
|
2
2
|
import { i as err } from "../utils-C9HsYDpO.mjs";
|
|
3
3
|
import "../migrate-BD7D8EEh.mjs";
|
|
4
4
|
import "../helpers-BRCJg0G3.mjs";
|
|
5
|
-
import "../factory-
|
|
6
|
-
import { n as drainStdio, t as buildProgram } from "../program-
|
|
7
|
-
import "../wakeup-
|
|
8
|
-
import "../context-handover-cache-
|
|
9
|
-
import "../config-
|
|
10
|
-
import "../run-env-
|
|
11
|
-
import "../run-
|
|
12
|
-
import "../federation-db-
|
|
13
|
-
import "../registry-db-
|
|
14
|
-
import "../async-
|
|
15
|
-
import "../indexer-backend-
|
|
16
|
-
import "../ipc-client-
|
|
17
|
-
import "../fallback-
|
|
18
|
-
import "../main-resolver-
|
|
19
|
-
import "../server-
|
|
20
|
-
import "../state-
|
|
21
|
-
import "../aibroker-client-
|
|
22
|
-
import "../session-keepalive-
|
|
23
|
-
import "../chain-
|
|
5
|
+
import "../factory-y36qGegI.mjs";
|
|
6
|
+
import { n as drainStdio, t as buildProgram } from "../program-DiktbiWa.mjs";
|
|
7
|
+
import "../wakeup-CZw88uXf.mjs";
|
|
8
|
+
import "../context-handover-cache-mGxq2-8f.mjs";
|
|
9
|
+
import "../config-BbLFD7Uf.mjs";
|
|
10
|
+
import "../run-env-BHWnXqle.mjs";
|
|
11
|
+
import "../run-DUZRSnIT.mjs";
|
|
12
|
+
import "../federation-db-BTyoufBh.mjs";
|
|
13
|
+
import "../registry-db-C7voqML9.mjs";
|
|
14
|
+
import "../async-CNn36gm4.mjs";
|
|
15
|
+
import "../indexer-backend-isSLg6yE.mjs";
|
|
16
|
+
import "../ipc-client-D16Xw6Uo.mjs";
|
|
17
|
+
import "../fallback-CWDQYmJi.mjs";
|
|
18
|
+
import "../main-resolver-CDe7DCso.mjs";
|
|
19
|
+
import "../server-DlL1QI2k.mjs";
|
|
20
|
+
import "../state-HyjqTihC.mjs";
|
|
21
|
+
import "../aibroker-client-C5Fw7DNz.mjs";
|
|
22
|
+
import "../session-keepalive-CZUwp5IQ.mjs";
|
|
23
|
+
import "../chain-C8QO8gwj.mjs";
|
|
24
24
|
import { CommanderError } from "commander";
|
|
25
25
|
|
|
26
26
|
//#region src/cli/index.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"program.d.mts","names":[],"sources":["../../src/cli/program.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"program.d.mts","names":[],"sources":["../../src/cli/program.ts"],"mappings":";;;;;;;iBA+EgB,YAAA,CAAA,GAAgB,OAAA"}
|
package/dist/cli/program.mjs
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import "../utils-C9HsYDpO.mjs";
|
|
2
2
|
import "../migrate-BD7D8EEh.mjs";
|
|
3
3
|
import "../helpers-BRCJg0G3.mjs";
|
|
4
|
-
import "../factory-
|
|
5
|
-
import { t as buildProgram } from "../program-
|
|
6
|
-
import "../wakeup-
|
|
7
|
-
import "../context-handover-cache-
|
|
8
|
-
import "../config-
|
|
9
|
-
import "../run-env-
|
|
10
|
-
import "../run-
|
|
11
|
-
import "../federation-db-
|
|
12
|
-
import "../registry-db-
|
|
13
|
-
import "../async-
|
|
14
|
-
import "../indexer-backend-
|
|
15
|
-
import "../ipc-client-
|
|
16
|
-
import "../fallback-
|
|
17
|
-
import "../main-resolver-
|
|
18
|
-
import "../server-
|
|
19
|
-
import "../state-
|
|
20
|
-
import "../aibroker-client-
|
|
21
|
-
import "../session-keepalive-
|
|
22
|
-
import "../chain-
|
|
4
|
+
import "../factory-y36qGegI.mjs";
|
|
5
|
+
import { t as buildProgram } from "../program-DiktbiWa.mjs";
|
|
6
|
+
import "../wakeup-CZw88uXf.mjs";
|
|
7
|
+
import "../context-handover-cache-mGxq2-8f.mjs";
|
|
8
|
+
import "../config-BbLFD7Uf.mjs";
|
|
9
|
+
import "../run-env-BHWnXqle.mjs";
|
|
10
|
+
import "../run-DUZRSnIT.mjs";
|
|
11
|
+
import "../federation-db-BTyoufBh.mjs";
|
|
12
|
+
import "../registry-db-C7voqML9.mjs";
|
|
13
|
+
import "../async-CNn36gm4.mjs";
|
|
14
|
+
import "../indexer-backend-isSLg6yE.mjs";
|
|
15
|
+
import "../ipc-client-D16Xw6Uo.mjs";
|
|
16
|
+
import "../fallback-CWDQYmJi.mjs";
|
|
17
|
+
import "../main-resolver-CDe7DCso.mjs";
|
|
18
|
+
import "../server-DlL1QI2k.mjs";
|
|
19
|
+
import "../state-HyjqTihC.mjs";
|
|
20
|
+
import "../aibroker-client-C5Fw7DNz.mjs";
|
|
21
|
+
import "../session-keepalive-CZUwp5IQ.mjs";
|
|
22
|
+
import "../chain-C8QO8gwj.mjs";
|
|
23
23
|
|
|
24
24
|
export { buildProgram };
|