@tekmidian/pai 0.39.0 → 0.40.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.
Files changed (126) hide show
  1. package/README.md +41 -0
  2. package/dist/{auto-route-BlWOWT4z.mjs → auto-route-BwzmlW3W.mjs} +2 -2
  3. package/dist/{auto-route-BlWOWT4z.mjs.map → auto-route-BwzmlW3W.mjs.map} +1 -1
  4. package/dist/{chain-CTtHligI.mjs → chain-DhVHVnmT.mjs} +263 -791
  5. package/dist/chain-DhVHVnmT.mjs.map +1 -0
  6. package/dist/cli/index.mjs +10 -8
  7. package/dist/cli/index.mjs.map +1 -1
  8. package/dist/cli/program.mjs +10 -8
  9. package/dist/{clusters-wlK0w41E.mjs → clusters-DRHhfli9.mjs} +1 -1
  10. package/dist/{clusters-wlK0w41E.mjs.map → clusters-DRHhfli9.mjs.map} +1 -1
  11. package/dist/{config-B64vFg14.mjs → config-D1G9IFpn.mjs} +50 -3
  12. package/dist/config-D1G9IFpn.mjs.map +1 -0
  13. package/dist/{config-C_ErGddD.mjs → config-DD52BPB-.mjs} +1 -1
  14. package/dist/config-Dl8lT4Lu.mjs +527 -0
  15. package/dist/config-Dl8lT4Lu.mjs.map +1 -0
  16. package/dist/{context-handover-cache-BpUojjsi.mjs → context-handover-cache-C6_rs7JT.mjs} +112 -24
  17. package/dist/context-handover-cache-C6_rs7JT.mjs.map +1 -0
  18. package/dist/daemon/index.mjs +12 -10
  19. package/dist/daemon/index.mjs.map +1 -1
  20. package/dist/daemon-BSL9sXxm.mjs +22 -0
  21. package/dist/{daemon-CTkN_Vn8.mjs → daemon-CpdVUnl_.mjs} +26 -26
  22. package/dist/{daemon-CTkN_Vn8.mjs.map → daemon-CpdVUnl_.mjs.map} +1 -1
  23. package/dist/daemon-mcp/index.mjs +127 -6
  24. package/dist/daemon-mcp/index.mjs.map +1 -1
  25. package/dist/detector-BLVOCiJ_.mjs +5 -0
  26. package/dist/{detector-Bwk_4Pk4.mjs → detector-smpCm7ku.mjs} +1 -1
  27. package/dist/{detector-Bwk_4Pk4.mjs.map → detector-smpCm7ku.mjs.map} +1 -1
  28. package/dist/{factory-A9x-T2Kg.mjs → factory-Bc5EyN54.mjs} +7 -7
  29. package/dist/{factory-A9x-T2Kg.mjs.map → factory-Bc5EyN54.mjs.map} +1 -1
  30. package/dist/factory-C7ua_3ik.mjs +3 -0
  31. package/dist/{providers-FYkZjn_C.mjs → fallback-CdXv-Np0.mjs} +411 -51
  32. package/dist/fallback-CdXv-Np0.mjs.map +1 -0
  33. package/dist/hooks/context-compression-hook.mjs +136 -22
  34. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  35. package/dist/hooks/post-compact-inject.mjs +98 -7
  36. package/dist/hooks/post-compact-inject.mjs.map +4 -4
  37. package/dist/hooks/route-agents-to-worker.mjs +75 -6
  38. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  39. package/dist/hooks/worker-proxy.mjs +75 -6
  40. package/dist/hooks/worker-proxy.mjs.map +2 -2
  41. package/dist/hooks/worker-status-line.mjs +149 -90
  42. package/dist/hooks/worker-status-line.mjs.map +4 -4
  43. package/dist/{indexer-backend-TG64CCQC.mjs → indexer-backend-rohVVKrA.mjs} +1 -1
  44. package/dist/{indexer-backend-TG64CCQC.mjs.map → indexer-backend-rohVVKrA.mjs.map} +1 -1
  45. package/dist/{ipc-client-BmypMNYk.mjs → ipc-client-C7AVpsyv.mjs} +2 -2
  46. package/dist/{ipc-client-BmypMNYk.mjs.map → ipc-client-C7AVpsyv.mjs.map} +1 -1
  47. package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
  48. package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
  49. package/dist/{latent-ideas-B7wq75Pt.mjs → latent-ideas-iNp5s55R.mjs} +2 -2
  50. package/dist/{latent-ideas-B7wq75Pt.mjs.map → latent-ideas-iNp5s55R.mjs.map} +1 -1
  51. package/dist/{link-boost-HkG7JWZR.mjs → link-boost-NNzADHX_.mjs} +1 -1
  52. package/dist/{link-boost-HkG7JWZR.mjs.map → link-boost-NNzADHX_.mjs.map} +1 -1
  53. package/dist/{main-resolver-DlaLOFBA.mjs → main-resolver-CM1IHbuu.mjs} +78 -75
  54. package/dist/main-resolver-CM1IHbuu.mjs.map +1 -0
  55. package/dist/main-resolver-E5WlilhM.mjs +6 -0
  56. package/dist/{neighborhood-lThN-MaQ.mjs → neighborhood-AGL7oXQ2.mjs} +1 -1
  57. package/dist/{neighborhood-lThN-MaQ.mjs.map → neighborhood-AGL7oXQ2.mjs.map} +1 -1
  58. package/dist/{note-context-b6k0mAKi.mjs → note-context-BHwYpSFf.mjs} +1 -1
  59. package/dist/{note-context-b6k0mAKi.mjs.map → note-context-BHwYpSFf.mjs.map} +1 -1
  60. package/dist/{planner-BDI7bE9B.mjs → planner-Cm3g6fWH.mjs} +5 -3
  61. package/dist/{planner-BDI7bE9B.mjs.map → planner-Cm3g6fWH.mjs.map} +1 -1
  62. package/dist/{postgres-mW1n7Vi1.mjs → postgres-Dwxe290H.mjs} +1 -1
  63. package/dist/{postgres-mW1n7Vi1.mjs.map → postgres-Dwxe290H.mjs.map} +1 -1
  64. package/dist/{program-DXBwJV7h.mjs → program-AR0qlYRg.mjs} +334 -78
  65. package/dist/program-AR0qlYRg.mjs.map +1 -0
  66. package/dist/query-feedback-CGcEiGtY.mjs +3 -0
  67. package/dist/{query-feedback-BV4CcxqS.mjs → query-feedback-DZfjI34w.mjs} +1 -1
  68. package/dist/{query-feedback-BV4CcxqS.mjs.map → query-feedback-DZfjI34w.mjs.map} +1 -1
  69. package/dist/router-CAtyVQud.mjs +3 -0
  70. package/dist/{router-DcHKnEPa.mjs → router-DHracTUC.mjs} +1 -1
  71. package/dist/{router-DcHKnEPa.mjs.map → router-DHracTUC.mjs.map} +1 -1
  72. package/dist/server-BOuAOj9b.mjs +386 -0
  73. package/dist/server-BOuAOj9b.mjs.map +1 -0
  74. package/dist/skills/Worker/SKILL.md +19 -1
  75. package/dist/{sources-kLnQsNrW.mjs → sources-ju6AA7lL.mjs} +1 -1
  76. package/dist/{sources-kLnQsNrW.mjs.map → sources-ju6AA7lL.mjs.map} +1 -1
  77. package/dist/{sqlite-BenGr3UP.mjs → sqlite-kFZeWCJ2.mjs} +1 -1
  78. package/dist/{sqlite-BenGr3UP.mjs.map → sqlite-kFZeWCJ2.mjs.map} +1 -1
  79. package/dist/{state-Ca9F_sZc.mjs → state-DH_mp5Xe.mjs} +1 -1
  80. package/dist/{state-CAeyOdfq.mjs → state-ahFAbD8b.mjs} +1 -1
  81. package/dist/{state-CAeyOdfq.mjs.map → state-ahFAbD8b.mjs.map} +1 -1
  82. package/dist/{themes-BI4GMRP9.mjs → themes-DqhNUUQk.mjs} +1 -1
  83. package/dist/{themes-BI4GMRP9.mjs.map → themes-DqhNUUQk.mjs.map} +1 -1
  84. package/dist/{tools-DGcB3o_S.mjs → tools-CMlHa9lZ.mjs} +14 -14
  85. package/dist/{tools-DGcB3o_S.mjs.map → tools-CMlHa9lZ.mjs.map} +1 -1
  86. package/dist/{tools-Bp7hj6OW.mjs → tools-COna9vZR.mjs} +1 -1
  87. package/dist/{trace-bobARFEX.mjs → trace-DTP6k4B_.mjs} +1 -1
  88. package/dist/{trace-bobARFEX.mjs.map → trace-DTP6k4B_.mjs.map} +1 -1
  89. package/dist/{vault-indexer-C3OfXTNF.mjs → vault-indexer-YMH22mLU.mjs} +1 -1
  90. package/dist/{vault-indexer-C3OfXTNF.mjs.map → vault-indexer-YMH22mLU.mjs.map} +1 -1
  91. package/dist/{work-queue-worker-R7UGIag_.mjs → work-queue-worker-B3nl8yyn.mjs} +23 -26
  92. package/dist/work-queue-worker-B3nl8yyn.mjs.map +1 -0
  93. package/dist/work-queue-worker-BM1GcAAq.mjs +14 -0
  94. package/dist/{zettelkasten-m5QPtb-L.mjs → zettelkasten-DDvKRm2V.mjs} +3 -3
  95. package/dist/{zettelkasten-m5QPtb-L.mjs.map → zettelkasten-DDvKRm2V.mjs.map} +1 -1
  96. package/docs/commands/README.md +5 -2
  97. package/docs/commands/worker.md +66 -5
  98. package/docs/images/provider-abstraction-live.png +0 -0
  99. package/docs/images/workers.png +0 -0
  100. package/docs/launcher-independence.md +70 -0
  101. package/docs/provider-abstraction.md +87 -0
  102. package/docs/provider-independence-details.md +330 -0
  103. package/docs/provider-independence.md +75 -0
  104. package/docs/worker.md +72 -11
  105. package/package.json +1 -1
  106. package/scripts/build-hooks.mjs +6 -1
  107. package/scripts/build-skill-stubs.mjs +17 -4
  108. package/src/hooks/ts/lib/context-fill.test.ts +158 -4
  109. package/src/hooks/ts/lib/context-fill.ts +102 -35
  110. package/statusline-command.sh +120 -21
  111. package/dist/chain-CTtHligI.mjs.map +0 -1
  112. package/dist/config-B64vFg14.mjs.map +0 -1
  113. package/dist/context-handover-cache-BpUojjsi.mjs.map +0 -1
  114. package/dist/daemon-CmHHmKpg.mjs +0 -20
  115. package/dist/detector-DO730Zq0.mjs +0 -5
  116. package/dist/factory-BD-np0Vd.mjs +0 -3
  117. package/dist/main-resolver-D6IImXvF.mjs +0 -4
  118. package/dist/main-resolver-DlaLOFBA.mjs.map +0 -1
  119. package/dist/program-DXBwJV7h.mjs.map +0 -1
  120. package/dist/providers-FYkZjn_C.mjs.map +0 -1
  121. package/dist/query-feedback-DSVyHtrG.mjs +0 -3
  122. package/dist/router-Bk77E7hj.mjs +0 -3
  123. package/dist/runtime-paths-rni52zHX.mjs +0 -50
  124. package/dist/runtime-paths-rni52zHX.mjs.map +0 -1
  125. package/dist/work-queue-worker-DW8lz-Oo.mjs +0 -11
  126. package/dist/work-queue-worker-R7UGIag_.mjs.map +0 -1
@@ -1,7 +1,10 @@
1
- import { A as contextPercent, B as workersLogDir, C as quotaSkipThreshold, D as workerInScope, E as sessionTag, G as keysDir, H as PROVIDER_TAGS, J as readWorkersSection, K as parseWorkersConfig, L as shortText, M as loadStatuses, O as ageOf, R as eventsPath, S as probeQuota, T as resolveSession, U as WorkersConfigError, V as DEFAULT_LOG_DIR, W as expandHome, Y as writeWorkersSection, _ as workerDepth, d as renderReport, g as sayToWorker, h as readInbox, k as alive, q as providerCostTier, u as parseWorkerReport, w as currentTabKey, x as clearCooldown } from "./chain-CTtHligI.mjs";
2
- import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, writeFileSync } from "node:fs";
3
- import { basename, relative } from "node:path";
4
- import { spawn } from "node:child_process";
1
+ import { a as WorkersConfigError, b as readJsonStrict, c as expandHome, d as parseWorkersConfig, f as providerCostTier, g as writeWorkersSection, h as resolveModelCapability, l as isModelCapability, m as readWorkersSection, n as MODEL_CAPABILITIES, p as providerKeyPath, r as PROVIDER_TAGS, t as DEFAULT_LOG_DIR, u as keysDir, x as writeJsonAtomic } from "./config-Dl8lT4Lu.mjs";
2
+ import { _ as workersLogDir, a as clearCooldown, c as probeQuota, f as eventsPath, l as quotaSkipThreshold, u as resolveTarget } from "./server-BOuAOj9b.mjs";
3
+ import { C as ageOf, E as contextPercent, N as shortText, O as loadStatuses, S as UNLABELED, T as contextLabel, b as sessionTag, c as renderReport, f as readInbox, m as workerDepth, p as sayToWorker, s as parseWorkerReport, v as currentTabKey, w as alive, x as workerInScope, y as resolveSession } from "./chain-DhVHVnmT.mjs";
4
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { basename, join, relative } from "node:path";
7
+ import { execFileSync, spawn } from "node:child_process";
5
8
  import { createInterface } from "node:readline";
6
9
 
7
10
  //#region src/workers/chatui.ts
@@ -158,17 +161,23 @@ function wrapText(text, width) {
158
161
  function chatScrollRegion(rows) {
159
162
  return `\x1b[1;${Math.max(1, rows - 3)}r`;
160
163
  }
161
- /** Clear the blank separator row between the transcript and the prompt. */
162
- function chatBlankRow(rows) {
163
- return `\x1b[${Math.max(1, rows - 2)};1H\x1b[K`;
164
+ /**
165
+ * The separator row between the transcript and the prompt: cleared, then —
166
+ * when the pane's width is known — a dim rule of `cols` box-drawing `─`
167
+ * filling the row. `cols` 0 (unknown) keeps the row blank; `dim` is the
168
+ * pane's colour helper, identity when colours are off.
169
+ */
170
+ function chatBlankRow(rows, cols = 0, dim = (s) => s) {
171
+ const rule = cols > 0 ? dim("─".repeat(cols)) : "";
172
+ return `\x1b[${Math.max(1, rows - 2)};1H\x1b[K` + rule;
164
173
  }
165
174
  /**
166
- * Enter the chat layout: clear the pane, set the scroll region, blank the
175
+ * Enter the chat layout: clear the pane, set the scroll region, draw the
167
176
  * separator row and park the cursor at column 3 of the prompt row (rows-1,
168
177
  * right after `› `). The ticker owns row `rows`.
169
178
  */
170
- function chatEnter(rows) {
171
- return "\x1B[2J" + chatScrollRegion(rows) + chatBlankRow(rows) + `\x1b[${Math.max(1, rows - 1)};3H`;
179
+ function chatEnter(rows, cols = 0, dim = (s) => s) {
180
+ return "\x1B[2J" + chatScrollRegion(rows) + chatBlankRow(rows, cols, dim) + `\x1b[${Math.max(1, rows - 1)};3H`;
172
181
  }
173
182
  /** Leave it: reset the scroll region, show the cursor, drop to the last row. */
174
183
  function chatLeave(rows) {
@@ -358,7 +367,7 @@ function fmtElapsed(secs) {
358
367
  }
359
368
  /**
360
369
  * The chat pane's status row (always visible while following):
361
- * `[glm/glm-5.3] ctx 84k/200k 42% · turns 12 · tools 7 · 4m12s · ⋯ 7s · <intent> · <tool>`
370
+ * `[glm/glm-5.3] ctx 84k/200k (42%) · turns 12 · tools 7 · 4m12s · ⋯ 7s · <intent> · <tool>`
362
371
  * — context yellow past 70 %, red past 85 %, dropped when unknown; once the
363
372
  * worker finished, the ticker part is replaced by `✓ done` / `✗ failed`.
364
373
  */
@@ -366,7 +375,7 @@ function chatStatusRow(c, s) {
366
375
  const parts = [];
367
376
  const pct = contextPercent(s);
368
377
  if (pct !== null) {
369
- const label = `ctx ${fmtK(s.contextTokens ?? 0)}/${fmtK(s.contextWindow ?? 0)} ${pct}%`;
378
+ const label = contextLabel(s);
370
379
  parts.push(pct > 85 ? c("red", label) : pct > 70 ? c("yellow", label) : label);
371
380
  }
372
381
  parts.push(`turns ${s.turns}`, `tools ${s.tools}`, fmtElapsed(s.elapsed));
@@ -392,19 +401,16 @@ function blankBetween(prev, e) {
392
401
  function intentOf(text) {
393
402
  return shortText((text.trim().split("\n").find((l) => l.trim()) ?? "").trim().replace(/\s+/g, " "), 60);
394
403
  }
395
- /** Compact token count: 84k, 200k, 900. */
396
- function fmtK(n) {
397
- return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
398
- }
399
404
  /**
400
- * The context meter `ctx 84k/200k (42%)`, yellow from 70 %, red from 85 %.
401
- * null when the numbers are missing or below `minPct` (the table only shows
402
- * it past 60; the pane liveness line always shows it).
405
+ * The context meter `ctx 84k/200k (42%)` (contextLabel, shared with the
406
+ * chat status row), yellow from 70 %, red from 85 %. null when the numbers
407
+ * are missing or below `minPct` (the table only shows it past 60; the pane
408
+ * liveness line always shows it).
403
409
  */
404
410
  function contextMeter(c, s, minPct = 0) {
405
411
  const pct = contextPercent(s);
406
412
  if (pct === null || pct <= minPct) return null;
407
- const label = `ctx ${fmtK(s.contextTokens ?? 0)}/${fmtK(s.contextWindow ?? 0)} (${pct}%)`;
413
+ const label = contextLabel(s);
408
414
  if (pct > 85) return c("red", label);
409
415
  if (pct > 70) return c("yellow", label);
410
416
  return label;
@@ -552,28 +558,49 @@ function renderTable(c, statuses, scopeLabel, now = /* @__PURE__ */ new Date(),
552
558
  return lines.join("\n");
553
559
  }
554
560
  /**
555
- * One-line status-bar summary (same shape glm-ps --status printed). Sub-workers
556
- * render after their parent with a `↳` prefix (`all` carries the whole forest
557
- * so depth is right even when only some workers are this session's);
558
- * `◆N` marks an inbox with N handoffs.
561
+ * The step of a statusline row: what the worker is doing as a verb, never a
562
+ * raw command. A Bash line trims to its first word (`grep -n -A4 "x" src/` →
563
+ * `grep …` no `Bash:` prefix, no flags, no quotes); the file tools keep
564
+ * their `Edit: button.ts` shape; free text (what the worker last said) is
565
+ * only shortened. Always ≤ 24 chars.
566
+ */
567
+ function stepOf(last) {
568
+ const s = String(last ?? "").trim();
569
+ if (s.startsWith("Bash: ")) {
570
+ const cmd = s.slice(6).trim();
571
+ const verb = cmd.split(/\s+/)[0] ?? "";
572
+ return shortText(cmd.length > verb.length ? `${verb} …` : verb, 24);
573
+ }
574
+ return shortText(s, 24);
575
+ }
576
+ /**
577
+ * The statusline bar: `provider ▶N` then one row per running spawned worker —
578
+ * `name · age · step` — and today's ✓/✗ tail. The chat pane contributes
579
+ * nothing but its provider: its age, state and inbox live in `pai worker ps`,
580
+ * not here. Rows are flat (the ↳ depth is ps territory) and oldest first;
581
+ * `#id` joins a name only when two running workers share one label, so ids
582
+ * stay rare enough to read.
559
583
  */
560
- function renderStatusLine(mine, now = /* @__PURE__ */ new Date(), c = makeColor(true), all = mine, inbox = {}) {
584
+ function renderStatusLine(mine, now = /* @__PURE__ */ new Date()) {
561
585
  if (!mine.length) return "";
562
- const running = mine.filter((s) => s.state === "running" && alive(s.pid)).sort((a, b) => workerDepth(all, a.id) - workerDepth(all, b.id));
586
+ const isChat = (s) => s.origin === "chat" || !s.origin && (s.label === UNLABELED || s.label === "(no prompt)") && s.turns === 0;
587
+ const chat = mine.find((s) => isChat(s) && s.state === "running" && alive(s.pid));
588
+ const running = mine.filter((s) => !isChat(s) && s.state === "running" && alive(s.pid)).sort((a, b) => a.started.localeCompare(b.started));
563
589
  const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
564
590
  const doneToday = mine.filter((s) => s.state !== "running" && s.started.startsWith(today));
565
591
  const ok = doneToday.filter((s) => s.state === "done").length;
566
592
  const bad = doneToday.length - ok;
567
- const providers = new Set(running.map((s) => s.provider));
568
- let head = `${providers.size === 1 ? [...providers][0] : "workers"} ▶${running.length}`;
569
- const parts = running.slice(0, 3).map((s) => {
570
- const meter = contextMeter(c, s, 60);
571
- const depth = workerDepth(all, s.id);
572
- const lead = depth > 0 ? " ".repeat(depth - 1) + "↳ " : "";
573
- const box = inbox[s.id] ? ` ◆${inbox[s.id]}` : "";
574
- return `${lead}${s.id.slice(-4)} ${s.label.slice(0, 26)} ${ageOf(s.started, now)} · ${s.last.slice(0, 30)}${box}` + (meter ? ` ${meter}` : "");
593
+ const rows = running.slice(0, 3).map((s) => {
594
+ return `${`${running.some((o) => o !== s && o.label === s.label) ? `#${s.id.slice(-4)} ` : ""}${s.label.slice(0, 26)}`} · ${ageOf(s.started, now)} · ${stepOf(s.last)}`;
575
595
  });
576
- if (parts.length) head += " " + parts.join(" | ");
596
+ let head;
597
+ if (chat) head = chat.provider;
598
+ else {
599
+ const providers = new Set(running.map((s) => s.provider));
600
+ head = providers.size === 1 ? [...providers][0] : "workers";
601
+ }
602
+ if (running.length) head += ` ▶${running.length}`;
603
+ if (rows.length) head += " | " + rows.join(" | ");
577
604
  if (doneToday.length) head += ` ✓${ok} ✗${bad} today`;
578
605
  return head;
579
606
  }
@@ -676,7 +703,7 @@ function applyEvent(c, s, e, cwd, tag, offMin, wrapWidth) {
676
703
  }
677
704
  const gutter = gutterFor(c, e, tag, offMin);
678
705
  const body = gutterBody(renderEvent(c, "", e, cwd, tools), gutter, wrapWidth ?? null);
679
- const lines = blankBetween(s.prev, e) ? ["", ...body] : body;
706
+ const lines = blankBetween(s.prev, e) ? [gutter ? gutter.barCont : "", ...body] : body;
680
707
  return {
681
708
  lines,
682
709
  day: day && day !== s.lastDay ? day : null,
@@ -807,13 +834,19 @@ async function followWorkers(logDir, target, showAll, autoExit, env = process.en
807
834
  const columns = () => typeof out_.columns === "number" ? out_.columns : null;
808
835
  let fill = 0;
809
836
  const regionRows = () => Math.max(1, rows - 3);
837
+ const retained = [];
838
+ const RETAIN_CAP = 500;
810
839
  /** Every line the pane shows goes through here: plain newline, or a row
811
840
  * inserted above the fixed prompt/ticker rows (chatui.chatInsertLine). */
812
- const out = (line) => {
841
+ const out = (line, keep = true) => {
813
842
  if (!chat) {
814
843
  out_.write(line + "\n");
815
844
  return;
816
845
  }
846
+ if (keep) {
847
+ retained.push(line);
848
+ if (retained.length > RETAIN_CAP) retained.splice(0, retained.length - RETAIN_CAP);
849
+ }
817
850
  const r = chatInsertLine(line, fill, regionRows());
818
851
  out_.write(r.seq);
819
852
  fill = r.fill;
@@ -1030,9 +1063,17 @@ async function followWorkers(logDir, target, showAll, autoExit, env = process.en
1030
1063
  }
1031
1064
  if (!redrew) drawPrompt();
1032
1065
  };
1066
+ const silent = {
1067
+ write: () => true,
1068
+ on: () => silent,
1069
+ once: () => silent,
1070
+ off: () => silent,
1071
+ removeListener: () => silent,
1072
+ emit: () => false
1073
+ };
1033
1074
  rlIn = createInterface({
1034
1075
  input: in_,
1035
- output: { write: () => true },
1076
+ output: silent,
1036
1077
  terminal: terminalIn
1037
1078
  });
1038
1079
  rlIn.on("line", handleChatLine);
@@ -1049,13 +1090,14 @@ async function followWorkers(logDir, target, showAll, autoExit, env = process.en
1049
1090
  rlIn.on("close", () => {
1050
1091
  aborted = true;
1051
1092
  });
1052
- out_.write(chatEnter(rows));
1093
+ out_.write(chatEnter(rows, columns() ?? 0, (s) => c("dim", s)));
1053
1094
  drawPrompt();
1054
1095
  if (terminalIn) in_.on("keypress", () => drawPrompt());
1055
1096
  onResize = () => {
1056
1097
  if (typeof out_.rows === "number") rows = out_.rows;
1057
1098
  fill = 0;
1058
- out_.write(chatScrollRegion(rows) + chatBlankRow(rows));
1099
+ out_.write("\x1B[2J" + chatScrollRegion(rows) + chatBlankRow(rows, columns() ?? 0, (s) => c("dim", s)));
1100
+ for (const ln of retained.slice(-regionRows())) out(ln, false);
1059
1101
  drawPrompt();
1060
1102
  };
1061
1103
  out_.on?.("resize", onResize);
@@ -1200,14 +1242,13 @@ function sleep(ms) {
1200
1242
  return new Promise((r) => setTimeout(r, ms));
1201
1243
  }
1202
1244
  /** Workers of this terminal for the status bar; "" when there are none. */
1203
- function statusLineOutput(logDir, term, cwd, now = /* @__PURE__ */ new Date()) {
1204
- const statuses = loadStatuses(logDir);
1205
- const mine = statuses.filter((s) => {
1245
+ function statusLineOutput(logDir, term, cwd, claudeSession = "", now = /* @__PURE__ */ new Date()) {
1246
+ return renderStatusLine(loadStatuses(logDir).filter((s) => {
1206
1247
  const sameScope = term && workerInScope(s, term);
1248
+ const spawnedHere = claudeSession && s.spawnerSession === claudeSession;
1207
1249
  const sameDir = cwd && s.cwd.startsWith(cwd);
1208
- return sameScope || !term && sameDir;
1209
- });
1210
- return renderStatusLine(mine, now, makeColor(false), statuses, inboxCounts(logDir, mine));
1250
+ return sameScope || spawnedHere || !term && sameDir;
1251
+ }), now);
1211
1252
  }
1212
1253
 
1213
1254
  //#endregion
@@ -1315,6 +1356,32 @@ function useProvider(name) {
1315
1356
  writeWorkersSection(raw, workers);
1316
1357
  return workers;
1317
1358
  }
1359
+ /**
1360
+ * The provider a model command targets: the named one, else the active one.
1361
+ * "auto" and an unset active both ask for an explicit name.
1362
+ */
1363
+ function resolveProviderName(workers, name) {
1364
+ const target = name ?? workers.active ?? "";
1365
+ if (workers.providers[target]) return target;
1366
+ if (!target) throw new WorkersConfigError("no active provider — name one explicitly");
1367
+ if (target === "auto") throw new WorkersConfigError("active is \"auto\" — name a provider explicitly");
1368
+ throw new WorkersConfigError(`no provider named "${target}". Configured: ${Object.keys(workers.providers).join(", ") || "(none)"}`);
1369
+ }
1370
+ /**
1371
+ * Set a provider's model id for a capability — default, fast, image, …
1372
+ * (`pai worker model`, MCP worker_model).
1373
+ */
1374
+ function setProviderModel(name, capability, model, configPath) {
1375
+ if (!isModelCapability(capability)) throw new WorkersConfigError(`"${capability}" is not a model capability (from: ${MODEL_CAPABILITIES.join(", ")})`);
1376
+ const id = model.trim();
1377
+ if (!id) throw new WorkersConfigError(`a ${capability} model id must not be empty`);
1378
+ const { raw, workers } = readWorkersSection(configPath);
1379
+ const p = workers.providers[name];
1380
+ if (!p) throw new WorkersConfigError(`no provider named "${name}". Configured: ${Object.keys(workers.providers).join(", ") || "(none)"}`);
1381
+ p.models[capability] = id;
1382
+ writeWorkersSection(raw, workers, configPath);
1383
+ return workers;
1384
+ }
1318
1385
  function setProviderEnabled(name, enabled) {
1319
1386
  const { raw, workers } = readWorkersSection();
1320
1387
  const p = workers.providers[name];
@@ -1335,8 +1402,10 @@ function setClass(name, target) {
1335
1402
  const [provider, alias] = target.split("/");
1336
1403
  const p = workers.providers[provider];
1337
1404
  if (!p) throw new WorkersConfigError(`no provider named "${provider}" in "${target}". Configured: ${Object.keys(workers.providers).join(", ") || "(none)"}`);
1338
- if (alias && alias !== "default" && alias !== "fast") throw new WorkersConfigError(`unknown model alias "${alias}" — providers expose "default" and "fast"`);
1339
- if (alias === "fast" && !p.models.fast) throw new WorkersConfigError(`provider "${provider}" has no fast model configured`);
1405
+ if (alias) {
1406
+ if (!isModelCapability(alias)) throw new WorkersConfigError(`unknown model alias "${alias}" providers expose: ${MODEL_CAPABILITIES.join(", ")}`);
1407
+ if (alias !== "default" && !p.models[alias]) throw new WorkersConfigError(`provider "${provider}" has no ${alias} model configured`);
1408
+ }
1340
1409
  } else if (target.provider) {
1341
1410
  if (!workers.providers[target.provider]) throw new WorkersConfigError(`no provider named "${target.provider}". Configured: ${Object.keys(workers.providers).join(", ") || "(none)"}`);
1342
1411
  }
@@ -1368,6 +1437,28 @@ function classTargetText(target) {
1368
1437
  ...target.order?.length ? [`order [${target.order.join(",")}]`] : []
1369
1438
  ].join(" ");
1370
1439
  }
1440
+ /**
1441
+ * `default X fast Y image Z` — every model capability on one line, unset
1442
+ * ones shown as "(none)" (they resolve to the default model).
1443
+ */
1444
+ function modelPrefsText(p) {
1445
+ return MODEL_CAPABILITIES.map((c) => `${c} ${p.models[c] ?? "(none)"}`).join(" ");
1446
+ }
1447
+ /**
1448
+ * Compact model listing for `pai worker model` / worker_model get: the active
1449
+ * provider, then one line per provider with all its capability preferences.
1450
+ */
1451
+ function describeModels(workers) {
1452
+ const names = Object.keys(workers.providers);
1453
+ if (!names.length) return ["no providers configured — add one with `pai worker providers add <name> …`"];
1454
+ const lines = [`active provider: ${workers.active ?? "(none)"}`];
1455
+ for (const name of names) {
1456
+ const p = workers.providers[name];
1457
+ const active = workers.active === name ? " [active]" : "";
1458
+ lines.push(`${name}${active} ${modelPrefsText(p)}`);
1459
+ }
1460
+ return lines;
1461
+ }
1371
1462
  /** Human-readable provider listing (quota probe included when configured). */
1372
1463
  function describeProviders(workers) {
1373
1464
  const lines = [];
@@ -1384,7 +1475,7 @@ function describeProviders(workers) {
1384
1475
  const quotaNote = quota === null ? "" : ` quota ${quota}% (skip at ${quotaSkipThreshold(p)})`;
1385
1476
  const tierTags = [`tier ${providerCostTier(p)}`, ...p.tags ?? []].join(", ");
1386
1477
  lines.push(`${name} [${flags.join(", ")}] ${p.baseUrl}`);
1387
- lines.push(` model ${p.models.default}${p.models.fast ? ` (fast: ${p.models.fast})` : ""}${quotaNote}`);
1478
+ lines.push(` ${modelPrefsText(p)}${quotaNote}`);
1388
1479
  lines.push(` ${tierTags}`);
1389
1480
  if (p.keyFile) lines.push(` key file ${expandHome(p.keyFile)}`);
1390
1481
  else lines.push(` no key file (token "local")`);
@@ -1401,5 +1492,274 @@ function describeProviders(workers) {
1401
1492
  }
1402
1493
 
1403
1494
  //#endregion
1404
- export { setClass as a, unsetClass as c, followWorkers as d, psOutput as f, removeProvider as i, updateProvider as l, statusLineOutput as m, classTargetText as n, setProviderEnabled as o, replayOutput as p, describeProviders as r, setWorkersEnabled as s, addProvider as t, useProvider as u };
1405
- //# sourceMappingURL=providers-FYkZjn_C.mjs.map
1495
+ //#region src/workers/fallback.ts
1496
+ /**
1497
+ * fallback.ts — machine-wide Claude Code fallback to a worker provider.
1498
+ *
1499
+ * When the Anthropic plan runs out, `pai worker fallback on <provider>`
1500
+ * points EVERY new Claude Code process on this machine at that provider:
1501
+ * interactive sessions, task-bus sessions, the daemon's headless summarizer.
1502
+ * It writes the provider's base URL, token (read from its key file at switch
1503
+ * time — so the token then sits in settings.json until `off`), the three
1504
+ * DEFAULT_*_MODEL pins, its extra env (API_TIMEOUT_MS …) plus tool search on
1505
+ * and nonessential traffic off into the `env` block of ~/.claude/settings.json,
1506
+ * and pins the top-level `model` to the provider's default model.
1507
+ *
1508
+ * What settings.json carried before the switch is saved under
1509
+ * `workers.fallback.saved` in ~/.config/pai/config.json; `fallback off`
1510
+ * restores it exactly and removes the added keys. Both files are written
1511
+ * atomically; nothing else in them is touched. While fallback is on the
1512
+ * Agent hook keeps routing subagents to workers, unchanged — those runs set
1513
+ * their own env per provider and are unaffected.
1514
+ *
1515
+ * Running Claude Code processes keep the provider they started with until
1516
+ * restarted; `fallback status` lists them and points at the note `on` leaves
1517
+ * in the workers log dir (FALLBACK-ACTIVE.md).
1518
+ *
1519
+ * CLAUDE_SETTINGS_PATH overrides the settings.json location (dry runs against
1520
+ * a copy); the config path is injectable for tests the same way.
1521
+ */
1522
+ /** settings.json location; CLAUDE_SETTINGS_PATH points it at a copy for dry runs. */
1523
+ function fallbackSettingsPath() {
1524
+ return process.env.CLAUDE_SETTINGS_PATH ?? join(homedir(), ".claude", "settings.json");
1525
+ }
1526
+ /** The note `on` writes so a human landing in the log dir sees the switch. */
1527
+ function fallbackNotePath(logDir) {
1528
+ return join(logDir, "FALLBACK-ACTIVE.md");
1529
+ }
1530
+ /**
1531
+ * The env block fallback writes: the provider's endpoint and token, model
1532
+ * pins (fast model as haiku, default as sonnet and opus — the same mapping
1533
+ * buildRunEnv uses), the provider's own env, and the two fallback constants.
1534
+ */
1535
+ function fallbackEnv(provider) {
1536
+ let token = "local";
1537
+ const keyPath = providerKeyPath(provider);
1538
+ if (keyPath) {
1539
+ token = readFileSync(keyPath, "utf8").trim();
1540
+ if (!token) throw new WorkersConfigError(`key file is empty: ${keyPath}`);
1541
+ }
1542
+ return {
1543
+ ANTHROPIC_BASE_URL: provider.baseUrl,
1544
+ ANTHROPIC_AUTH_TOKEN: token,
1545
+ ANTHROPIC_DEFAULT_OPUS_MODEL: provider.models.default,
1546
+ ANTHROPIC_DEFAULT_SONNET_MODEL: provider.models.default,
1547
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: resolveModelCapability(provider, "fast"),
1548
+ ...provider.env,
1549
+ ENABLE_TOOL_SEARCH: "true",
1550
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
1551
+ };
1552
+ }
1553
+ function assertFallbackCapable(name, p) {
1554
+ if (p.protocol === "openai") throw new WorkersConfigError(`provider "${name}" speaks the openai protocol and only works through the live PAI proxy — fallback needs an anthropic-protocol provider every process can reach directly.`);
1555
+ if (p.engine === "codex") throw new WorkersConfigError(`provider "${name}" runs through the Codex CLI — fallback switches Claude Code itself and needs a claude-engine provider.`);
1556
+ if (!p.baseUrl) throw new WorkersConfigError(`provider "${name}" has no baseUrl — fallback cannot point at it.`);
1557
+ }
1558
+ /** The env block of a parsed settings.json record, or null when absent. */
1559
+ function envBlockOf(settings) {
1560
+ return typeof settings.env === "object" && settings.env !== null ? settings.env : null;
1561
+ }
1562
+ /** Apply a saved state to a parsed settings.json record (inverse of the switch). */
1563
+ function restoreSettings(settings, saved) {
1564
+ const keys = Object.keys(saved.env);
1565
+ if (saved.envExisted) {
1566
+ const env = { ...typeof settings.env === "object" && settings.env !== null ? settings.env : {} };
1567
+ for (const k of keys) {
1568
+ const v = saved.env[k];
1569
+ if (v === null) delete env[k];
1570
+ else env[k] = v;
1571
+ }
1572
+ settings.env = env;
1573
+ } else delete settings.env;
1574
+ if (saved.model === null) delete settings.model;
1575
+ else settings.model = saved.model;
1576
+ return keys;
1577
+ }
1578
+ function writeFallbackNote(logDir, r) {
1579
+ const path = fallbackNotePath(logDir);
1580
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
1581
+ writeFileSync(path, [
1582
+ `# Fallback active`,
1583
+ ``,
1584
+ `- switched: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ")} UTC`,
1585
+ `- provider: ${r.provider} — every NEW Claude Code process on this machine runs on it`,
1586
+ `- settings.json: env got ${r.envKeys.join(", ")}; top-level model pinned to ${r.model}`,
1587
+ `- turn off: pai worker fallback off (restores settings.json exactly)`,
1588
+ ``,
1589
+ `## Running sessions`,
1590
+ ``,
1591
+ `restart this session in its project directory; the new process uses ${r.provider}; keep the same AIBroker name`,
1592
+ ``
1593
+ ].join("\n"), "utf8");
1594
+ return path;
1595
+ }
1596
+ /**
1597
+ * Switch every new Claude Code process to a worker provider.
1598
+ *
1599
+ * Write order: the saved state lands in the pai config BEFORE settings.json
1600
+ * is touched, so a crash between the two writes leaves `off` able to restore
1601
+ * (and `on` re-applies idempotently, healing the gap).
1602
+ */
1603
+ function fallbackOn(providerFlag, paths = {}) {
1604
+ const settingsPath = paths.settingsPath ?? fallbackSettingsPath();
1605
+ const { raw, workers } = readWorkersSection(paths.configPath);
1606
+ const logDir = workersLogDir(workers);
1607
+ const { providerName, provider } = resolveTarget(workers, logDir, { flagProvider: providerFlag });
1608
+ assertFallbackCapable(providerName, provider);
1609
+ const env = fallbackEnv(provider);
1610
+ const envKeys = Object.keys(env);
1611
+ const settings = readJsonStrict(settingsPath, settingsPath);
1612
+ const prev = workers.fallback;
1613
+ if (prev && prev.provider === providerName) {
1614
+ settings.env = {
1615
+ ...envBlockOf(settings) ?? {},
1616
+ ...env
1617
+ };
1618
+ settings.model = provider.models.default;
1619
+ writeJsonAtomic(settingsPath, settings, { label: settingsPath });
1620
+ return {
1621
+ provider: providerName,
1622
+ alreadyOn: true,
1623
+ envKeys,
1624
+ model: provider.models.default
1625
+ };
1626
+ }
1627
+ if (prev) restoreSettings(settings, prev.saved);
1628
+ const envBlock = envBlockOf(settings);
1629
+ const saved = {
1630
+ env: {},
1631
+ model: typeof settings.model === "string" ? settings.model : null,
1632
+ envExisted: envBlock !== null
1633
+ };
1634
+ for (const k of envKeys) saved.env[k] = envBlock && typeof envBlock[k] === "string" ? envBlock[k] : null;
1635
+ workers.fallback = {
1636
+ provider: providerName,
1637
+ saved,
1638
+ on: (/* @__PURE__ */ new Date()).toISOString()
1639
+ };
1640
+ writeWorkersSection(raw, workers, paths.configPath);
1641
+ settings.env = {
1642
+ ...envBlock ?? {},
1643
+ ...env
1644
+ };
1645
+ settings.model = provider.models.default;
1646
+ writeJsonAtomic(settingsPath, settings, { label: settingsPath });
1647
+ const r = {
1648
+ provider: providerName,
1649
+ alreadyOn: false,
1650
+ envKeys,
1651
+ model: provider.models.default
1652
+ };
1653
+ writeFallbackNote(logDir, r);
1654
+ return r;
1655
+ }
1656
+ /** Restore settings.json exactly and clear the switch. Errors when not on. */
1657
+ function fallbackOff(paths = {}) {
1658
+ const settingsPath = paths.settingsPath ?? fallbackSettingsPath();
1659
+ const { raw, workers } = readWorkersSection(paths.configPath);
1660
+ const fb = workers.fallback;
1661
+ if (!fb) throw new WorkersConfigError("fallback is not on — nothing to restore");
1662
+ const settings = readJsonStrict(settingsPath, settingsPath);
1663
+ const envKeys = restoreSettings(settings, fb.saved);
1664
+ writeJsonAtomic(settingsPath, settings, { label: settingsPath });
1665
+ workers.fallback = null;
1666
+ writeWorkersSection(raw, workers, paths.configPath);
1667
+ try {
1668
+ const note = fallbackNotePath(workersLogDir(workers));
1669
+ if (existsSync(note)) unlinkSync(note);
1670
+ } catch {}
1671
+ return {
1672
+ provider: fb.provider,
1673
+ envKeys
1674
+ };
1675
+ }
1676
+ /** Running Claude Code processes: the AIBroker managers registry when it has
1677
+ * entries, else `ps`. Informational — sessions keep their provider until
1678
+ * restarted whatever this shows. */
1679
+ function listClaudeSessions() {
1680
+ try {
1681
+ const regPath = join(homedir(), ".aibroker", "managers.json");
1682
+ if (existsSync(regPath)) {
1683
+ const reg = JSON.parse(readFileSync(regPath, "utf8"));
1684
+ const names = Object.keys(reg);
1685
+ if (names.length) return {
1686
+ source: "aibroker",
1687
+ lines: names.map((name) => {
1688
+ const rec = typeof reg[name] === "object" && reg[name] !== null ? reg[name] : {};
1689
+ const bits = [name];
1690
+ for (const field of [
1691
+ "session",
1692
+ "cwd",
1693
+ "pid"
1694
+ ]) {
1695
+ const v = rec[field];
1696
+ if (typeof v === "string" || typeof v === "number") bits.push(`${field} ${v}`);
1697
+ }
1698
+ return bits.join(" ");
1699
+ })
1700
+ };
1701
+ }
1702
+ } catch {}
1703
+ try {
1704
+ return {
1705
+ source: "ps",
1706
+ lines: execFileSync("/bin/ps", ["-eo", "pid=,etime=,command="], {
1707
+ encoding: "utf8",
1708
+ timeout: 1e4
1709
+ }).split("\n").map((l) => l.trim()).filter(Boolean).map((l) => {
1710
+ const m = l.match(/^\d+\s+\S+\s+(.*)$/);
1711
+ return m ? m[1] : "";
1712
+ }).filter((cmd) => {
1713
+ const first = cmd.split(/\s+/)[0] ?? "";
1714
+ return first === "claude" || first.endsWith("/claude");
1715
+ }).map((cmd) => cmd.length > 100 ? cmd.slice(0, 97) + "…" : cmd).slice(0, 20)
1716
+ };
1717
+ } catch {
1718
+ return {
1719
+ source: "ps",
1720
+ lines: []
1721
+ };
1722
+ }
1723
+ }
1724
+ function fallbackStatus(paths = {}) {
1725
+ const { workers } = readWorkersSection(paths.configPath);
1726
+ const fb = workers.fallback;
1727
+ const sessions = listClaudeSessions();
1728
+ if (!fb) return {
1729
+ on: false,
1730
+ provider: null,
1731
+ onAt: null,
1732
+ notePath: null,
1733
+ sessionsSource: sessions.source,
1734
+ sessions: sessions.lines
1735
+ };
1736
+ const notePath = fallbackNotePath(workersLogDir(workers));
1737
+ return {
1738
+ on: true,
1739
+ provider: fb.provider,
1740
+ onAt: fb.on,
1741
+ notePath,
1742
+ sessionsSource: sessions.source,
1743
+ sessions: sessions.lines
1744
+ };
1745
+ }
1746
+ /** Human-readable status block for the CLI and the MCP tool. */
1747
+ function fallbackStatusText(s) {
1748
+ const lines = [];
1749
+ if (!s.on) {
1750
+ lines.push("fallback: off — new Claude Code processes run on the Anthropic login");
1751
+ lines.push("turn on with: pai worker fallback on [provider]");
1752
+ } else {
1753
+ lines.push(`fallback: on — provider ${s.provider} (since ${s.onAt})`);
1754
+ lines.push("every NEW Claude Code process runs on it; turn off with: pai worker fallback off");
1755
+ if (s.notePath) lines.push(`note: ${s.notePath}${existsSync(s.notePath) ? "" : " (missing)"}`);
1756
+ }
1757
+ lines.push("");
1758
+ lines.push(s.sessions.length ? `running Claude Code sessions (${s.sessionsSource}; they keep their current provider until restarted):` : `no running Claude Code sessions detected (${s.sessionsSource})`);
1759
+ lines.push(...s.sessions.length ? s.sessions.map((l) => ` ${l}`) : []);
1760
+ return lines;
1761
+ }
1762
+
1763
+ //#endregion
1764
+ export { statusLineOutput as S, updateProvider as _, addProvider as a, psOutput as b, describeProviders as c, resolveProviderName as d, setClass as f, unsetClass as g, setWorkersEnabled as h, fallbackStatusText as i, modelPrefsText as l, setProviderModel as m, fallbackOn as n, classTargetText as o, setProviderEnabled as p, fallbackStatus as r, describeModels as s, fallbackOff as t, removeProvider as u, useProvider as v, replayOutput as x, followWorkers as y };
1765
+ //# sourceMappingURL=fallback-CdXv-Np0.mjs.map