@tekmidian/pai 0.42.0 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/{chain-DGXNoTwc.mjs → chain-DsvSHnO9.mjs} +68 -37
  2. package/dist/chain-DsvSHnO9.mjs.map +1 -0
  3. package/dist/cli/index.mjs +8 -8
  4. package/dist/cli/program.mjs +8 -8
  5. package/dist/{config-DgzR5sfb.mjs → config-Bi_F6lXf.mjs} +31 -2
  6. package/dist/config-Bi_F6lXf.mjs.map +1 -0
  7. package/dist/{context-handover-cache-dUnSZwaA.mjs → context-handover-cache--bm6Siim.mjs} +27 -8
  8. package/dist/context-handover-cache--bm6Siim.mjs.map +1 -0
  9. package/dist/daemon/index.mjs +6 -6
  10. package/dist/{daemon-CKBkhOsZ.mjs → daemon-BOAPyL_b.mjs} +6 -6
  11. package/dist/{daemon-DsvP8KMy.mjs → daemon-DvXih78C.mjs} +11 -9
  12. package/dist/daemon-DvXih78C.mjs.map +1 -0
  13. package/dist/daemon-mcp/index.mjs +5 -5
  14. package/dist/{fallback-5UJO-RyX.mjs → fallback-8bzmg6No.mjs} +6 -6
  15. package/dist/fallback-8bzmg6No.mjs.map +1 -0
  16. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  17. package/dist/hooks/worker-proxy.mjs.map +2 -2
  18. package/dist/hooks/worker-status-line.mjs +4 -1
  19. package/dist/hooks/worker-status-line.mjs.map +2 -2
  20. package/dist/hooks/worker-supervision.mjs.map +2 -2
  21. package/dist/{main-resolver-DXGuygFG.mjs → main-resolver-CqppP_Bw.mjs} +2 -2
  22. package/dist/{main-resolver-BgIrC-D9.mjs → main-resolver-Dip2VLB5.mjs} +2 -2
  23. package/dist/{main-resolver-BgIrC-D9.mjs.map → main-resolver-Dip2VLB5.mjs.map} +1 -1
  24. package/dist/{planner-BLDJt3ZS.mjs → planner-ogy-Zy4O.mjs} +5 -5
  25. package/dist/{planner-BLDJt3ZS.mjs.map → planner-ogy-Zy4O.mjs.map} +1 -1
  26. package/dist/{program-CruRnLtc.mjs → program-B7N5OIJR.mjs} +10 -10
  27. package/dist/{program-CruRnLtc.mjs.map → program-B7N5OIJR.mjs.map} +1 -1
  28. package/dist/{scope-NIwCGxRI.mjs → scope-CfYnjy5R.mjs} +12 -3
  29. package/dist/scope-CfYnjy5R.mjs.map +1 -0
  30. package/dist/{server-Cp7tNzNC.mjs → server-DvN-IFIV.mjs} +26 -4
  31. package/dist/server-DvN-IFIV.mjs.map +1 -0
  32. package/dist/{work-queue-worker-D1p2V-ae.mjs → work-queue-worker-19r0cpH4.mjs} +4 -4
  33. package/dist/{work-queue-worker-7Vkli4Yh.mjs → work-queue-worker-DXh_FnT5.mjs} +2 -2
  34. package/dist/{work-queue-worker-7Vkli4Yh.mjs.map → work-queue-worker-DXh_FnT5.mjs.map} +1 -1
  35. package/docs/merge-demo.md +1 -0
  36. package/package.json +1 -1
  37. package/dist/chain-DGXNoTwc.mjs.map +0 -1
  38. package/dist/config-DgzR5sfb.mjs.map +0 -1
  39. package/dist/context-handover-cache-dUnSZwaA.mjs.map +0 -1
  40. package/dist/daemon-DsvP8KMy.mjs.map +0 -1
  41. package/dist/fallback-5UJO-RyX.mjs.map +0 -1
  42. package/dist/scope-NIwCGxRI.mjs.map +0 -1
  43. package/dist/server-Cp7tNzNC.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
- import { a as WorkersConfigError, h as resolveModelCapability, l as isModelCapability, m as readWorkersSection, o as assertProviderRunnable, p as providerKeyPath, s as classModelCapability } from "./config-DgzR5sfb.mjs";
2
- import { _ as workersLogDir, d as setCooldown, f as eventsPath, g as statusPath, h as panesDir, i as buildRunEnv, m as noMcpConfigPath, n as ensureProxyRunning, o as isQuotaFailure, p as ledgerPath, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveTarget } from "./server-Cp7tNzNC.mjs";
3
- import { S as stripPromptValues, _ as newWorkerId, a as resolveSpawnerSession, b as parseRunnerArgs, d as alive, g as loadStatuses, h as loadStatus, i as resolveSession, l as UNLABELED, m as describeTool, o as scopeKey, r as itermUuid, v as nowStamp, x as shortText, y as saveStatus } from "./scope-NIwCGxRI.mjs";
1
+ import { a as WorkersConfigError, g as resolveModelCapability, h as readWorkersSection, m as providerKeyPath, o as assertProviderRunnable, s as classModelCapability, u as isModelCapability } from "./config-Bi_F6lXf.mjs";
2
+ import { _ as workersLogDir, d as setCooldown, f as ensureNoMcpConfig, g as statusPath, h as panesDir, i as buildRunEnv, m as ledgerPath, n as ensureProxyRunning, o as isQuotaFailure, p as eventsPath, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveTarget } from "./server-DvN-IFIV.mjs";
3
+ import { C as stripPromptValues, S as shortText, _ as loadStatuses, a as resolveSpawnerSession, b as saveStatus, d as alive, g as loadStatus, i as resolveSession, l as UNLABELED, m as describeTool, o as scopeKey, r as itermUuid, v as newWorkerId, x as parseRunnerArgs, y as nowStamp } from "./scope-CfYnjy5R.mjs";
4
4
  import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
@@ -87,10 +87,14 @@ function ledgerSummary(path, scope, lastN = 15, now = /* @__PURE__ */ new Date()
87
87
  * one splits the lowest live worker pane horizontally, so panes stack top to
88
88
  * bottom. The split never sizes the new session — that would grow the whole
89
89
  * window; instead the window's bounds are read before the split and restored
90
- * right after, so the panes share the space the window already had. A split
91
- * would also make the new session the tab's active one the scripts re-select
92
- * the launching session right after, so a pane opening never steals the
93
- * keystrokes the operator is typing. Each
90
+ * right after, so the panes share the space the window already had. The follow
91
+ * command is part of the split itself iTerm creates the new session already
92
+ * running it so no text is ever typed into any session afterwards; a separate
93
+ * typing step raced the operator's keystrokes (focus briefly sits on the new
94
+ * pane after a split, and in one observed failure the command landed in the
95
+ * operator's own shell). A split still makes the new session the tab's active
96
+ * one — the scripts re-select the launching session right after, so a pane
97
+ * opening never steals the keystrokes the operator is typing. Each
94
98
  * pane runs `pai worker follow <id> --auto-exit <n>` under the `pai-worker`
95
99
  * dynamic profile (Close Sessions On End), so panes disappear by themselves.
96
100
  *
@@ -145,34 +149,35 @@ const WORKER_SPLIT_SCRIPT = `on run(argv)
145
149
  end if
146
150
  end if
147
151
  end repeat
152
+ -- the follow command is part of the split itself: the
153
+ -- pane is born already running it, so no text is ever
154
+ -- typed into any session — a typing step raced the
155
+ -- operator's keystrokes and could even land in the
156
+ -- operator's own session
148
157
  if profileName is "" then
149
158
  if splitS is missing value then
150
159
  tell s
151
- set newS to split vertically with default profile
160
+ set newS to split vertically with default profile command followCmd
152
161
  end tell
153
162
  else
154
163
  tell splitS
155
- set newS to split horizontally with default profile
164
+ set newS to split horizontally with default profile command followCmd
156
165
  end tell
157
166
  end if
158
167
  else
159
168
  if splitS is missing value then
160
169
  tell s
161
- set newS to split vertically with profile profileName
170
+ set newS to split vertically with profile profileName command followCmd
162
171
  end tell
163
172
  else
164
173
  tell splitS
165
- set newS to split horizontally with profile profileName
174
+ set newS to split horizontally with profile profileName command followCmd
166
175
  end tell
167
176
  end if
168
177
  end if
169
- tell newS
170
- write text followCmd
171
- end tell
172
178
  -- a split makes the new session the tab's active one:
173
- -- re-select the launching session so keystrokes keep
174
- -- going where the operator was typing, not into the
175
- -- pane's chat prompt
179
+ -- re-select the launching session so focus returns to
180
+ -- where the operator was typing, never the new pane
176
181
  try
177
182
  select s
178
183
  end try
@@ -253,11 +258,11 @@ const SPLIT_SCRIPT = `on run(argv)
253
258
  repeat with t in tabs of w
254
259
  repeat with s in sessions of t
255
260
  if id of s is targetID then
261
+ -- the follow command is part of the split itself (see
262
+ -- WORKER_SPLIT_SCRIPT): nothing is ever typed into a
263
+ -- session afterwards
256
264
  tell s
257
- set newS to split vertically with default profile
258
- end tell
259
- tell newS
260
- write text followCmd
265
+ set newS to split vertically with default profile command followCmd
261
266
  end tell
262
267
  -- keep the tab's active session where it was (see
263
268
  -- WORKER_SPLIT_SCRIPT)
@@ -490,6 +495,23 @@ function saveRegistry(path, reg) {
490
495
  writeFileSync(tmp, JSON.stringify(reg, null, 1), "utf8");
491
496
  renameSync(tmp, path);
492
497
  }
498
+ /**
499
+ * The command a worker pane runs: one exec-able line with no shell syntax —
500
+ * iTerm's split command parameter execs without a shell (proven live: panes
501
+ * survive `sleep 30` and a single-quoted `/bin/sh -c`, die instantly on
502
+ * `export …; …` or a redirect), so the command must be one exec-able line.
503
+ * Absolute node on the CLI entry file also sidesteps the panes' bare launchd
504
+ * PATH (no pai, no node) and the `#!/usr/bin/env node` shebang.
505
+ */
506
+ function followCommand(wid, autoExitSecs) {
507
+ return `${process.execPath} ${absoluteCliPath()} worker follow ${wid} --auto-exit ${autoExitSecs}`;
508
+ }
509
+ /** Absolute path of the running pai CLI; falls back to PATH lookup at spawn. */
510
+ function absoluteCliPath() {
511
+ const argv1 = process.argv[1] ?? "";
512
+ if (argv1.endsWith("pai")) return argv1;
513
+ return "pai";
514
+ }
493
515
  /** Open (or only report on) the stacked follow pane of one worker. */
494
516
  async function openPaneForWorker(logDir, config, wid, term) {
495
517
  if (workerPaneOpen(wid)) return `pane for ${wid} already open`;
@@ -497,7 +519,7 @@ async function openPaneForWorker(logDir, config, wid, term) {
497
519
  const regPath = join(panesDir(logDir), `${scopeKey(term)}.json`);
498
520
  const reg = loadRegistry(regPath);
499
521
  const profile = await followProfile(config.pane.fontSize);
500
- const cmd = ` exec pai worker follow ${JSON.stringify(wid)} --auto-exit ${config.pane.autoExitSecs}`;
522
+ const cmd = followCommand(wid, config.pane.autoExitSecs);
501
523
  const p = await osascript(WORKER_SPLIT_SCRIPT, [
502
524
  uid,
503
525
  [...reg].reverse().map((e) => e.session).join(","),
@@ -571,7 +593,7 @@ async function openFollowPane(logDir, _config, term, checkOnly) {
571
593
  if (out === "notrunning") throw new Error("pai worker pane: iTerm2 is not running");
572
594
  if ([...new Set(out.split(",").map((t) => t.trim()).filter(Boolean))].filter((t) => followTtys().has(t)).length > 0) return "follow pane already open";
573
595
  if (checkOnly) return "no follow pane";
574
- const s = await osascript(SPLIT_SCRIPT, [uid, " exec pai worker follow"]);
596
+ const s = await osascript(SPLIT_SCRIPT, [uid, `${process.execPath} ${absoluteCliPath()} worker follow`]);
575
597
  const sout = s.stdout.trim();
576
598
  if (sout === "opened") return "follow pane opened";
577
599
  if (sout === "notfound") throw new Error("pai worker pane: no open iTerm2 session matches ITERM_SESSION_ID");
@@ -1627,8 +1649,12 @@ function codexInstalled() {
1627
1649
  * demand, the provider name in the URL path); codex-engine providers run
1628
1650
  * the Codex CLI.
1629
1651
  * - headless (-p): strict empty MCP config unless the caller brings one or
1630
- * names servers via --mcp / a role (then a filtered <id>.mcp.json),
1631
- * PAI_WORKER=1 so PAI's per-session hooks leave it alone, the worker
1652
+ * names servers via --mcp / a role (then a filtered <id>.mcp.json), the
1653
+ * core tool grants the caller did not bring (a headless run cannot
1654
+ * approve a permission-gated tool mid-flight — with no grant at all,
1655
+ * claude drops the file/shell tools entirely and the worker is
1656
+ * tool-blind), PAI_WORKER=1 so PAI's per-session hooks leave it alone,
1657
+ * the worker
1632
1658
  * contract appended to the system prompt, `--input-format stream-json`
1633
1659
  * with the prompt as the first stdin user message (the operator socket
1634
1660
  * can add more mid-run), stream-json mirroring (every line stamped `_ts`)
@@ -1641,15 +1667,6 @@ function codexInstalled() {
1641
1667
  * Auto-routed runs that die of a quota error before the first tool call are
1642
1668
  * restarted on the next provider in routing order (WORKER-REROUTE ledger line).
1643
1669
  */
1644
- /** The strict empty MCP config for headless workers, written on demand. */
1645
- function ensureNoMcpConfig(logDir) {
1646
- const path = noMcpConfigPath(logDir);
1647
- if (!existsSync(path)) {
1648
- mkdirSync(logDir, { recursive: true });
1649
- writeFileSync(path, "{ \"mcpServers\": {} }\n", "utf8");
1650
- }
1651
- return path;
1652
- }
1653
1670
  /** A stream-json user message for the child's stdin. */
1654
1671
  function stdinUserMessage(text) {
1655
1672
  return JSON.stringify({
@@ -1725,7 +1742,7 @@ async function runWorker(opts) {
1725
1742
  const logDir = workersLogDir(config);
1726
1743
  mkdirSync(logDir, { recursive: true });
1727
1744
  if (opts.className === "plan" && !opts._planner) {
1728
- const { runPlanner } = await import("./planner-BLDJt3ZS.mjs");
1745
+ const { runPlanner } = await import("./planner-ogy-Zy4O.mjs");
1729
1746
  return runPlanner(opts);
1730
1747
  }
1731
1748
  const parent = launchParent(opts.parent);
@@ -1784,6 +1801,19 @@ async function runWorker(opts) {
1784
1801
  throw e;
1785
1802
  }
1786
1803
  }
1804
+ /**
1805
+ * The core tools a headless run grants when the caller brings none of its
1806
+ * own. Permission-gated tools (Bash, Read, Write, …) are never offered to a
1807
+ * headless session that has no allow rule for them — the child starts with
1808
+ * only the tools that never ask (tool search, web, cron, tasks) and cannot
1809
+ * touch a file or shell (2026-09-18). A caller's own --allowedTools is a
1810
+ * deliberate restriction and passes through untouched.
1811
+ */
1812
+ const DEFAULT_WORKER_TOOLS = "Read,Edit,Write,Bash,Grep,Glob";
1813
+ /** The --allowedTools args a headless run needs; [] when the caller granted. */
1814
+ function headlessToolGrants(allowedTools) {
1815
+ return allowedTools.length ? [] : ["--allowedTools", DEFAULT_WORKER_TOOLS];
1816
+ }
1787
1817
  async function executeRun(a) {
1788
1818
  const { config, logDir, target, model, label, parsed, noPane } = a;
1789
1819
  const headless = parsed.headless;
@@ -1872,9 +1902,10 @@ async function executeRun(a) {
1872
1902
  ];
1873
1903
  }
1874
1904
  const restArgs = headless ? stripPromptValues(parsed.rest) : parsed.rest;
1905
+ const toolArgs = headless ? headlessToolGrants(parsed.allowedTools) : [];
1875
1906
  const cmd = ["claude"];
1876
1907
  if (!parsed.callerModel) cmd.push("--model", model);
1877
- cmd.push(...mcpArgs, ...restArgs);
1908
+ cmd.push(...mcpArgs, ...toolArgs, ...restArgs);
1878
1909
  if (headless) {
1879
1910
  cmd.push("--output-format", "stream-json", "--verbose", "--input-format", "stream-json");
1880
1911
  if (!parsed.callerSystemPrompt) cmd.push("--append-system-prompt", WORKER_CONTRACT_PROMPT);
@@ -2567,4 +2598,4 @@ async function runChain(opts, deps = {}) {
2567
2598
 
2568
2599
  //#endregion
2569
2600
  export { openPaneForWorker as _, testProvider as a, renderReport as c, handoffFromInside as d, readInbox as f, openFollowPane as g, checkPaneForWorker as h, runWorker as i, discardWorker as l, workerDepth as m, swapPromptArg as n, describeMcp as o, sayToWorker as p, printResult as r, parseWorkerReport as s, runChain as t, mergeWorker as u, appendLedger as v, ledgerSummary as y };
2570
- //# sourceMappingURL=chain-DGXNoTwc.mjs.map
2601
+ //# sourceMappingURL=chain-DsvSHnO9.mjs.map