@cydm/pie 2.1.1 → 2.3.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.
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
2
  import {
3
3
  createAskUserCapability
4
- } from "../../../chunks/chunk-LU7RKALH.js";
4
+ } from "../../../chunks/chunk-UPPICLVS.js";
5
5
  import "../../../chunks/chunk-QJH6YN2W.js";
6
6
  import "../../../chunks/chunk-IM43UYVV.js";
7
7
  import "../../../chunks/chunk-XXGYUYXI.js";
@@ -3776,10 +3776,26 @@ function containsEditable(node) {
3776
3776
  if (node.canSetValue || node.role.toLowerCase().includes("text")) return true;
3777
3777
  return node.children.some(containsEditable);
3778
3778
  }
3779
+ function wireableInputDescendants(node) {
3780
+ const matches = [];
3781
+ const visit = (current) => {
3782
+ for (const child of current.children) {
3783
+ if (child.isTextInput && child.wireRef && !child.pictureOnly && !child.offscreen) matches.push(child);
3784
+ visit(child);
3785
+ }
3786
+ };
3787
+ visit(node);
3788
+ return matches;
3789
+ }
3779
3790
  function recordActionFocus(action, state, node, coordinateFocus = false) {
3780
3791
  if (!["click", "press", "keypress", "typeText"].includes(action.action) || !action.ref && action.action !== "click") return;
3781
3792
  state.currentFocus = Boolean(node?.isTextInput && !node.pictureOnly) || coordinateFocus;
3782
3793
  state.focusRef = node?.isTextInput && node.wireRef && !node.pictureOnly ? node.ref : void 0;
3794
+ if (!state.currentFocus && node && !node.pictureOnly && action.ref && (action.action === "click" || action.action === "press") && containsEditable(node)) {
3795
+ const wireable = wireableInputDescendants(node);
3796
+ state.currentFocus = true;
3797
+ state.focusRef = wireable.length === 1 ? wireable[0].ref : void 0;
3798
+ }
3783
3799
  }
3784
3800
  function editorAtPoint(nodes, x, y) {
3785
3801
  const matches = nodes.filter((node) => node.isTextInput && node.wireRef && !node.pictureOnly && !node.offscreen && node.rect && x >= node.rect.x && x < node.rect.x + node.rect.w && y >= node.rect.y && y < node.rect.y + node.rect.h);
@@ -3790,24 +3806,23 @@ function prepareAction(action, state, env) {
3790
3806
  const focusedKeyboard = !env.headless && state.currentFocus && !action.ref && (operation === "typeText" || operation === "keypress");
3791
3807
  const usesCurrentFocus = focusedKeyboard && !state.focusRef;
3792
3808
  const target = usesCurrentFocus ? focusedTarget(env) : nativeTarget(focusedKeyboard && state.focusRef ? { ...action, ref: state.focusRef } : action, operation, env);
3793
- const establishesFocus = !env.headless && Boolean(action.ref) && (operation === "click" || operation === "press") && containsEditable(env.node(action.ref));
3794
3809
  const needsForeground = !env.headless && (operation === "click" || operation === "press") && "x" in target;
3795
3810
  switch (operation) {
3796
3811
  case "press":
3797
3812
  case "click":
3798
- return { action: operation, target, params: { button: mouseButton(action.button), clickCount: clickCount(action.clickCount) }, establishesFocus, usesCurrentFocus: false, needsForeground };
3813
+ return { action: operation, target, params: { button: mouseButton(action.button), clickCount: clickCount(action.clickCount) }, usesCurrentFocus: false, needsForeground };
3799
3814
  case "setText":
3800
- return { action: operation, target, params: { text: action.text ?? "" }, establishesFocus: false, usesCurrentFocus: false, needsForeground: false };
3815
+ return { action: operation, target, params: { text: action.text ?? "" }, usesCurrentFocus: false, needsForeground: false };
3801
3816
  case "typeText":
3802
- return { action: operation, target, params: { text: action.text ?? "" }, establishesFocus: false, usesCurrentFocus, needsForeground: false };
3817
+ return { action: operation, target, params: { text: action.text ?? "" }, usesCurrentFocus, needsForeground: false };
3803
3818
  case "keypress":
3804
- return { action: operation, target, params: { keys: keys(action.keys) }, establishesFocus: false, usesCurrentFocus, needsForeground: false };
3819
+ return { action: operation, target, params: { keys: keys(action.keys) }, usesCurrentFocus, needsForeground: false };
3805
3820
  case "scroll":
3806
- return { action: operation, target, params: { scrollX: scrollDelta(action.scrollX), scrollY: scrollDelta(action.scrollY) }, establishesFocus: false, usesCurrentFocus: false, needsForeground: false };
3821
+ return { action: operation, target, params: { scrollX: scrollDelta(action.scrollX), scrollY: scrollDelta(action.scrollY) }, usesCurrentFocus: false, needsForeground: false };
3807
3822
  case "drag":
3808
- return { action: operation, target, params: { path: path(action.path, env) }, establishesFocus: false, usesCurrentFocus: false, needsForeground: false };
3823
+ return { action: operation, target, params: { path: path(action.path, env) }, usesCurrentFocus: false, needsForeground: false };
3809
3824
  case "moveMouse":
3810
- return { action: operation, target, params: {}, establishesFocus: false, usesCurrentFocus: false, needsForeground: false };
3825
+ return { action: operation, target, params: {}, usesCurrentFocus: false, needsForeground: false };
3811
3826
  }
3812
3827
  }
3813
3828
  function canRetryInForeground(action, outcome, headless) {
@@ -6901,17 +6916,28 @@ async function releaseOwnedBrowser(child, port, profile) {
6901
6916
  if (port) await requestBrowserClose(port).catch(() => void 0);
6902
6917
  if (!await waitForExit(child, 700)) {
6903
6918
  if (process.platform === "win32" && child.pid) {
6904
- await new Promise((resolve, reject) => execFile("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], {
6905
- windowsHide: true,
6906
- timeout: 2e3
6907
- }, (error) => {
6908
- if (error && child.exitCode === null) reject(error);
6909
- else resolve();
6910
- }));
6919
+ const pid = child.pid;
6920
+ await new Promise((resolve) => {
6921
+ execFile("taskkill.exe", ["/PID", String(pid), "/T", "/F"], {
6922
+ windowsHide: true,
6923
+ timeout: 2e3
6924
+ }, () => resolve());
6925
+ });
6926
+ if (!await waitForExit(child, 1500)) {
6927
+ try {
6928
+ process.kill(pid);
6929
+ } catch {
6930
+ }
6931
+ if (!await waitForExit(child, 2e3)) {
6932
+ throw new Error(`Owned browser process ${pid} survived taskkill escalation.`);
6933
+ }
6934
+ }
6911
6935
  } else child.kill("SIGTERM");
6912
- if (!await waitForExit(child, 2e3)) {
6913
- if (process.platform !== "win32") child.kill("SIGKILL");
6914
- if (!await waitForExit(child, 2e3)) throw new Error("Owned browser did not exit.");
6936
+ if (process.platform !== "win32") {
6937
+ if (!await waitForExit(child, 2e3)) {
6938
+ child.kill("SIGKILL");
6939
+ if (!await waitForExit(child, 2e3)) throw new Error("Owned browser did not exit.");
6940
+ }
6915
6941
  }
6916
6942
  }
6917
6943
  }
@@ -7206,16 +7232,24 @@ function shutdownComputerUseSession() {
7206
7232
  }
7207
7233
  async function releaseComputerUseSession() {
7208
7234
  const drain = resourceScheduler.close();
7209
- await disconnectCdp();
7210
- await currentPlatformBackend.shutdown?.();
7211
- await drain;
7212
- await releaseOwnedBrowserState(runtimeState);
7235
+ const failures = [];
7236
+ const attempt = (step, work) => work().catch((error) => {
7237
+ failures.push(error instanceof Error ? new Error(`${step}: ${error.message}`) : error);
7238
+ });
7239
+ await attempt("disconnect CDP", () => disconnectCdp());
7240
+ await attempt("platform shutdown", async () => {
7241
+ await currentPlatformBackend.shutdown?.();
7242
+ });
7243
+ await attempt("drain scheduler", () => drain);
7244
+ await attempt("release owned browser", () => releaseOwnedBrowserState(runtimeState));
7213
7245
  if (runtimeState.managedBrowserCdpPort && process.env.PIE_COMPUTER_USE_CDP_PORT === runtimeState.managedBrowserCdpPort) {
7214
7246
  if (runtimeState.previousCdpPort === void 0) delete process.env.PIE_COMPUTER_USE_CDP_PORT;
7215
7247
  else process.env.PIE_COMPUTER_USE_CDP_PORT = runtimeState.previousCdpPort;
7216
7248
  }
7217
- runtimeState.managedBrowserCdpPort = void 0;
7218
7249
  runtimeState.previousCdpPort = void 0;
7250
+ if (runtimeState.managedBrowser === void 0) {
7251
+ runtimeState.managedBrowserCdpPort = void 0;
7252
+ }
7219
7253
  savedStates.clear();
7220
7254
  clearStoredOutputs();
7221
7255
  runtimeState.windowRefs.clear();
@@ -7227,6 +7261,8 @@ async function releaseComputerUseSession() {
7227
7261
  runtimeState.helperDiagnostics = void 0;
7228
7262
  runtimeState.lastPermissionCheckAt = 0;
7229
7263
  resourceScheduler = new ResourceScheduler();
7264
+ if (failures.length === 1) throw failures[0];
7265
+ if (failures.length > 1) throw new AggregateError(failures, "computer-use session release had partial failures (state was reset)");
7230
7266
  }
7231
7267
  function currentRuntimeMode() {
7232
7268
  return isHeadlessMode() ? "stealth" : "default";
@@ -0,0 +1,272 @@
1
+ import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
+ import "../../../chunks/chunk-LZQXQOPJ.js";
3
+
4
+ // builtin/extensions/herdr-agent-state/index.ts
5
+ import { spawn } from "node:child_process";
6
+ var AGENT_LABEL = "pie";
7
+ var SOURCE_ID = "pie";
8
+ var BLOCKING_TOOL_NAME = "ask_user_multi";
9
+ var REPORT_TIMEOUT_MS = 3e3;
10
+ var MAX_RESUME_ARGS = 64;
11
+ var MAX_RESUME_TOTAL_CHARS = 8192;
12
+ var RESUME_ARG_PATTERN = /^[A-Za-z0-9._:/=@,%+-]+$/;
13
+ var MESSAGE_MAX_CHARS = 120;
14
+ function readHerdrEnvironment(env = process.env) {
15
+ if (env.HERDR_ENV !== "1") return void 0;
16
+ const paneId = env.HERDR_PANE_ID;
17
+ const binPath = env.HERDR_BIN_PATH;
18
+ if (typeof paneId !== "string" || !paneId) return void 0;
19
+ if (typeof binPath !== "string" || !binPath) return void 0;
20
+ return { paneId, binPath };
21
+ }
22
+ function createReportSequencer(now = Date.now) {
23
+ let seq = now() * 1e3;
24
+ return () => {
25
+ seq += 1;
26
+ return seq;
27
+ };
28
+ }
29
+ function summarizeAskUserQuestion(args) {
30
+ if (!args || typeof args !== "object") return void 0;
31
+ const record = args;
32
+ let text = record.question;
33
+ if (typeof text !== "string" || !text.trim()) {
34
+ const questions = record.questions;
35
+ if (Array.isArray(questions) && questions.length > 0) {
36
+ const first = questions[0];
37
+ if (first && typeof first === "object") {
38
+ text = first.question;
39
+ }
40
+ }
41
+ }
42
+ if (typeof text !== "string") return void 0;
43
+ const cleaned = text.replace(/\s+/g, " ").trim();
44
+ if (!cleaned) return void 0;
45
+ return cleaned.length > MESSAGE_MAX_CHARS ? `${cleaned.slice(0, MESSAGE_MAX_CHARS - 3)}...` : cleaned;
46
+ }
47
+ function buildResumeArgv(sessionId) {
48
+ const argv = ["pie", "chat", "--session-id", sessionId];
49
+ if (argv.length > MAX_RESUME_ARGS) return void 0;
50
+ if (argv.join(" ").length > MAX_RESUME_TOTAL_CHARS) return void 0;
51
+ for (const arg of argv) {
52
+ if (!RESUME_ARG_PATTERN.test(arg)) return void 0;
53
+ }
54
+ return argv;
55
+ }
56
+ function buildStateReportArgs(params) {
57
+ const args = [
58
+ "pane",
59
+ "report-agent",
60
+ params.paneId,
61
+ "--source",
62
+ params.source,
63
+ "--agent",
64
+ params.agent,
65
+ "--state",
66
+ params.state,
67
+ "--seq",
68
+ String(params.seq)
69
+ ];
70
+ if (params.message) {
71
+ args.push("--message", params.message);
72
+ }
73
+ if (params.agentSessionId) {
74
+ args.push("--agent-session-id", params.agentSessionId);
75
+ }
76
+ if (params.resumeArgv && params.resumeArgv.length > 0) {
77
+ args.push("--", ...params.resumeArgv);
78
+ }
79
+ return args;
80
+ }
81
+ function buildSessionReportArgs(params) {
82
+ const args = [
83
+ "pane",
84
+ "report-agent-session",
85
+ params.paneId,
86
+ "--source",
87
+ params.source,
88
+ "--agent",
89
+ params.agent,
90
+ "--seq",
91
+ String(params.seq),
92
+ "--agent-session-id",
93
+ params.agentSessionId
94
+ ];
95
+ if (params.resumeArgv?.length) args.push("--", ...params.resumeArgv);
96
+ return args;
97
+ }
98
+ function buildReleaseReportArgs(params) {
99
+ return [
100
+ "pane",
101
+ "release-agent",
102
+ params.paneId,
103
+ "--source",
104
+ params.source,
105
+ "--agent",
106
+ params.agent,
107
+ "--seq",
108
+ String(params.seq)
109
+ ];
110
+ }
111
+ function spawnHerdrReport(binPath, args, timeoutMs = REPORT_TIMEOUT_MS) {
112
+ try {
113
+ const child = spawn(binPath, args, { detached: true, stdio: "ignore" });
114
+ let killTimer;
115
+ const clearTimers = () => {
116
+ clearTimeout(timer);
117
+ if (killTimer) clearTimeout(killTimer);
118
+ };
119
+ const timer = setTimeout(() => {
120
+ try {
121
+ child.kill("SIGTERM");
122
+ killTimer = setTimeout(() => {
123
+ try {
124
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
125
+ } catch {
126
+ }
127
+ }, 1e3);
128
+ killTimer.unref?.();
129
+ } catch {
130
+ }
131
+ }, timeoutMs);
132
+ timer.unref?.();
133
+ child.on("error", clearTimers);
134
+ child.on("exit", clearTimers);
135
+ child.unref();
136
+ } catch {
137
+ }
138
+ }
139
+ function attachHerdrReporter(host, herdr, transport = (args) => spawnHerdrReport(herdr.binPath, args), sequencer = createReportSequencer()) {
140
+ const nextSeq = sequencer;
141
+ let agentActive = false;
142
+ const blockingToolCalls = /* @__PURE__ */ new Map();
143
+ let blockedMessage;
144
+ let heldPane = false;
145
+ let lastSessionId;
146
+ let lastState;
147
+ let lastMessage;
148
+ function report(args) {
149
+ transport(args);
150
+ }
151
+ function publishState(options) {
152
+ const next = desiredState();
153
+ const unchanged = next.state === lastState && next.message === lastMessage && !options?.force && !options?.resumeArgv;
154
+ if (unchanged) return;
155
+ lastState = next.state;
156
+ lastMessage = next.message;
157
+ heldPane = true;
158
+ report(
159
+ buildStateReportArgs({
160
+ paneId: herdr.paneId,
161
+ source: SOURCE_ID,
162
+ agent: AGENT_LABEL,
163
+ state: next.state,
164
+ seq: nextSeq(),
165
+ message: next.message,
166
+ agentSessionId: lastSessionId,
167
+ resumeArgv: options?.resumeArgv
168
+ })
169
+ );
170
+ }
171
+ function publishSession(sessionId) {
172
+ const resumeArgv = buildResumeArgv(sessionId);
173
+ if (!heldPane) {
174
+ return;
175
+ }
176
+ report(
177
+ buildSessionReportArgs({
178
+ paneId: herdr.paneId,
179
+ source: SOURCE_ID,
180
+ agent: AGENT_LABEL,
181
+ seq: nextSeq(),
182
+ agentSessionId: sessionId,
183
+ resumeArgv
184
+ })
185
+ );
186
+ }
187
+ function desiredState() {
188
+ if (blockingToolCalls.size > 0) {
189
+ return { state: "blocked", message: blockedMessage };
190
+ }
191
+ if (agentActive) {
192
+ return { state: "working" };
193
+ }
194
+ return { state: "idle" };
195
+ }
196
+ host.on("session:changed", (event) => {
197
+ const sessionId = typeof event.sessionId === "string" && event.sessionId ? event.sessionId : void 0;
198
+ const sessionIdChanged = sessionId !== lastSessionId;
199
+ lastSessionId = sessionId;
200
+ if (!heldPane) {
201
+ publishState({
202
+ force: true,
203
+ resumeArgv: sessionId ? buildResumeArgv(sessionId) : void 0
204
+ });
205
+ return;
206
+ }
207
+ if (sessionIdChanged && sessionId) {
208
+ publishSession(sessionId);
209
+ }
210
+ });
211
+ host.on("turn:start", () => {
212
+ agentActive = true;
213
+ publishState();
214
+ });
215
+ host.on("agent:end", () => {
216
+ agentActive = false;
217
+ blockingToolCalls.clear();
218
+ blockedMessage = void 0;
219
+ publishState();
220
+ });
221
+ host.on("tool:execution:start", (event) => {
222
+ if (event.toolName !== BLOCKING_TOOL_NAME) return;
223
+ const message = summarizeAskUserQuestion(event.args);
224
+ blockingToolCalls.set(event.toolCallId, message);
225
+ if (message) {
226
+ blockedMessage = message;
227
+ } else if (!blockedMessage) {
228
+ blockedMessage = "waiting for your answer";
229
+ }
230
+ publishState();
231
+ });
232
+ host.on("tool:execution:end", (event) => {
233
+ if (event.toolName !== BLOCKING_TOOL_NAME) return;
234
+ blockingToolCalls.delete(event.toolCallId);
235
+ blockedMessage = void 0;
236
+ for (const message of blockingToolCalls.values()) {
237
+ if (message) blockedMessage = message;
238
+ }
239
+ publishState();
240
+ });
241
+ host.registerCleanup?.((reason) => {
242
+ if (reason !== "shutdown") return;
243
+ if (!heldPane) return;
244
+ report(
245
+ buildReleaseReportArgs({
246
+ paneId: herdr.paneId,
247
+ source: SOURCE_ID,
248
+ agent: AGENT_LABEL,
249
+ seq: nextSeq()
250
+ })
251
+ );
252
+ });
253
+ }
254
+ function herdrAgentStateExtension(ctx) {
255
+ const herdr = readHerdrEnvironment();
256
+ if (!herdr) return;
257
+ if (!ctx.hasUI) return;
258
+ ctx.log("herdr agent-state reporter enabled");
259
+ attachHerdrReporter(ctx, herdr);
260
+ }
261
+ export {
262
+ attachHerdrReporter,
263
+ buildReleaseReportArgs,
264
+ buildResumeArgv,
265
+ buildSessionReportArgs,
266
+ buildStateReportArgs,
267
+ createReportSequencer,
268
+ herdrAgentStateExtension as default,
269
+ readHerdrEnvironment,
270
+ spawnHerdrReport,
271
+ summarizeAskUserQuestion
272
+ };
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "herdr-agent-state-extension",
3
+ "version": "1.0.0",
4
+ "description": "Report Pie interactive agent lifecycle state (working/idle/blocked), session identity, and resume command to the Herdr terminal workspace manager.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "pie": {
8
+ "name": "herdr-agent-state",
9
+ "main": "index.js"
10
+ }
11
+ }
@@ -544,6 +544,7 @@ function normalizeEntry(name, raw, sourcePath, sourceScope, homeDir) {
544
544
  const hasStdio = typeof raw.command === "string" && raw.command.length > 0;
545
545
  const hasHttp = typeof raw.url === "string" && raw.url.length > 0;
546
546
  if (!hasStdio && !hasHttp) return null;
547
+ const directTools = normalizeDirectTools(raw.directTools);
547
548
  return {
548
549
  name,
549
550
  command: hasStdio ? expandHome(raw.command, homeDir) : void 0,
@@ -554,8 +555,8 @@ function normalizeEntry(name, raw, sourcePath, sourceScope, homeDir) {
554
555
  headers: asStringRecord(raw.headers),
555
556
  inheritEnv: raw.inheritEnv !== false,
556
557
  connectTimeoutMs: typeof raw.connectTimeoutMs === "number" && Number.isFinite(raw.connectTimeoutMs) && raw.connectTimeoutMs > 0 ? raw.connectTimeoutMs : void 0,
557
- directTools: normalizeDirectTools(raw.directTools).mode,
558
- directToolsWhitelist: normalizeDirectTools(raw.directTools).whitelist,
558
+ directTools: directTools.mode,
559
+ directToolsWhitelist: directTools.whitelist,
559
560
  disabled: raw.disabled === true,
560
561
  sourcePath,
561
562
  sourceScope
@@ -615,8 +616,8 @@ function resolveMcpAdapterConfig(cwd, configDir, homeDir = os.homedir()) {
615
616
  continue;
616
617
  }
617
618
  const existing = merged.get(name);
618
- if (existing && raw.disabled === true) {
619
- merged.set(name, { ...existing, disabled: true });
619
+ if (existing && (raw.disabled === true || raw.disabled === false)) {
620
+ merged.set(name, { ...existing, disabled: raw.disabled });
620
621
  }
621
622
  }
622
623
  }
@@ -24438,6 +24439,7 @@ import * as path2 from "node:path";
24438
24439
  var CONNECT_TIMEOUT_MS = 3e4;
24439
24440
  var PACKAGE_MANAGER_CONNECT_TIMEOUT_MS = 12e4;
24440
24441
  var PACKAGE_MANAGER_COMMANDS = /* @__PURE__ */ new Set(["npx", "npm", "bunx", "uvx", "uv", "pnpm", "pnpmx"]);
24442
+ var METADATA_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
24441
24443
  var CALL_TIMEOUT_MS = 12e4;
24442
24444
  var STDERR_TAIL_LIMIT = 4e3;
24443
24445
  function connectTimeoutFor(definition) {
@@ -24445,6 +24447,17 @@ function connectTimeoutFor(definition) {
24445
24447
  const base = path2.basename(definition.command ?? "");
24446
24448
  return PACKAGE_MANAGER_COMMANDS.has(base) ? PACKAGE_MANAGER_CONNECT_TIMEOUT_MS : CONNECT_TIMEOUT_MS;
24447
24449
  }
24450
+ function unpinnedWarnings(definitions) {
24451
+ const commandPattern = new RegExp(`(^|\\s)(?:${[...PACKAGE_MANAGER_COMMANDS].join("|")})\\s`);
24452
+ const warnings = [];
24453
+ for (const definition of definitions) {
24454
+ const spec = [definition.command ?? "", ...definition.args].join(" ");
24455
+ if (commandPattern.test(spec) && /@latest\b/.test(spec)) {
24456
+ warnings.push(`${definition.name}: unpinned "@latest" fetch \u2014 pin an exact version (e.g. pkg@1.2.3).`);
24457
+ }
24458
+ }
24459
+ return warnings;
24460
+ }
24448
24461
  function appendTail(tail, chunk) {
24449
24462
  return (tail + String(chunk)).slice(-STDERR_TAIL_LIMIT);
24450
24463
  }
@@ -24487,11 +24500,8 @@ function withTimeout(promise, timeoutMs, label, signal) {
24487
24500
  });
24488
24501
  }
24489
24502
  var McpServerManager = class {
24490
- runtimes = /* @__PURE__ */ new Map();
24491
- definitions;
24492
- cachePath;
24493
- log;
24494
- constructor(definitions, cachePath, log) {
24503
+ constructor(definitions, cacheDir, log, now = Date.now) {
24504
+ this.now = now;
24495
24505
  this.definitions = new Map(definitions.map((definition) => [sanitizeName(definition.name), definition]));
24496
24506
  if (this.definitions.size !== definitions.length) {
24497
24507
  const counts = /* @__PURE__ */ new Map();
@@ -24506,9 +24516,22 @@ var McpServerManager = class {
24506
24516
  );
24507
24517
  }
24508
24518
  }
24509
- this.cachePath = cachePath;
24519
+ this.cacheDir = cacheDir;
24510
24520
  this.log = log;
24511
24521
  }
24522
+ now;
24523
+ runtimes = /* @__PURE__ */ new Map();
24524
+ definitions;
24525
+ // Cache directory holding one <server>.json shard per connected server. A
24526
+ // legacy pre-sharding single file (<configDir>/mcp-metadata.json) is
24527
+ // deliberately ignored — the cache is disposable and shards rebuild on the
24528
+ // next connect.
24529
+ cacheDir;
24530
+ log;
24531
+ /** Set by dispose(); after it, no new connects and no late attach. */
24532
+ disposed = false;
24533
+ /** Prefixed names claimed by more than one server in the last loadCatalog(); callTool fail-closes on them. */
24534
+ conflictedNames = /* @__PURE__ */ new Set();
24512
24535
  getDefinition(server) {
24513
24536
  return this.definitions.get(sanitizeName(server));
24514
24537
  }
@@ -24549,6 +24572,7 @@ var McpServerManager = class {
24549
24572
  }
24550
24573
  /** Connect lazily. Concurrent callers share one in-flight connect. */
24551
24574
  async connect(server, signal) {
24575
+ if (this.disposed) throw new Error("MCP server manager is disposed");
24552
24576
  const key = sanitizeName(server);
24553
24577
  const definition = this.definitions.get(key);
24554
24578
  if (!definition) throw new Error(`unknown MCP server "${server}"`);
@@ -24561,12 +24585,17 @@ var McpServerManager = class {
24561
24585
  runtime.connecting = (async () => {
24562
24586
  this.log(`mcp: connecting to "${key}" (${definition.command ?? definition.url})`);
24563
24587
  const { client, transport } = this.startTransport(definition, runtime);
24588
+ runtime.transport = transport;
24564
24589
  client.onclose = () => {
24565
24590
  this.log(`mcp: server "${key}" connection closed`);
24566
24591
  runtime.client = void 0;
24567
24592
  };
24568
24593
  try {
24569
24594
  await withTimeout(client.connect(transport), connectTimeoutFor(definition), `connect "${key}"`, signal);
24595
+ if (this.disposed) {
24596
+ await transport.close().catch(() => void 0);
24597
+ throw new Error(`MCP manager disposed while connecting to "${key}"`);
24598
+ }
24570
24599
  runtime.client = client;
24571
24600
  await this.refreshTools(runtime, connectTimeoutFor(definition));
24572
24601
  return runtime;
@@ -24576,6 +24605,7 @@ var McpServerManager = class {
24576
24605
  }
24577
24606
  })().finally(() => {
24578
24607
  runtime.connecting = void 0;
24608
+ runtime.transport = void 0;
24579
24609
  });
24580
24610
  try {
24581
24611
  return await runtime.connecting;
@@ -24603,13 +24633,11 @@ var McpServerManager = class {
24603
24633
  }
24604
24634
  seen.add(tool.prefixedName);
24605
24635
  }
24606
- for (const [otherKey, otherRuntime] of this.runtimes) {
24636
+ for (const otherRuntime of this.runtimes.values()) {
24607
24637
  if (otherRuntime === runtime || !otherRuntime.tools) continue;
24608
24638
  for (const otherTool of otherRuntime.tools) {
24609
24639
  if (seen.has(otherTool.prefixedName)) {
24610
- throw new Error(
24611
- `tool name collision between "${runtime.definition.name}" and "${otherKey}": ${otherTool.prefixedName}`
24612
- );
24640
+ this.log(`mcp: tool name "${otherTool.prefixedName}" is claimed by more than one server \u2014 disabled until the servers are renamed (fail-closed)`);
24613
24641
  }
24614
24642
  }
24615
24643
  }
@@ -24618,16 +24646,45 @@ var McpServerManager = class {
24618
24646
  }
24619
24647
  persistMetadata() {
24620
24648
  try {
24621
- const snapshot = { ...this.readCacheFile() };
24649
+ fs2.mkdirSync(this.cacheDir, { recursive: true });
24622
24650
  for (const [key, runtime] of this.runtimes) {
24623
- if (runtime.tools) snapshot[key] = runtime.tools;
24651
+ if (!runtime.tools) continue;
24652
+ const shardPath = path2.join(this.cacheDir, `${key}.json`);
24653
+ const tempPath = `${shardPath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
24654
+ fs2.writeFileSync(tempPath, JSON.stringify({ updatedAt: this.now(), tools: runtime.tools }, null, 2), "utf8");
24655
+ try {
24656
+ fs2.renameSync(tempPath, shardPath);
24657
+ } catch (error2) {
24658
+ fs2.unlinkSync(tempPath);
24659
+ throw error2;
24660
+ }
24624
24661
  }
24625
- fs2.mkdirSync(path2.dirname(this.cachePath), { recursive: true });
24626
- fs2.writeFileSync(this.cachePath, JSON.stringify({ updatedAt: Date.now(), tools: snapshot }, null, 2), "utf8");
24662
+ this.deleteExpiredShards();
24627
24663
  } catch (error2) {
24628
24664
  this.log(`mcp: metadata cache write failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
24629
24665
  }
24630
24666
  }
24667
+ /** Delete shard files older than METADATA_TTL_MS (stale entries of offline servers age out). */
24668
+ deleteExpiredShards() {
24669
+ let names;
24670
+ try {
24671
+ names = fs2.readdirSync(this.cacheDir);
24672
+ } catch {
24673
+ return;
24674
+ }
24675
+ for (const name of names) {
24676
+ if (!name.endsWith(".json")) continue;
24677
+ const filePath = path2.join(this.cacheDir, name);
24678
+ try {
24679
+ const mtimeMs = fs2.statSync(filePath).mtimeMs;
24680
+ const parsed = JSON.parse(fs2.readFileSync(filePath, "utf8"));
24681
+ if (typeof parsed.updatedAt !== "number" || this.now() - parsed.updatedAt > METADATA_TTL_MS) {
24682
+ if (fs2.statSync(filePath).mtimeMs === mtimeMs) fs2.unlinkSync(filePath);
24683
+ }
24684
+ } catch {
24685
+ }
24686
+ }
24687
+ }
24631
24688
  /** Cached descriptors for one server (from the metadata cache file). */
24632
24689
  cachedToolsFor(server) {
24633
24690
  return this.loadCatalog().filter((tool) => tool.server === sanitizeName(server));
@@ -24635,47 +24692,98 @@ var McpServerManager = class {
24635
24692
  /** Metadata catalog: live runtimes plus the last persisted snapshot. */
24636
24693
  loadCatalog() {
24637
24694
  const catalog = /* @__PURE__ */ new Map();
24638
- const cached2 = this.readCacheFile();
24639
- for (const tools of Object.values(cached2)) {
24695
+ const ownerOf = /* @__PURE__ */ new Map();
24696
+ const conflicts = /* @__PURE__ */ new Set();
24697
+ const claim2 = (tool, live) => {
24698
+ const owner = ownerOf.get(tool.prefixedName);
24699
+ if (owner !== void 0 && owner !== tool.server) {
24700
+ conflicts.add(tool.prefixedName);
24701
+ return;
24702
+ }
24703
+ if (owner !== void 0 && !live) return;
24704
+ catalog.set(tool.prefixedName, tool);
24705
+ ownerOf.set(tool.prefixedName, tool.server);
24706
+ };
24707
+ for (const tools of Object.values(this.readCacheFile())) {
24640
24708
  for (const tool of tools) {
24641
24709
  if (!this.definitions.has(tool.server)) continue;
24642
- if (catalog.has(tool.prefixedName)) {
24643
- this.log(`mcp: metadata cache collision on ${tool.prefixedName} (server "${tool.server}", first seen from another server) \u2014 keeping the first entry`);
24644
- continue;
24645
- }
24646
- catalog.set(tool.prefixedName, tool);
24710
+ claim2(tool, false);
24647
24711
  }
24648
24712
  }
24649
24713
  for (const runtime of this.runtimes.values()) {
24650
- for (const tool of runtime.tools ?? []) catalog.set(tool.prefixedName, tool);
24714
+ for (const tool of runtime.tools ?? []) claim2(tool, true);
24715
+ }
24716
+ for (const name of conflicts) {
24717
+ catalog.delete(name);
24718
+ this.log(`mcp: tool name "${name}" is claimed by more than one server \u2014 disabled until the servers are renamed (fail-closed)`);
24651
24719
  }
24720
+ this.conflictedNames = conflicts;
24652
24721
  return [...catalog.values()].sort((a, b) => a.prefixedName.localeCompare(b.prefixedName));
24653
24722
  }
24654
- cacheMtime;
24723
+ cacheSignature;
24655
24724
  cacheSnapshot = {};
24656
- /** Read the metadata cache file, re-reading only when the mtime changed. */
24725
+ cacheUpdatedAt = {};
24726
+ pruneExpiredSnapshot() {
24727
+ for (const [key, updatedAt] of Object.entries(this.cacheUpdatedAt)) {
24728
+ if (this.now() - updatedAt > METADATA_TTL_MS) {
24729
+ delete this.cacheSnapshot[key];
24730
+ delete this.cacheUpdatedAt[key];
24731
+ }
24732
+ }
24733
+ return this.cacheSnapshot;
24734
+ }
24735
+ /**
24736
+ * Read the sharded metadata cache (<cacheDir>/<server>.json), merging every
24737
+ * shard's tools by server key (the file name is the sanitized server key).
24738
+ * Re-reads only when the signature changed; the signature must cover each
24739
+ * shard's name + mtime — the directory's own mtime is NOT enough, because a
24740
+ * rename replacing an existing shard leaves it unchanged.
24741
+ */
24657
24742
  readCacheFile() {
24658
- let mtime;
24743
+ let names;
24659
24744
  try {
24660
- mtime = fs2.statSync(this.cachePath).mtimeMs;
24745
+ names = fs2.readdirSync(this.cacheDir).sort();
24661
24746
  } catch {
24662
- this.cacheMtime = void 0;
24747
+ this.cacheSignature = void 0;
24663
24748
  this.cacheSnapshot = {};
24749
+ this.cacheUpdatedAt = {};
24664
24750
  return this.cacheSnapshot;
24665
24751
  }
24666
- if (mtime === this.cacheMtime) return this.cacheSnapshot;
24667
- try {
24668
- const parsed = JSON.parse(fs2.readFileSync(this.cachePath, "utf8"));
24669
- this.cacheSnapshot = parsed.tools ?? {};
24670
- this.cacheMtime = mtime;
24671
- } catch {
24672
- this.cacheSnapshot = {};
24673
- this.cacheMtime = mtime;
24752
+ let signature = "";
24753
+ for (const name of names) {
24754
+ try {
24755
+ signature += `${name}:${fs2.statSync(path2.join(this.cacheDir, name)).mtimeMs};`;
24756
+ } catch {
24757
+ }
24758
+ }
24759
+ if (signature === this.cacheSignature) return this.pruneExpiredSnapshot();
24760
+ const snapshot = {};
24761
+ const updatedAt = {};
24762
+ for (const name of names) {
24763
+ if (!name.endsWith(".json")) continue;
24764
+ try {
24765
+ const parsed = JSON.parse(fs2.readFileSync(path2.join(this.cacheDir, name), "utf8"));
24766
+ if (typeof parsed.updatedAt !== "number" || this.now() - parsed.updatedAt > METADATA_TTL_MS) continue;
24767
+ if (!Array.isArray(parsed.tools)) continue;
24768
+ const key = name.slice(0, -".json".length);
24769
+ snapshot[key] = parsed.tools;
24770
+ updatedAt[key] = parsed.updatedAt;
24771
+ } catch {
24772
+ }
24674
24773
  }
24774
+ this.cacheSnapshot = snapshot;
24775
+ this.cacheUpdatedAt = updatedAt;
24776
+ this.cacheSignature = signature;
24675
24777
  return this.cacheSnapshot;
24676
24778
  }
24677
24779
  async callTool(prefixedName, args, signal) {
24678
24780
  const catalog = this.loadCatalog();
24781
+ if (this.conflictedNames.has(prefixedName)) {
24782
+ return {
24783
+ delivery: "not_sent",
24784
+ error: `MCP tool name "${prefixedName}" is claimed by more than one configured server. Rename one of the servers so every tool name is unambiguous; the call was NOT sent.`
24785
+ };
24786
+ }
24679
24787
  const descriptor = catalog.find((tool) => tool.prefixedName === prefixedName);
24680
24788
  let server = descriptor?.server;
24681
24789
  let toolName = descriptor?.tool;
@@ -24746,12 +24854,17 @@ ${tail}` : "")
24746
24854
  }
24747
24855
  /** Close every client and reap every child process. */
24748
24856
  async dispose() {
24857
+ this.disposed = true;
24749
24858
  for (const runtime of this.runtimes.values()) {
24750
24859
  try {
24751
24860
  await runtime.client?.close();
24752
24861
  } catch {
24753
24862
  }
24863
+ if (!runtime.client && runtime.transport) {
24864
+ await runtime.transport.close().catch(() => void 0);
24865
+ }
24754
24866
  runtime.client = void 0;
24867
+ runtime.transport = void 0;
24755
24868
  }
24756
24869
  this.runtimes.clear();
24757
24870
  }
@@ -24820,10 +24933,14 @@ var DEFAULT_OUTPUT_MAX_BYTES = 8192;
24820
24933
  var SEARCH_RESULT_LIMIT = 20;
24821
24934
  function truncate(text, maxBytes) {
24822
24935
  if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
24823
- const cut = Buffer.from(text, "utf8").subarray(0, maxBytes).toString("utf8");
24824
- return `${cut}
24936
+ const notice = `
24825
24937
 
24826
24938
  \u2026[truncated: result exceeded ${maxBytes} bytes. Narrow the call arguments or read the resource directly.]`;
24939
+ const noticeBytes = Buffer.byteLength(notice, "utf8");
24940
+ const bodyBudget = Math.max(1, maxBytes - noticeBytes);
24941
+ let cut = Buffer.from(text, "utf8").subarray(0, bodyBudget).toString("utf8");
24942
+ if (cut.endsWith("\uFFFD")) cut = cut.slice(0, -1);
24943
+ return `${cut}${notice}`;
24827
24944
  }
24828
24945
  function summarizeParams(schema) {
24829
24946
  if (!schema || typeof schema !== "object") return "";
@@ -24915,7 +25032,7 @@ function createMcpProxyTool(getManager, ensureTrusted, options = {}) {
24915
25032
  return {
24916
25033
  name: "mcp",
24917
25034
  label: "MCP",
24918
- description: 'Call tools on configured MCP servers without loading their definitions. Discover: mcp({ search: "screenshot" }) lists matching <server>_<tool> names with compact parameter hints (cached metadata; servers do not start for search). Inspect: mcp({ tool: "<name>", describe: true }) returns the full input schema. Call: mcp({ tool: "<name>", args: { ... } }) \u2014 the server starts lazily on first use. args may be a JSON object or JSON string. If a result is marked [outcome unknown], the tool may have executed: do not retry automatically \u2014 verify state with a read-only call first.',
25035
+ description: 'Call tools on configured MCP servers without loading their definitions. Discover: mcp({ search: "screenshot" }) lists matching <server>_<tool> names with compact parameter hints (cached metadata; servers do not start for search). Inspect: mcp({ tool: "<name>", describe: true }) returns the full input schema. Call: mcp({ tool: "<name>", args: { ... } }) \u2014 the server starts lazily on first use. args may be a JSON object or JSON string. Tool lists are a snapshot taken when a server connects: tools added or removed by a running server only appear after /reload refreshes the session. If a result is marked [outcome unknown], the tool may have executed: do not retry automatically \u2014 verify state with a read-only call first.',
24919
25036
  parameters: Type.Object({
24920
25037
  search: Type.Optional(Type.String({ description: "Query text to discover MCP tools by name/description" })),
24921
25038
  tool: Type.Optional(Type.String({ description: "Prefixed tool name <server>_<tool> to describe or call" })),
@@ -25115,10 +25232,10 @@ function formatServerForTrustPrompt(definition) {
25115
25232
  function mcpAdapter(ctx) {
25116
25233
  const configDir = resolveConfigDir();
25117
25234
  const trustStore = new McpTrustStore(path4.join(configDir, "mcp-trust.json"));
25118
- const metadataCachePath = path4.join(configDir, "mcp-metadata.json");
25235
+ const metadataCacheDir = path4.join(configDir, "mcp-metadata");
25119
25236
  let manager = new McpServerManager(
25120
25237
  resolveMcpAdapterConfig(ctx.cwd, configDir).servers,
25121
- metadataCachePath,
25238
+ metadataCacheDir,
25122
25239
  ctx.log
25123
25240
  );
25124
25241
  async function reloadManager() {
@@ -25126,23 +25243,29 @@ function mcpAdapter(ctx) {
25126
25243
  try {
25127
25244
  manager = new McpServerManager(
25128
25245
  resolveMcpAdapterConfig(ctx.cwd, configDir).servers,
25129
- metadataCachePath,
25246
+ metadataCacheDir,
25130
25247
  ctx.log
25131
25248
  );
25132
25249
  } catch (error2) {
25133
25250
  const message = error2 instanceof Error ? error2.message : String(error2);
25134
25251
  ctx.log(`mcp: config reload failed: ${message}`);
25135
25252
  if (ctx.hasUI) ctx.ui.notify(`MCP adapter config error: ${message}`, "error");
25136
- manager = new McpServerManager([], metadataCachePath, ctx.log);
25253
+ manager = new McpServerManager([], metadataCacheDir, ctx.log);
25137
25254
  }
25138
25255
  }
25139
- function writeProjectOverride(serverName, disabled) {
25256
+ ctx.registerCleanup?.(() => manager.dispose().catch(() => void 0));
25257
+ function readProjectOverride() {
25140
25258
  const filePath = path4.join(ctx.cwd, ".pie", "mcp-adapter.json");
25141
25259
  let parsed = {};
25142
25260
  try {
25143
25261
  parsed = JSON.parse(fs4.readFileSync(filePath, "utf8"));
25144
25262
  } catch {
25145
25263
  }
25264
+ return parsed;
25265
+ }
25266
+ function writeProjectOverride(serverName, disabled) {
25267
+ const filePath = path4.join(ctx.cwd, ".pie", "mcp-adapter.json");
25268
+ const parsed = readProjectOverride();
25146
25269
  const servers = parsed.mcpServers && typeof parsed.mcpServers === "object" && !Array.isArray(parsed.mcpServers) ? { ...parsed.mcpServers } : {};
25147
25270
  if (disabled === null) {
25148
25271
  delete servers[serverName];
@@ -25184,16 +25307,6 @@ function mcpAdapter(ctx) {
25184
25307
  if (eagerRegistered.length > 0) {
25185
25308
  ctx.log(`mcp: registered ${eagerRegistered.length} direct tool(s) from metadata cache: ${eagerRegistered.join(", ")}`);
25186
25309
  }
25187
- function unpinnedWarnings() {
25188
- const warnings = [];
25189
- for (const definition of manager.listDefinitions()) {
25190
- const spec = [definition.command ?? "", ...definition.args].join(" ");
25191
- if (/(^|\s)(npx|bunx|uvx)\s/.test(spec) && /@latest\b/.test(spec)) {
25192
- warnings.push(`${definition.name}: unpinned "@latest" fetch \u2014 pin an exact version (e.g. pkg@1.2.3).`);
25193
- }
25194
- }
25195
- return warnings;
25196
- }
25197
25310
  ctx.registerCommand({
25198
25311
  name: "mcp",
25199
25312
  description: "MCP adapter: status (default), enable/disable <server>",
@@ -25209,10 +25322,21 @@ function mcpAdapter(ctx) {
25209
25322
  commandCtx.ui.notify(`Unknown MCP server "${target}". Run /mcp for the configured list.`, "warning");
25210
25323
  return;
25211
25324
  }
25212
- const filePath = writeProjectOverride(definition.name, sub === "disable" ? true : null);
25325
+ const overrides = readProjectOverride().mcpServers;
25326
+ const forceEnabled = overrides?.[definition.name]?.disabled === false;
25327
+ const filePath = writeProjectOverride(
25328
+ definition.name,
25329
+ // enable must lift a source-level disabled:true with an explicit
25330
+ // {disabled:false} force-override: deleting the override alone
25331
+ // would leave the source-level disable in force. Preserve an
25332
+ // existing force-enable on repeated enable commands too.
25333
+ sub === "disable" ? true : definition.disabled || forceEnabled ? false : null
25334
+ );
25213
25335
  await reloadManager();
25336
+ const effectiveDisabled = manager.getDefinition(definition.name)?.disabled ?? false;
25337
+ const stateLabel = effectiveDisabled ? "disabled" : sub === "disable" ? "still enabled" : "enabled";
25214
25338
  commandCtx.ui.notify(
25215
- `MCP server "${definition.name}" ${sub}d (wrote ${filePath}). ` + (eagerRegistered.length > 0 ? "Restart or /reload to resync direct tools." : "Effective immediately."),
25339
+ `MCP server "${definition.name}" ${stateLabel} (wrote ${filePath}). ` + (eagerRegistered.length > 0 ? "Restart or /reload to resync direct tools." : "Effective immediately."),
25216
25340
  "info"
25217
25341
  );
25218
25342
  return;
@@ -25234,15 +25358,15 @@ function mcpAdapter(ctx) {
25234
25358
  lines.push(`- ${sanitizeName(definition.name)} [${manager.statusOf(definition.name)}, ${scopeLabel}] ${spec}`);
25235
25359
  }
25236
25360
  }
25237
- const warnings = unpinnedWarnings();
25361
+ const warnings = unpinnedWarnings(manager.listDefinitions());
25238
25362
  if (warnings.length > 0) lines.push("", "Warnings:", ...warnings.map((warning) => `- ${warning}`));
25239
25363
  const config2 = resolveMcpAdapterConfig(commandCtx.cwd, configDir);
25240
25364
  if (config2.problems.length > 0) lines.push("", "Config problems:", ...config2.problems.map((problem) => `- ${problem}`));
25241
25365
  commandCtx.ui.notify(lines.join("\n"), "info");
25242
25366
  }
25243
25367
  });
25244
- ctx.on("session:changed", () => {
25245
- void reloadManager().catch(() => void 0);
25368
+ ctx.on("session:changed", async () => {
25369
+ await reloadManager();
25246
25370
  });
25247
25371
  }
25248
25372
  export {
@@ -7,7 +7,7 @@ import {
7
7
  isPlanModeSafeCommand,
8
8
  markCompletedPlanSteps,
9
9
  restoreExecutionState
10
- } from "../../../chunks/chunk-LU7RKALH.js";
10
+ } from "../../../chunks/chunk-UPPICLVS.js";
11
11
  import "../../../chunks/chunk-QJH6YN2W.js";
12
12
  import "../../../chunks/chunk-IM43UYVV.js";
13
13
  import {
@@ -1,14 +1,14 @@
1
1
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
2
  import {
3
3
  createCliHostCapabilities
4
- } from "../../../chunks/chunk-TJ2F4FL5.js";
5
- import "../../../chunks/chunk-5353DEEC.js";
4
+ } from "../../../chunks/chunk-P3QPJXS7.js";
6
5
  import {
7
6
  createSharedFileSystemTools,
8
7
  createSubagentCapability
9
- } from "../../../chunks/chunk-LU7RKALH.js";
8
+ } from "../../../chunks/chunk-UPPICLVS.js";
10
9
  import "../../../chunks/chunk-QJH6YN2W.js";
11
10
  import "../../../chunks/chunk-IM43UYVV.js";
11
+ import "../../../chunks/chunk-5353DEEC.js";
12
12
  import "../../../chunks/chunk-XXGYUYXI.js";
13
13
  import "../../../chunks/chunk-LZQXQOPJ.js";
14
14
 
@@ -8,7 +8,7 @@ import {
8
8
  executeManageTodoList,
9
9
  executionStateToTodos,
10
10
  restoreExecutionState
11
- } from "../../../chunks/chunk-LU7RKALH.js";
11
+ } from "../../../chunks/chunk-UPPICLVS.js";
12
12
  import "../../../chunks/chunk-QJH6YN2W.js";
13
13
  import "../../../chunks/chunk-IM43UYVV.js";
14
14
  import "../../../chunks/chunk-XXGYUYXI.js";
@@ -9999,6 +9999,16 @@ function validateRequest(value) {
9999
9999
  string("command", true, 32768);
10000
10000
  string("cwd");
10001
10001
  string("label", false, 1024);
10002
+ if (r.env !== void 0) {
10003
+ if (!r.env || typeof r.env !== "object" || Array.isArray(r.env)) throw new Error("Invalid env: expected an object of string to string.");
10004
+ const entries = Object.entries(r.env);
10005
+ if (entries.length > 512) throw new Error("Invalid env: at most 512 variables.");
10006
+ for (const [k, v] of entries) {
10007
+ if (!k.length || k.length > 256 || k.includes("\0") || typeof v !== "string" || v.length > 65536 || v.includes("\0")) {
10008
+ throw new Error(`Invalid env entry ${JSON.stringify(k)}: expected string key and value without NUL bytes.`);
10009
+ }
10010
+ }
10011
+ }
10002
10012
  if (r.argv !== void 0 && (!Array.isArray(r.argv) || r.argv.length < 1 || r.argv.length > 256 || r.argv.some((a) => typeof a !== "string" || a.includes("\0") || a.length > 32768) || !r.argv[0])) throw new Error("Invalid argv: expected a non-empty array of executable and arguments.");
10003
10013
  // Fall through to the shared PTY size limits.
10004
10014
  case "resize":
@@ -10294,6 +10304,13 @@ var SUPPORTED_KEYS = Object.keys(KEY_MAP);
10294
10304
  var import_node_fs2 = require("node:fs");
10295
10305
  var import_node_path3 = __toESM(require("node:path"));
10296
10306
  var program2 = new Command();
10307
+ function callerEnv() {
10308
+ const env = {};
10309
+ for (const [key, value] of Object.entries(process.env)) {
10310
+ if (typeof value === "string") env[key] = value;
10311
+ }
10312
+ return env;
10313
+ }
10297
10314
  program2.name("tui-use").description("TUI automation for AI agents \u2014 like BrowserUse, but for the terminal").option("--session <id>", "Target this session explicitly (safe for concurrent tasks)").version(version);
10298
10315
  program2.command("start [args...]").description("Start an interactive program in a PTY session").option("--cwd <dir>", "Working directory for the command").option("--label <name>", "Human-readable label for this session").option("--cols <n>", "Terminal width (default: 120)", "120").option("--rows <n>", "Terminal height (default: 30)", "30").option("--argv-stdin", "Read a JSON array of executable and arguments from UTF-8 stdin; bypass the shell").action(async (args, opts) => {
10299
10316
  let argv;
@@ -10311,6 +10328,7 @@ program2.command("start [args...]").description("Start an interactive program in
10311
10328
  type: "start",
10312
10329
  command,
10313
10330
  cwd: import_node_path3.default.resolve(opts.cwd ?? process.cwd()),
10331
+ env: callerEnv(),
10314
10332
  label: opts.label,
10315
10333
  cols: Number(opts.cols),
10316
10334
  rows: Number(opts.rows),
@@ -7267,7 +7267,10 @@ var Session = class {
7267
7267
  rows,
7268
7268
  useConptyDll: process.platform === "win32",
7269
7269
  cwd: options.cwd ?? process.cwd(),
7270
- env: process.env
7270
+ // The caller's env, not the daemon starter's: a persistent daemon
7271
+ // outlives many projects, and each session must see the environment
7272
+ // of the shell that started it (PATH setups, PIE_* overrides, ...).
7273
+ env: options.env ?? process.env
7271
7274
  });
7272
7275
  } catch (error) {
7273
7276
  this.terminal.dispose();
@@ -7606,6 +7609,16 @@ function validateRequest(value) {
7606
7609
  string("command", true, 32768);
7607
7610
  string("cwd");
7608
7611
  string("label", false, 1024);
7612
+ if (r.env !== void 0) {
7613
+ if (!r.env || typeof r.env !== "object" || Array.isArray(r.env)) throw new Error("Invalid env: expected an object of string to string.");
7614
+ const entries = Object.entries(r.env);
7615
+ if (entries.length > 512) throw new Error("Invalid env: at most 512 variables.");
7616
+ for (const [k, v] of entries) {
7617
+ if (!k.length || k.length > 256 || k.includes("\0") || typeof v !== "string" || v.length > 65536 || v.includes("\0")) {
7618
+ throw new Error(`Invalid env entry ${JSON.stringify(k)}: expected string key and value without NUL bytes.`);
7619
+ }
7620
+ }
7621
+ }
7609
7622
  if (r.argv !== void 0 && (!Array.isArray(r.argv) || r.argv.length < 1 || r.argv.length > 256 || r.argv.some((a) => typeof a !== "string" || a.includes("\0") || a.length > 32768) || !r.argv[0])) throw new Error("Invalid argv: expected a non-empty array of executable and arguments.");
7610
7623
  // Fall through to the shared PTY size limits.
7611
7624
  case "resize":
@@ -1,8 +1,4 @@
1
1
  import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
2
- import {
3
- getBuiltinDir,
4
- getConfigDir
5
- } from "./chunk-5353DEEC.js";
6
2
  import {
7
3
  classifyShellCommand,
8
4
  createCodeIntelTool,
@@ -12,7 +8,11 @@ import {
12
8
  createSharedWebSearchTool,
13
9
  interpretShellExit,
14
10
  requestInteraction
15
- } from "./chunk-LU7RKALH.js";
11
+ } from "./chunk-UPPICLVS.js";
12
+ import {
13
+ getBuiltinDir,
14
+ getConfigDir
15
+ } from "./chunk-5353DEEC.js";
16
16
  import {
17
17
  Type
18
18
  } from "./chunk-XXGYUYXI.js";
@@ -4856,7 +4856,7 @@ var AgentSessionRetryController = class {
4856
4856
  }
4857
4857
  return;
4858
4858
  }
4859
- const maxRetries = this.retry.maxRetries ?? 3;
4859
+ const maxRetries = this.retry.maxRetriesForFailure?.(failure) ?? this.retry.maxRetries ?? 3;
4860
4860
  this.retryAttempt += 1;
4861
4861
  if (this.retryAttempt > maxRetries) {
4862
4862
  const terminalFailure = asTerminalFailure(failure);
@@ -4869,7 +4869,8 @@ var AgentSessionRetryController = class {
4869
4869
  return;
4870
4870
  }
4871
4871
  const attempt = this.retryAttempt;
4872
- const delayMs = (this.retry.baseDelayMs ?? 2e3) * 2 ** (attempt - 1);
4872
+ const rawDelayMs = (this.retry.baseDelayMs ?? 2e3) * 2 ** (attempt - 1);
4873
+ const delayMs = this.retry.maxDelayMs !== void 0 ? Math.min(rawDelayMs, this.retry.maxDelayMs) : rawDelayMs;
4873
4874
  this.ensurePending();
4874
4875
  this.setRecoveryState("retry_pending");
4875
4876
  this.emit({ type: "turn_recovery_pending", recovery: "retry", failure, attempt, maxRetries, delayMs });
@@ -4917,7 +4918,7 @@ var AgentSessionRetryController = class {
4917
4918
  userAbort: false,
4918
4919
  terminal: true
4919
4920
  });
4920
- const maxRetries = this.retry?.maxRetries ?? 3;
4921
+ const maxRetries = (failure !== void 0 ? this.retry?.maxRetriesForFailure?.(failure) : void 0) ?? this.retry?.maxRetries ?? 3;
4921
4922
  this.emit({ type: "retry_exhausted", errorMessage: terminalFailure.message, maxRetries });
4922
4923
  this.emit({ type: "turn_failed_terminal", failure: terminalFailure });
4923
4924
  this.emit({ type: "turn_settled" });
package/dist/cli.js CHANGED
@@ -7,25 +7,7 @@ import {
7
7
  createCliWebResearchHealthStore,
8
8
  formatSize,
9
9
  truncateHeadTail
10
- } from "./chunks/chunk-TJ2F4FL5.js";
11
- import {
12
- VERSION,
13
- getAgentDir,
14
- getBuiltinDir,
15
- getConfigDir,
16
- getCustomThemesDir,
17
- getLogsDir,
18
- getPackageDir,
19
- getProjectSettingsPath,
20
- getRuntimeLogPath,
21
- getSessionsDir,
22
- getSettingsPath,
23
- getThemesDir,
24
- migrateConfigFromAgentDir
25
- } from "./chunks/chunk-5353DEEC.js";
26
- import {
27
- resolveCliProjectRoot
28
- } from "./chunks/chunk-WLFOBECN.js";
10
+ } from "./chunks/chunk-P3QPJXS7.js";
29
11
  import {
30
12
  AGENTS_CONTEXT_FILE_NAME,
31
13
  AgentSessionController,
@@ -78,7 +60,7 @@ import {
78
60
  setExtensionEnabledInSettings,
79
61
  shouldPreserveExecutionStateForUserText,
80
62
  supersedeExecutionState
81
- } from "./chunks/chunk-LU7RKALH.js";
63
+ } from "./chunks/chunk-UPPICLVS.js";
82
64
  import "./chunks/chunk-QJH6YN2W.js";
83
65
  import {
84
66
  Deref,
@@ -104,6 +86,24 @@ import {
104
86
  sortToolModelCandidatesByCapability,
105
87
  type_exports
106
88
  } from "./chunks/chunk-IM43UYVV.js";
89
+ import {
90
+ VERSION,
91
+ getAgentDir,
92
+ getBuiltinDir,
93
+ getConfigDir,
94
+ getCustomThemesDir,
95
+ getLogsDir,
96
+ getPackageDir,
97
+ getProjectSettingsPath,
98
+ getRuntimeLogPath,
99
+ getSessionsDir,
100
+ getSettingsPath,
101
+ getThemesDir,
102
+ migrateConfigFromAgentDir
103
+ } from "./chunks/chunk-5353DEEC.js";
104
+ import {
105
+ resolveCliProjectRoot
106
+ } from "./chunks/chunk-WLFOBECN.js";
107
107
  import {
108
108
  IsArray,
109
109
  IsBigInt,
@@ -77119,7 +77119,7 @@ function stripStaleUsageBeforeCompaction(messages, latestCompactionTimestamp) {
77119
77119
  }
77120
77120
 
77121
77121
  // src/runtime/retry-policy.ts
77122
- var TRANSIENT_NETWORK_ERROR_PATTERN = /sse stream aborted|sse stream closed before end|sse stream ended before response completed|(?:read\s+)?ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|socket hang up|request timeout|client network socket disconnected before secure TLS connection was established|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|terminated/i;
77122
+ var TRANSIENT_NETWORK_ERROR_PATTERN = /sse stream aborted|sse stream closed before end|sse stream ended before response completed|(?:read\s+)?ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|(?:read\s+|write\s+)?EPIPE|socket hang up|request timeout|client network socket disconnected before secure TLS connection was established|certificate (?:has )?expired|unable to verify (?:the first certificate|certificate)|self[- ]signed cert|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|terminated/i;
77123
77123
  var PROVIDER_TRANSIENT_ERROR_PATTERN = /overloaded|rate.?limit|too many requests|\b429\b|\b500\b|\b502\b|\b503\b|\b504\b|service.?unavailable|server error|internal error|retry delay/i;
77124
77124
  var USER_ABORT_PATTERN = /request was aborted|local abort signal|user abort|user_interrupt|interrupted by user|esc pressed|ctrl\+c/i;
77125
77125
  var AUTH_CONFIG_PATTERN = /missing api key|invalid api key|api key.*invalid|unauthorized|\b401\b|\b403\b|forbidden|authentication failed|no configured model|no models? are configured/i;
@@ -77127,6 +77127,8 @@ var INVALID_FILE_REFERENCE_PATTERN = /not the owner of this file|owner of this f
77127
77127
  var INVALID_REQUEST_PATTERN = /bad request|\b400\b|invalid request|invalid schema|schema validation|invalid parameter|unsupported parameter|malformed/i;
77128
77128
  var CLI_RETRY_MAX_RETRIES = 3;
77129
77129
  var CLI_RETRY_BASE_DELAY_MS = 2e3;
77130
+ var CLI_TRANSIENT_NETWORK_MAX_RETRIES = 10;
77131
+ var CLI_RETRY_MAX_DELAY_MS = 6e4;
77130
77132
  function isCliContextOverflowError(errorMessage4) {
77131
77133
  return /context length exceeded|context_length_exceeded|maximum context length|maximum context size|too many tokens|input.*too long|prompt.*too long|context window|token limit exceeded|exceeds.{0,20}context/i.test(
77132
77134
  errorMessage4
@@ -77203,6 +77205,31 @@ function getCliRetryBaseDelayMs() {
77203
77205
  const parsed = Number(override);
77204
77206
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : CLI_RETRY_BASE_DELAY_MS;
77205
77207
  }
77208
+ function getCliTransientNetworkMaxRetries() {
77209
+ const override = process.env.PIE_RETRY_TRANSIENT_NETWORK_MAX_RETRIES;
77210
+ if (!override) {
77211
+ return CLI_TRANSIENT_NETWORK_MAX_RETRIES;
77212
+ }
77213
+ const parsed = Number(override);
77214
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : CLI_TRANSIENT_NETWORK_MAX_RETRIES;
77215
+ }
77216
+ function getCliRetryMaxDelayMs() {
77217
+ const override = process.env.PIE_RETRY_MAX_DELAY_MS;
77218
+ if (!override) {
77219
+ return CLI_RETRY_MAX_DELAY_MS;
77220
+ }
77221
+ const parsed = Number(override);
77222
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : CLI_RETRY_MAX_DELAY_MS;
77223
+ }
77224
+ function createCliRetryPolicy(isRetryableErrorOverride) {
77225
+ return {
77226
+ isRetryableError: isRetryableErrorOverride ?? isCliRetryableError,
77227
+ maxRetries: CLI_RETRY_MAX_RETRIES,
77228
+ baseDelayMs: getCliRetryBaseDelayMs(),
77229
+ maxDelayMs: getCliRetryMaxDelayMs(),
77230
+ maxRetriesForFailure: (failure) => failure.kind === "transient_network" ? getCliTransientNetworkMaxRetries() : CLI_RETRY_MAX_RETRIES
77231
+ };
77232
+ }
77206
77233
 
77207
77234
  // src/runtime/interactive-context-ui.ts
77208
77235
  function calculateContextUsage(host) {
@@ -77921,11 +77948,9 @@ function createInteractiveAgentSessionController(host) {
77921
77948
  },
77922
77949
  getAutoContinueMessage: () => host.getTodoAutoContinueMessage(),
77923
77950
  classifyFailure: (context) => classifyCliFailure(context),
77924
- retry: {
77925
- isRetryableError: (errorMessage4) => !host._isContextOverflowError(errorMessage4) && isCliRetryableError(errorMessage4),
77926
- maxRetries: CLI_RETRY_MAX_RETRIES,
77927
- baseDelayMs: getCliRetryBaseDelayMs()
77928
- },
77951
+ retry: createCliRetryPolicy(
77952
+ (errorMessage4) => !host._isContextOverflowError(errorMessage4) && isCliRetryableError(errorMessage4)
77953
+ ),
77929
77954
  compaction: {
77930
77955
  compact: async (context) => {
77931
77956
  const beforeCompact = await host.extensionRunner?.emitSessionBeforeCompact({
@@ -79597,6 +79622,7 @@ var InteractiveMode = class {
79597
79622
  await this.loadExtensions();
79598
79623
  this.applyRuntimeToolMode();
79599
79624
  this.noteRuntimeModeChanged("extension_reload");
79625
+ await this.syncSessionScopedState();
79600
79626
  this.ui.requestRender();
79601
79627
  }
79602
79628
  calculateContextUsage() {
@@ -80152,6 +80178,24 @@ async function composeCliRuntime(params) {
80152
80178
  };
80153
80179
  }
80154
80180
 
80181
+ // src/app/print-mode-retry-status.ts
80182
+ function createPrintModeRetryStatusReporter() {
80183
+ let exhaustedRetryBudget;
80184
+ return (event) => {
80185
+ if (event.type === "turn_recovery_pending" && event.recovery === "retry") {
80186
+ console.error(`Transient request error: ${event.failure.message}. Retrying in ${(event.delayMs ?? 0) / 1e3}s (${event.attempt}/${event.maxRetries}).`);
80187
+ } else if (event.type === "turn_retry_started") {
80188
+ console.error(`Retrying request (${event.attempt}/${event.maxRetries})...`);
80189
+ } else if (event.type === "turn_retry_succeeded") {
80190
+ console.error(`Retry succeeded after ${event.attempts} attempt${event.attempts === 1 ? "" : "s"}.`);
80191
+ } else if (event.type === "retry_exhausted") {
80192
+ exhaustedRetryBudget = event.maxRetries;
80193
+ } else if (event.type === "turn_failed_terminal" && (event.failure.kind === "transient_network" || event.failure.kind === "provider_transient")) {
80194
+ console.error(`Request failed after ${exhaustedRetryBudget ?? CLI_RETRY_MAX_RETRIES} retries: ${event.failure.message}`);
80195
+ }
80196
+ };
80197
+ }
80198
+
80155
80199
  // src/app/print-mode.ts
80156
80200
  function createNoOpExtensionUIContext() {
80157
80201
  return {
@@ -80329,6 +80373,7 @@ async function runPrintMode(params) {
80329
80373
  streamFn: params.streamFn
80330
80374
  });
80331
80375
  const sessionTrace = new SessionTraceWriter(runtime.paths.sessionsDir, runtime.sessionManager);
80376
+ const reportRetryStatus = createPrintModeRetryStatusReporter();
80332
80377
  const agentSessionController = new AgentSessionController({
80333
80378
  agent,
80334
80379
  sessionManager: runtime.sessionManager,
@@ -80349,11 +80394,7 @@ async function runPrintMode(params) {
80349
80394
  messages: result.messages?.length ? [...context.messages, ...result.messages] : context.messages
80350
80395
  };
80351
80396
  },
80352
- retry: {
80353
- isRetryableError: isCliRetryableError,
80354
- maxRetries: CLI_RETRY_MAX_RETRIES,
80355
- baseDelayMs: getCliRetryBaseDelayMs()
80356
- },
80397
+ retry: createCliRetryPolicy(),
80357
80398
  getAutoCompactDecision: ({ message, messages }) => createCliAutoCompactDecision({
80358
80399
  message,
80359
80400
  messages,
@@ -80388,20 +80429,14 @@ async function runPrintMode(params) {
80388
80429
  classifyFailure: (context) => classifyCliFailure(context),
80389
80430
  onEvent: (event) => {
80390
80431
  sessionTrace.noteRuntimeEvent(event.type, event);
80391
- if (event.type === "turn_recovery_pending" && event.recovery === "retry") {
80392
- console.error(`Transient request error: ${event.failure.message}. Retrying in ${(event.delayMs ?? 0) / 1e3}s (${event.attempt}/${event.maxRetries}).`);
80393
- } else if (event.type === "turn_recovery_pending" && event.recovery === "compaction") {
80432
+ if (event.type === "turn_recovery_pending" && event.recovery === "compaction") {
80394
80433
  console.error(event.failure.kind === "context_overflow" ? "Context overflow detected, compacting..." : "Auto-compacting context before retry...");
80395
80434
  } else if (event.type === "auto_compaction_start") {
80396
80435
  console.error(event.reason === "overflow" ? "Context overflow detected, compacting..." : "Auto-compacting context...");
80397
80436
  } else if (event.type === "auto_compaction_end" && !event.errorMessage) {
80398
80437
  runtime.fileObservationState.clear();
80399
- } else if (event.type === "turn_retry_started") {
80400
- console.error(`Retrying request (${event.attempt}/${event.maxRetries})...`);
80401
- } else if (event.type === "turn_retry_succeeded") {
80402
- console.error(`Retry succeeded after ${event.attempts} attempt${event.attempts === 1 ? "" : "s"}.`);
80403
- } else if (event.type === "turn_failed_terminal" && (event.failure.kind === "transient_network" || event.failure.kind === "provider_transient")) {
80404
- console.error(`Request failed after ${CLI_RETRY_MAX_RETRIES} retries: ${event.failure.message}`);
80438
+ } else {
80439
+ reportRetryStatus(event);
80405
80440
  }
80406
80441
  }
80407
80442
  });
@@ -81518,11 +81553,7 @@ async function runJsonEventsMode(params) {
81518
81553
  messages: result?.messages?.length ? [...context.messages, ...result.messages] : context.messages
81519
81554
  };
81520
81555
  },
81521
- retry: {
81522
- isRetryableError: isCliRetryableError,
81523
- maxRetries: CLI_RETRY_MAX_RETRIES,
81524
- baseDelayMs: getCliRetryBaseDelayMs()
81525
- },
81556
+ retry: createCliRetryPolicy(),
81526
81557
  getAutoCompactDecision: ({ message, messages }) => createCliAutoCompactDecision({
81527
81558
  message,
81528
81559
  messages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/pie",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "description": "Pie AI Agent CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "pretest:offline": "npm run build --workspace @pie/agent-framework && npm run build --workspace @pie/shared-headless-capabilities && npm run build && npm run test:skill-regressions",
20
20
  "test:skill-regressions": "node --test --import tsx test/bash-lifecycle.test.ts test/skill-creator-runner.test.ts test/tui-use-contract.test.ts test/tui-use-runtime.test.ts test/browser-runtime.test.ts test/computer-evidence.test.ts test/interactive-lifecycle.test.ts",
21
21
  "posttest:offline": "node --test --import tsx test/remote-model-catalog.test.ts",
22
- "test:offline": "node --test --test-concurrency=4 --import tsx test/builtin-extensions.test.ts test/core-services.test.ts test/cli.test.ts test/cli-auto-compaction.test.ts test/execution-state-manager.test.ts test/queued-steer-execution-state.test.ts test/architecture-boundaries.test.ts test/attachment-resolver.test.ts test/kimi-attachment-provider.test.ts test/document-attachment-extension.test.ts test/auto-compact-overflow.test.ts test/hierarchical-command-system.test.ts test/mcp-adapter.test.ts test/extensions-enablement.test.ts test/extension-selector-shortcuts.test.ts test/skill-tool-enablement.test.ts test/message-queue-unit.test.ts test/evals-harness.test.ts test/session-trace.test.ts test/runtime-mode-context.test.ts test/tool-policy.test.ts test/retry-policy.test.ts test/yolo-command.test.ts test/footer-lifecycle.test.ts test/files-utils.test.ts test/source-size-guard.test.ts test/runtime-logger.test.ts test/release-readiness.test.ts test/package-artifacts.test.ts test/code-intel-typescript-provider.test.ts test/init-extension-contract.test.ts test/skills-runtime.test.ts test/skills-agents-discovery.test.ts test/skills-symlink.test.ts test/model-overrides.test.ts test/auth-storage.test.ts test/extension-register-provider.test.ts test/extension-reload.test.ts test/extension-terminal-input.test.ts test/extension-session-before-events.test.ts test/extension-widgets.test.ts test/extension-render-hint.test.ts test/extension-oauth-restore.test.ts test/cross-host-parity.test.ts test/tool-name-rendering.test.ts test/session-selector.test.ts test/terminal-title.test.ts test/interactive-controller-title.test.ts test/todo-title.terminal.test.ts test/plan-mode-extension.test.ts test/rendered-page-reader.test.ts test/tools-registry.test.ts test/system-prompt-contract.test.ts test/bash-yolo.test.ts test/direct-bash.test.ts test/terminal-bell.test.ts test/ctrl-d-exit.test.ts test/session-display-title.test.ts test/calm-mode.test.ts test/project-root.test.ts test/guard-advisory-context.test.ts test/clear-command-usage.test.ts test/daily-agent-smoke.terminal.test.ts test/slash-menu-toggles.terminal.test.ts test/cli-args.test.ts test/extensions-configured-paths.test.ts test/extensions-loader-path.test.ts test/file-picker.test.ts test/interrupt-recovery.test.ts test/mention-insert-regression.test.ts test/mention-picker-navigation.test.ts test/pie-unity-rpc-skill.test.ts test/skill-creator-scripts.test.ts",
22
+ "test:offline": "node --test --test-concurrency=4 --import tsx test/builtin-extensions.test.ts test/core-services.test.ts test/cli.test.ts test/cli-auto-compaction.test.ts test/execution-state-manager.test.ts test/queued-steer-execution-state.test.ts test/architecture-boundaries.test.ts test/attachment-resolver.test.ts test/kimi-attachment-provider.test.ts test/document-attachment-extension.test.ts test/auto-compact-overflow.test.ts test/hierarchical-command-system.test.ts test/mcp-adapter.test.ts test/herdr-agent-state.test.ts test/extensions-enablement.test.ts test/extension-selector-shortcuts.test.ts test/skill-tool-enablement.test.ts test/message-queue-unit.test.ts test/evals-harness.test.ts test/session-trace.test.ts test/runtime-mode-context.test.ts test/tool-policy.test.ts test/retry-policy.test.ts test/yolo-command.test.ts test/footer-lifecycle.test.ts test/files-utils.test.ts test/source-size-guard.test.ts test/runtime-logger.test.ts test/release-readiness.test.ts test/package-artifacts.test.ts test/code-intel-typescript-provider.test.ts test/init-extension-contract.test.ts test/skills-runtime.test.ts test/skills-agents-discovery.test.ts test/skills-symlink.test.ts test/model-overrides.test.ts test/auth-storage.test.ts test/extension-register-provider.test.ts test/extension-reload.test.ts test/extension-terminal-input.test.ts test/extension-session-before-events.test.ts test/extension-widgets.test.ts test/extension-render-hint.test.ts test/extension-oauth-restore.test.ts test/cross-host-parity.test.ts test/tool-name-rendering.test.ts test/session-selector.test.ts test/terminal-title.test.ts test/interactive-controller-title.test.ts test/todo-title.terminal.test.ts test/plan-mode-extension.test.ts test/rendered-page-reader.test.ts test/tools-registry.test.ts test/system-prompt-contract.test.ts test/bash-yolo.test.ts test/direct-bash.test.ts test/terminal-bell.test.ts test/ctrl-d-exit.test.ts test/session-display-title.test.ts test/calm-mode.test.ts test/project-root.test.ts test/guard-advisory-context.test.ts test/clear-command-usage.test.ts test/daily-agent-smoke.terminal.test.ts test/slash-menu-toggles.terminal.test.ts test/cli-args.test.ts test/extensions-configured-paths.test.ts test/extensions-loader-path.test.ts test/file-picker.test.ts test/interrupt-recovery.test.ts test/mention-insert-regression.test.ts test/mention-picker-navigation.test.ts test/pie-unity-rpc-skill.test.ts test/skill-creator-scripts.test.ts",
23
23
  "test:all": "npm run test:offline && npm run test:terminal:vitest",
24
24
  "test:terminal:vitest": "vitest run test/mention-file-select.terminal.test.ts test/tool-partial-display.terminal.test.ts test/footer-plan-mode.test.ts test/viewport-position-final.test.ts test/resume-header.terminal.test.ts --no-file-parallelism",
25
25
  "test:todo-tui": "node ../../scripts/run-with-env.mjs PIE_REAL_TODO_TUI=1 -- --test --import tsx test/todo-closure.terminal.test.ts",