@spinalfluid/ultrac 0.1.9 → 0.1.14

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 (3) hide show
  1. package/README.md +29 -7
  2. package/dist/cli.js +2124 -703
  3. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -286,7 +286,7 @@ var agentModePresets = {
286
286
  "Prefer narrow, reversible changes.",
287
287
  "Keep final answers concise and include verification status."
288
288
  ],
289
- tools: [...coreTools, "list_sessions", "read_session", "search_sessions"],
289
+ tools: [...coreTools, "create_skill", "list_sessions", "read_session", "search_sessions"],
290
290
  externalTools: true
291
291
  },
292
292
  plan: {
@@ -331,6 +331,7 @@ var agentModePresets = {
331
331
  ],
332
332
  tools: [
333
333
  ...coreTools,
334
+ "create_skill",
334
335
  "ast_search",
335
336
  "repo_handbook",
336
337
  "verification_plan",
@@ -398,6 +399,7 @@ function modePermissionAccessError(mode, toolName, permission) {
398
399
  var EchoProvider = class {
399
400
  name = "echo";
400
401
  async complete(request) {
402
+ if (request.messages.some((message) => message.image)) throw Object.assign(new Error("Echo provider cannot inspect image attachments."), { status: 400 });
401
403
  const lastUserMessage = [...request.messages].reverse().find((message) => message.role === "user");
402
404
  return {
403
405
  text: JSON.stringify(
@@ -1135,6 +1137,9 @@ function pinCurrentTurn(units, isUser) {
1135
1137
  }
1136
1138
  }
1137
1139
  function messageChars(message) {
1140
+ if (message && typeof message === "object" && "image" in message && message.image) {
1141
+ return JSON.stringify({ ...message, image: void 0 }).length + 4096 * CHARS_PER_TOKEN;
1142
+ }
1138
1143
  return JSON.stringify(message)?.length ?? 0;
1139
1144
  }
1140
1145
  var noticeSink = (message) => {
@@ -1142,8 +1147,8 @@ var noticeSink = (message) => {
1142
1147
  `);
1143
1148
  };
1144
1149
  var noticed = false;
1145
- function setContextBudgetNotice(sink) {
1146
- noticeSink = sink;
1150
+ function setContextBudgetNotice(sink2) {
1151
+ noticeSink = sink2;
1147
1152
  }
1148
1153
  function resetContextBudgetNotices() {
1149
1154
  noticed = false;
@@ -1222,20 +1227,20 @@ function compactRun(input, options = {}) {
1222
1227
  }
1223
1228
  async function compactShape(shape, options) {
1224
1229
  const reserveChars = options.summarise ? Math.max(0, options.reserveChars ?? shape.reserveChars ?? 0) : 0;
1225
- const plain = planKeepingSummaries(shape, shape.messages, reserveChars);
1230
+ const plain2 = planKeepingSummaries(shape, shape.messages, reserveChars);
1226
1231
  const skip = (reason) => ({
1227
- ...reserveChars > 0 ? planKeepingSummaries(shape, shape.messages, shape.reserveChars ?? 0) : plain,
1232
+ ...reserveChars > 0 ? planKeepingSummaries(shape, shape.messages, shape.reserveChars ?? 0) : plain2,
1228
1233
  compacted: false,
1229
1234
  summarisedMessages: 0,
1230
1235
  summarisedTurns: 0,
1231
1236
  skipped: reason
1232
1237
  });
1233
- if (plain.dropped.length === 0) return skip("nothing-dropped");
1238
+ if (plain2.dropped.length === 0) return skip("nothing-dropped");
1234
1239
  if (!options.summarise) return skip("no-summariser");
1235
- const source = plain.dropped.filter((message2) => !isCompactionSummary(message2));
1240
+ const source = plain2.dropped.filter((message2) => !isCompactionSummary(message2));
1236
1241
  if (source.length === 0) return skip("only-summaries");
1237
1242
  const ceiling = options.maxSummaryChars ?? DEFAULT_MAX_SUMMARY_CHARS;
1238
- const slack = plain.usage.promptLimitChars - plain.usage.totalChars - summaryEnvelopeChars();
1243
+ const slack = plain2.usage.promptLimitChars - plain2.usage.totalChars - summaryEnvelopeChars();
1239
1244
  const maxChars = Math.min(ceiling, slack);
1240
1245
  if (maxChars < MIN_SUMMARY_CHARS) return skip("no-room");
1241
1246
  const transcript = renderDroppedTranscript(source, {
@@ -1260,21 +1265,21 @@ async function compactShape(shape, options) {
1260
1265
  const text = truncateToLine(summary.trim(), maxChars);
1261
1266
  if (!text) return skip("summariser-empty");
1262
1267
  const message = compactionSummaryMessage(text);
1263
- const plan = replan(shape, plain, message);
1268
+ const plan = replan(shape, plain2, message);
1264
1269
  if (!plan) return skip("would-drop-more");
1265
1270
  return {
1266
1271
  ...plan,
1267
1272
  // The re-plan saw a transcript the span was already out of, so it has
1268
1273
  // nothing to report about it. What was dropped is what `plain` dropped.
1269
- dropped: plain.dropped,
1270
- droppedMessages: plain.droppedMessages,
1271
- droppedTurns: plain.droppedTurns,
1272
- droppedChars: plain.droppedChars,
1274
+ dropped: plain2.dropped,
1275
+ droppedMessages: plain2.droppedMessages,
1276
+ droppedTurns: plain2.droppedTurns,
1277
+ droppedChars: plain2.droppedChars,
1273
1278
  compacted: true,
1274
1279
  summary: text,
1275
- summarisedMessages: plain.dropped.length,
1276
- summarisedTurns: plain.droppedTurns,
1277
- warning: compactionWarning(plan, plain)
1280
+ summarisedMessages: plain2.dropped.length,
1281
+ summarisedTurns: plain2.droppedTurns,
1282
+ warning: compactionWarning(plan, plain2)
1278
1283
  };
1279
1284
  }
1280
1285
  function planKeepingSummaries(shape, messages, reserveChars) {
@@ -1290,8 +1295,8 @@ function planKeepingSummaries(shape, messages, reserveChars) {
1290
1295
  return shape.plan(messages, callerPinned, reserveChars);
1291
1296
  }
1292
1297
  }
1293
- function replan(shape, plain, summary) {
1294
- const kept = plain.messages;
1298
+ function replan(shape, plain2, summary) {
1299
+ const kept = plain2.messages;
1295
1300
  const insertAt = kept.findIndex((message) => message.role !== "system");
1296
1301
  const at = insertAt < 0 ? kept.length : insertAt;
1297
1302
  const next = [...kept.slice(0, at), summary, ...kept.slice(at)];
@@ -1307,9 +1312,9 @@ function replan(shape, plain, summary) {
1307
1312
  if (!kept.every((message) => survived.has(message))) return void 0;
1308
1313
  return plan;
1309
1314
  }
1310
- function compactionWarning(plan, plain) {
1311
- const turns = plural2(plain.droppedTurns, "earlier turn");
1312
- const messages = plural2(plain.dropped.length, "message");
1315
+ function compactionWarning(plan, plain2) {
1316
+ const turns = plural2(plain2.droppedTurns, "earlier turn");
1317
+ const messages = plural2(plain2.dropped.length, "message");
1313
1318
  return `Context budget: compacted ${turns} (${messages}) into a summary to stay under the ${count2(plan.usage.promptLimitChars)}-character prompt limit. The summary is in the transcript in their place; the recent turns are untouched.`;
1314
1319
  }
1315
1320
  function truncateToLine(text, maxChars) {
@@ -2043,6 +2048,10 @@ var UltracProvider = class {
2043
2048
  if (chunk.choices[0]?.finish_reason) finishReason = chunk.choices[0].finish_reason;
2044
2049
  const delta = chunk.choices[0]?.delta;
2045
2050
  if (!delta) continue;
2051
+ const reasoning = delta.reasoning_content;
2052
+ if (delta.content || reasoning || delta.tool_calls?.some(
2053
+ (part) => part.id || part.function?.name || part.function?.arguments
2054
+ )) request.onProgress?.();
2046
2055
  if (delta.content) content += delta.content;
2047
2056
  for (const part of delta.tool_calls ?? []) {
2048
2057
  const call = calls.get(part.index) ?? { id: "", name: "", arguments: "" };
@@ -2198,8 +2207,8 @@ async function validateSnapshotWithin(snapshot, timeoutMs) {
2198
2207
  try {
2199
2208
  return await Promise.race([
2200
2209
  snapshot.validate(),
2201
- new Promise((resolve15) => {
2202
- timer = setTimeout(() => resolve15(void 0), timeoutMs);
2210
+ new Promise((resolve17) => {
2211
+ timer = setTimeout(() => resolve17(void 0), timeoutMs);
2203
2212
  })
2204
2213
  ]);
2205
2214
  } finally {
@@ -2263,8 +2272,8 @@ async function settleWithin(promise, timeoutMs) {
2263
2272
  try {
2264
2273
  await Promise.race([
2265
2274
  Promise.resolve(promise).catch(() => void 0),
2266
- new Promise((resolve15) => {
2267
- timer = setTimeout(resolve15, timeoutMs);
2275
+ new Promise((resolve17) => {
2276
+ timer = setTimeout(resolve17, timeoutMs);
2268
2277
  })
2269
2278
  ]);
2270
2279
  } finally {
@@ -2308,10 +2317,11 @@ function toChatMessages(request) {
2308
2317
  ];
2309
2318
  }
2310
2319
  function toChatMessage(message) {
2311
- return {
2312
- role: message.role === "assistant" ? "assistant" : "user",
2313
- content: message.content
2314
- };
2320
+ if (message.role === "assistant") return { role: "assistant", content: message.content };
2321
+ return { role: "user", content: message.image ? [
2322
+ { type: "text", text: message.content },
2323
+ { type: "image_url", image_url: { url: message.image.url } }
2324
+ ] : message.content };
2315
2325
  }
2316
2326
  function toNativeChatMessage(message) {
2317
2327
  if (message.role === "tool") {
@@ -2331,6 +2341,10 @@ function toNativeChatMessage(message) {
2331
2341
  }))
2332
2342
  };
2333
2343
  }
2344
+ if (message.role === "user" && message.image) return { role: "user", content: [
2345
+ { type: "text", text: message.content },
2346
+ { type: "image_url", image_url: { url: message.image.url } }
2347
+ ] };
2334
2348
  return { role: message.role, content: message.content };
2335
2349
  }
2336
2350
  function toOpenAiTool(tool) {
@@ -2481,7 +2495,7 @@ var FallbackConfigSchema = z3.object({
2481
2495
  backoffFactor: z3.number().min(1).max(10).default(2),
2482
2496
  /** Randomize backoff to avoid synchronized retries. */
2483
2497
  jitter: z3.boolean().default(true),
2484
- /** Per-attempt timeout; for text streams it bounds time-to-first-token. 0 disables it. */
2498
+ /** Per-attempt timeout; text streams bound first-token time, native streams bound inactivity. 0 disables it. */
2485
2499
  requestTimeoutMs: z3.number().int().min(0).max(6e5).default(12e4)
2486
2500
  });
2487
2501
  var ProviderTimeoutError = class extends Error {
@@ -2722,17 +2736,17 @@ function computeBackoffDelay(attempt, options) {
2722
2736
  return Math.round(capped * (0.5 + random() * 0.5));
2723
2737
  }
2724
2738
  function defaultSleep(ms, cancel) {
2725
- return new Promise((resolve15) => {
2739
+ return new Promise((resolve17) => {
2726
2740
  if (cancel?.aborted) {
2727
- resolve15();
2741
+ resolve17();
2728
2742
  return;
2729
2743
  }
2730
- const timer = setTimeout(resolve15, ms);
2744
+ const timer = setTimeout(resolve17, ms);
2731
2745
  cancel?.addEventListener(
2732
2746
  "abort",
2733
2747
  () => {
2734
2748
  clearTimeout(timer);
2735
- resolve15();
2749
+ resolve17();
2736
2750
  },
2737
2751
  { once: true }
2738
2752
  );
@@ -2807,10 +2821,19 @@ var FallbackProvider = class {
2807
2821
  );
2808
2822
  }
2809
2823
  async chatWithTools(request) {
2810
- return this.run("chatWithTools", request.signal, request.model, (entry, model, signal) => {
2824
+ return this.run("chatWithTools", request.signal, request.model, (entry, model, signal, touch) => {
2811
2825
  if (entry.provider.supportsToolCalling && entry.provider.chatWithTools) {
2812
- return entry.provider.chatWithTools({ ...request, model, signal });
2826
+ return entry.provider.chatWithTools({
2827
+ ...request,
2828
+ model,
2829
+ signal,
2830
+ onProgress: () => {
2831
+ touch();
2832
+ request.onProgress?.();
2833
+ }
2834
+ });
2813
2835
  }
2836
+ if (request.messages.some((message) => "image" in message && message.image)) throw Object.assign(new Error("This fallback provider cannot inspect image attachments."), { status: 400 });
2814
2837
  return this.chatThroughToolProtocol(entry, model, request, signal);
2815
2838
  });
2816
2839
  }
@@ -2895,7 +2918,8 @@ var FallbackProvider = class {
2895
2918
  const result = await this.withDeadline(
2896
2919
  entry.name,
2897
2920
  signal,
2898
- (attemptSignal) => invoke(entry, model, attemptSignal)
2921
+ (attemptSignal, touch) => invoke(entry, model, attemptSignal, touch),
2922
+ operation === "chatWithTools"
2899
2923
  );
2900
2924
  this.active = target;
2901
2925
  trace.end({ outcome: "ok" });
@@ -3115,10 +3139,10 @@ var FallbackProvider = class {
3115
3139
  error.name = "AbortError";
3116
3140
  throw error;
3117
3141
  }
3118
- async withDeadline(provider, signal, run) {
3119
- const deadline = this.createDeadline(provider, signal);
3142
+ async withDeadline(provider, signal, run, inactivity = false) {
3143
+ const deadline = this.createDeadline(provider, signal, inactivity);
3120
3144
  try {
3121
- return await deadline.race(run(deadline.signal));
3145
+ return await deadline.race(run(deadline.signal, deadline.touch));
3122
3146
  } finally {
3123
3147
  deadline.clear();
3124
3148
  }
@@ -3128,31 +3152,55 @@ var FallbackProvider = class {
3128
3152
  * only ever rejects while the deadline is live, so clearing it can never leave
3129
3153
  * an unhandled rejection behind.
3130
3154
  */
3131
- createDeadline(provider, signal) {
3155
+ createDeadline(provider, signal, inactivity = false) {
3132
3156
  if (this.requestTimeoutMs <= 0) {
3133
- return { signal, race: (promise) => promise, clear: () => void 0 };
3157
+ return { signal, race: (promise) => promise, clear: () => void 0, touch: () => void 0 };
3134
3158
  }
3135
3159
  const controller = new AbortController();
3136
- const forwardAbort = () => controller.abort(signal?.reason);
3160
+ let cleared = false;
3161
+ let idleTimer;
3162
+ const hardTimer = new AbortController();
3163
+ let rejectExpired;
3164
+ const expired = new Promise((_, reject) => {
3165
+ rejectExpired = reject;
3166
+ });
3167
+ const forwardAbort = () => {
3168
+ const reason = signal?.reason ?? Object.assign(new Error("Request was aborted."), { name: "AbortError" });
3169
+ rejectExpired(reason);
3170
+ controller.abort(reason);
3171
+ };
3172
+ const expire = (timeoutMs) => {
3173
+ const error = new ProviderTimeoutError(timeoutMs, provider);
3174
+ rejectExpired(error);
3175
+ controller.abort(error);
3176
+ };
3177
+ const touch = () => {
3178
+ if (cleared || controller.signal.aborted) return;
3179
+ idleTimer?.abort();
3180
+ const timer = new AbortController();
3181
+ idleTimer = timer;
3182
+ void this.sleep(this.requestTimeoutMs, timer.signal).then(() => {
3183
+ if (!cleared && !timer.signal.aborted) expire(this.requestTimeoutMs);
3184
+ });
3185
+ };
3186
+ touch();
3187
+ if (inactivity) {
3188
+ void this.sleep(3e5, hardTimer.signal).then(() => {
3189
+ if (!cleared && !hardTimer.signal.aborted) expire(3e5);
3190
+ });
3191
+ }
3137
3192
  if (signal) {
3138
3193
  if (signal.aborted) forwardAbort();
3139
3194
  else signal.addEventListener("abort", forwardAbort, { once: true });
3140
3195
  }
3141
- let cleared = false;
3142
- const cancelDeadline = new AbortController();
3143
- const expired = new Promise((_, reject) => {
3144
- void this.sleep(this.requestTimeoutMs, cancelDeadline.signal).then(() => {
3145
- if (cleared) return;
3146
- controller.abort();
3147
- reject(new ProviderTimeoutError(this.requestTimeoutMs, provider));
3148
- });
3149
- });
3150
3196
  return {
3151
3197
  signal: controller.signal,
3152
3198
  race: (promise) => Promise.race([promise, expired]),
3199
+ touch: inactivity ? touch : () => void 0,
3153
3200
  clear: () => {
3154
3201
  cleared = true;
3155
- cancelDeadline.abort();
3202
+ idleTimer?.abort();
3203
+ hardTimer.abort();
3156
3204
  signal?.removeEventListener("abort", forwardAbort);
3157
3205
  }
3158
3206
  };
@@ -4065,7 +4113,7 @@ function runProcess(command, args, options) {
4065
4113
  });
4066
4114
  }
4067
4115
  const { env: env2, redactedEnvKeys } = createSanitizedProcessEnv();
4068
- return new Promise((resolve15, reject) => {
4116
+ return new Promise((resolve17, reject) => {
4069
4117
  const child = spawn(command, args, {
4070
4118
  cwd: options.cwd,
4071
4119
  stdio: ["pipe", "pipe", "pipe"],
@@ -4083,7 +4131,7 @@ function runProcess(command, args, options) {
4083
4131
  });
4084
4132
  const untrack = trackProcessGroup(child.pid);
4085
4133
  const stdout6 = createCapture(maxBytes);
4086
- const stderr = createCapture(maxBytes);
4134
+ const stderr2 = createCapture(maxBytes);
4087
4135
  let settled = false;
4088
4136
  let started = false;
4089
4137
  let termination;
@@ -4139,7 +4187,7 @@ function runProcess(command, args, options) {
4139
4187
  };
4140
4188
  const onAbort = () => beginTermination("aborted");
4141
4189
  child.stdout.on("data", (chunk) => stdout6.push(chunk));
4142
- child.stderr.on("data", (chunk) => stderr.push(chunk));
4190
+ child.stderr.on("data", (chunk) => stderr2.push(chunk));
4143
4191
  child.stdout.on("error", () => {
4144
4192
  });
4145
4193
  child.stderr.on("error", () => {
@@ -4150,9 +4198,9 @@ function runProcess(command, args, options) {
4150
4198
  child.on("error", (error) => {
4151
4199
  finish(() => {
4152
4200
  if (termination === "aborted") {
4153
- resolve15({
4201
+ resolve17({
4154
4202
  stdout: stdout6.text(),
4155
- stderr: stderr.text(),
4203
+ stderr: stderr2.text(),
4156
4204
  started,
4157
4205
  aborted: true,
4158
4206
  exitCode: null,
@@ -4160,7 +4208,7 @@ function runProcess(command, args, options) {
4160
4208
  timedOut: false,
4161
4209
  timeoutMs,
4162
4210
  ...stdout6.truncation() ? { stdoutTruncation: stdout6.truncation() } : {},
4163
- ...stderr.truncation() ? { stderrTruncation: stderr.truncation() } : {},
4211
+ ...stderr2.truncation() ? { stderrTruncation: stderr2.truncation() } : {},
4164
4212
  redactedEnvKeys
4165
4213
  });
4166
4214
  return;
@@ -4170,11 +4218,11 @@ function runProcess(command, args, options) {
4170
4218
  });
4171
4219
  child.on("close", (exitCode, signal) => {
4172
4220
  const stdoutTruncation = stdout6.truncation();
4173
- const stderrTruncation = stderr.truncation();
4221
+ const stderrTruncation = stderr2.truncation();
4174
4222
  finish(
4175
- () => resolve15({
4223
+ () => resolve17({
4176
4224
  stdout: stdout6.text(),
4177
- stderr: stderr.text(),
4225
+ stderr: stderr2.text(),
4178
4226
  started,
4179
4227
  aborted: termination === "aborted",
4180
4228
  exitCode,
@@ -5376,7 +5424,7 @@ var diagnosticsTool = {
5376
5424
  const byFile = summarizeByFile(matched);
5377
5425
  const records = matched.slice(0, input.maxRecords);
5378
5426
  const checkerExit = runs[0].exitCode;
5379
- const clean = matched.length === 0;
5427
+ const clean2 = matched.length === 0;
5380
5428
  const unparsed = matched.length === 0 && runs.some((run) => run.exitCode !== 0);
5381
5429
  const rawTail = unparsed ? tail(runs.find((run) => run.exitCode !== 0)?.output ?? "", 2e3) : void 0;
5382
5430
  const lines = [
@@ -5393,9 +5441,9 @@ var diagnosticsTool = {
5393
5441
  ...byFile.length ? byFile.slice(0, input.maxFiles).map((entry) => `- ${entry.file}: ${entry.errors} error(s), ${entry.warnings} warning(s)`) : ["- none"],
5394
5442
  byFile.length > input.maxFiles ? `- ...${byFile.length - input.maxFiles} more file(s)` : void 0,
5395
5443
  "",
5396
- clean ? unparsed ? "No diagnostics parsed, but the checker exited non-zero. Raw output tail:" : "No diagnostics." : `Diagnostics (showing ${records.length} of ${matched.length}):`,
5397
- ...clean ? rawTail ? [rawTail] : [] : records.map(renderDiagnostic),
5398
- !clean && matched.length > records.length ? `...${matched.length - records.length} more suppressed` : void 0
5444
+ clean2 ? unparsed ? "No diagnostics parsed, but the checker exited non-zero. Raw output tail:" : "No diagnostics." : `Diagnostics (showing ${records.length} of ${matched.length}):`,
5445
+ ...clean2 ? rawTail ? [rawTail] : [] : records.map(renderDiagnostic),
5446
+ !clean2 && matched.length > records.length ? `...${matched.length - records.length} more suppressed` : void 0
5399
5447
  ].filter((line) => line !== void 0);
5400
5448
  return {
5401
5449
  ok: !runs.some((run) => run.aborted) && (matched.length > 0 || runs.every((run) => run.exitCode === 0)),
@@ -5675,8 +5723,8 @@ function normalizeFilters(workspace, paths) {
5675
5723
  }
5676
5724
  function normalizeDiagnosticPath(workspace, diagnostic) {
5677
5725
  try {
5678
- const relative2 = toWorkspaceRelative(workspace, resolveInsideWorkspace(workspace, diagnostic.file));
5679
- return { ...diagnostic, file: relative2.replaceAll("\\", "/") };
5726
+ const relative3 = toWorkspaceRelative(workspace, resolveInsideWorkspace(workspace, diagnostic.file));
5727
+ return { ...diagnostic, file: relative3.replaceAll("\\", "/") };
5680
5728
  } catch {
5681
5729
  return { ...diagnostic, file: diagnostic.file.replaceAll("\\", "/") };
5682
5730
  }
@@ -6089,6 +6137,21 @@ var listFilesTool = {
6089
6137
  }
6090
6138
  };
6091
6139
  var RANGE_MARKER_RESERVE_CHARS = 300;
6140
+ async function resolveReadableFile(workspace, path) {
6141
+ try {
6142
+ const target = await resolveReadablePath(workspace, path);
6143
+ const fileStat = await stat4(target);
6144
+ if (!fileStat.isFile()) throw new Error(`Not a file: ${path}`);
6145
+ return { target, fileStat };
6146
+ } catch (error) {
6147
+ const code = error.code;
6148
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
6149
+ throw new Error(
6150
+ `File not found: ${JSON.stringify(path)}. Use list_files to discover the exact workspace path or search_files to locate relevant code, then retry with a confirmed path. No alternate file was read.`,
6151
+ { cause: error }
6152
+ );
6153
+ }
6154
+ }
6092
6155
  async function readFilePrefix(target, maxBytes) {
6093
6156
  const handle = await open3(target, "r");
6094
6157
  try {
@@ -6147,11 +6210,7 @@ var readFileTool = {
6147
6210
  description: "Read a UTF-8 text file from inside the workspace. Reads a line range (startLine, maxLines) when the whole file would not fit the tool-result cap, and says where to continue.",
6148
6211
  inputSchema: ReadFileInput,
6149
6212
  async execute(input, context) {
6150
- const target = await resolveReadablePath(context.workspace, input.path);
6151
- const fileStat = await stat4(target);
6152
- if (!fileStat.isFile()) {
6153
- throw new Error(`Not a file: ${input.path}`);
6154
- }
6213
+ const { target } = await resolveReadableFile(context.workspace, input.path);
6155
6214
  const { buffer: clipped, totalBytes, stable } = await readFilePrefix(target, input.maxBytes);
6156
6215
  const text = clipped.toString("utf8");
6157
6216
  const clippedByBytes = totalBytes > clipped.byteLength;
@@ -6247,11 +6306,7 @@ var readManyFilesTool = {
6247
6306
  const sections = [];
6248
6307
  let remainingBytes = input.maxTotalBytes;
6249
6308
  for (const path of paths) {
6250
- const target = await resolveReadablePath(context.workspace, path);
6251
- const fileStat = await stat4(target);
6252
- if (!fileStat.isFile()) {
6253
- throw new Error(`Not a file: ${path}`);
6254
- }
6309
+ const { target, fileStat } = await resolveReadableFile(context.workspace, path);
6255
6310
  const readBudget = Math.min(input.maxBytesPerFile, remainingBytes);
6256
6311
  if (readBudget <= 0) {
6257
6312
  files.push({ path, bytesRead: 0, totalBytes: fileStat.size, truncated: true });
@@ -6864,12 +6919,12 @@ var builtinSecretRules = [
6864
6919
  ];
6865
6920
  async function resolveArtifact(workspace, requestedPath) {
6866
6921
  const absolute = await resolveInsideWorkspaceReal(workspace, requestedPath);
6867
- const relative2 = toWorkspaceRelative(workspace, absolute);
6922
+ const relative3 = toWorkspaceRelative(workspace, absolute);
6868
6923
  const info = await stat5(absolute);
6869
6924
  if (!info.isFile()) {
6870
6925
  throw new Error(`Not a file: ${requestedPath}`);
6871
6926
  }
6872
- return { absolute, relative: relative2 };
6927
+ return { absolute, relative: relative3 };
6873
6928
  }
6874
6929
  function formatMode(mode) {
6875
6930
  return (mode & 511).toString(8).padStart(3, "0");
@@ -8082,6 +8137,213 @@ async function loadSkillsContext(workspace) {
8082
8137
  }).join("\n") + notice;
8083
8138
  }
8084
8139
 
8140
+ // src/skills/commands.ts
8141
+ var BUILTIN_SLASH_COMMANDS = [
8142
+ "doctor",
8143
+ "mcp",
8144
+ "continue",
8145
+ "autonomous",
8146
+ "skill",
8147
+ "update",
8148
+ "logs",
8149
+ "ghost",
8150
+ "output",
8151
+ "usage",
8152
+ "max-steps",
8153
+ "resume",
8154
+ "deny",
8155
+ "plan",
8156
+ "review",
8157
+ "coding",
8158
+ "status",
8159
+ "context",
8160
+ "compact",
8161
+ "diff",
8162
+ "sessions",
8163
+ "save",
8164
+ "test",
8165
+ "init",
8166
+ "new",
8167
+ "allow",
8168
+ "clear",
8169
+ "exit",
8170
+ "help",
8171
+ "memory",
8172
+ "mode",
8173
+ "model",
8174
+ "permissions",
8175
+ "quit",
8176
+ "skills",
8177
+ "tools",
8178
+ "whoami"
8179
+ ];
8180
+ var COMMAND_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8181
+ var MAX_COMMAND_LENGTH = 64;
8182
+ function parseSlashInput(input) {
8183
+ const trimmed = input.trim();
8184
+ if (!trimmed.startsWith("/")) return null;
8185
+ const rest = trimmed.slice(1);
8186
+ const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
8187
+ if (!match) return null;
8188
+ return { command: match[1], args: (match[2] ?? "").trim() };
8189
+ }
8190
+ function isBuiltinSlashCommand(command) {
8191
+ return BUILTIN_SLASH_COMMANDS.includes(command.toLowerCase());
8192
+ }
8193
+ function normalizeCommandName(value) {
8194
+ const candidate = value.trim().replace(/^\/+/, "").trim();
8195
+ if (!candidate) {
8196
+ throw new Error("Slash command name is empty; expected kebab-case, for example: open-pr.");
8197
+ }
8198
+ if (candidate.length > MAX_COMMAND_LENGTH) {
8199
+ throw new Error(
8200
+ `Slash command name "${candidate}" is longer than ${MAX_COMMAND_LENGTH} characters.`
8201
+ );
8202
+ }
8203
+ if (!COMMAND_NAME_RE.test(candidate)) {
8204
+ throw new Error(
8205
+ `Invalid slash command name "${candidate}": use kebab-case - lowercase letters, digits, and single hyphens, no spaces or slashes (for example: open-pr).`
8206
+ );
8207
+ }
8208
+ return candidate;
8209
+ }
8210
+ async function collectEntries(workspace) {
8211
+ const skills = await discoverSkills(workspace);
8212
+ const claimed = /* @__PURE__ */ new Map();
8213
+ const conflicts = [];
8214
+ for (const skill of skills) {
8215
+ let declaration;
8216
+ try {
8217
+ declaration = skill.command ?? null;
8218
+ } catch (error) {
8219
+ conflicts.push({
8220
+ command: "",
8221
+ skillName: skill.name,
8222
+ scope: skill.scope,
8223
+ path: skill.path,
8224
+ reason: "invalid",
8225
+ message: `Skill "${skill.name}" (${skill.scope}) was skipped: ${error instanceof Error ? error.message : String(error)}`
8226
+ });
8227
+ continue;
8228
+ }
8229
+ if (!declaration) continue;
8230
+ let command;
8231
+ try {
8232
+ command = normalizeCommandName(declaration.raw);
8233
+ } catch (error) {
8234
+ conflicts.push({
8235
+ command: declaration.raw,
8236
+ skillName: skill.name,
8237
+ scope: skill.scope,
8238
+ path: skill.path,
8239
+ reason: "invalid",
8240
+ message: `Skill "${skill.name}" (${skill.scope}) declares a malformed command: ${error instanceof Error ? error.message : String(error)}`
8241
+ });
8242
+ continue;
8243
+ }
8244
+ if (isBuiltinSlashCommand(command)) {
8245
+ conflicts.push({
8246
+ command,
8247
+ skillName: skill.name,
8248
+ scope: skill.scope,
8249
+ path: skill.path,
8250
+ reason: "builtin",
8251
+ message: `Skill "${skill.name}" (${skill.scope}) declares /${command}, which is a built-in command. Built-ins always win - rename the command: key in ${skill.path}.`
8252
+ });
8253
+ continue;
8254
+ }
8255
+ const entry = {
8256
+ skill,
8257
+ command: {
8258
+ command,
8259
+ skillName: skill.name,
8260
+ description: skill.description,
8261
+ argumentHint: declaration.argumentHint,
8262
+ scope: skill.scope,
8263
+ path: skill.path
8264
+ }
8265
+ };
8266
+ const existing = claimed.get(command);
8267
+ if (!existing) {
8268
+ claimed.set(command, entry);
8269
+ continue;
8270
+ }
8271
+ const winner = existing.skill.scope === "global" && skill.scope === "project" ? entry : existing;
8272
+ const loser = winner === entry ? existing : entry;
8273
+ claimed.set(command, winner);
8274
+ conflicts.push({
8275
+ command,
8276
+ skillName: loser.skill.name,
8277
+ scope: loser.skill.scope,
8278
+ path: loser.skill.path,
8279
+ reason: "duplicate",
8280
+ message: `/${command} is declared by both "${winner.skill.name}" (${winner.skill.scope}) and "${loser.skill.name}" (${loser.skill.scope}). "${winner.skill.name}" wins; rename the command: key in ${loser.skill.path}.`
8281
+ });
8282
+ }
8283
+ const entries = [...claimed.values()].sort((a, b) => a.command.command.localeCompare(b.command.command));
8284
+ return { entries, conflicts };
8285
+ }
8286
+ async function listSkillCommands(workspace) {
8287
+ const { entries, conflicts } = await collectEntries(workspace);
8288
+ return { commands: entries.map((entry) => entry.command), conflicts };
8289
+ }
8290
+ function buildSkillCommandPrompt(skill, command, args) {
8291
+ const argumentLine = command.argumentHint ? `Arguments (${command.argumentHint}): ${args || "(none provided)"}` : `Arguments: ${args || "(none provided)"}`;
8292
+ const header = [
8293
+ `The user ran the /${command.command} slash command, which runs the "${skill.name}" skill (${skill.scope}).`,
8294
+ skill.description ? `Description: ${skill.description}` : void 0,
8295
+ argumentLine,
8296
+ skill.permissions.length ? `Permissions needed: ${skill.permissions.join(", ")}` : void 0,
8297
+ "Follow the instructions below now, applying the arguments where they are relevant."
8298
+ ].filter(Boolean).join("\n");
8299
+ return `${header}
8300
+
8301
+ --- instructions ---
8302
+ ${skill.body}`;
8303
+ }
8304
+ async function resolveSkillCommand(workspace, input) {
8305
+ const parsed = parseSlashInput(input);
8306
+ if (!parsed) return { status: "not-a-command" };
8307
+ const command = parsed.command.toLowerCase();
8308
+ if (isBuiltinSlashCommand(command)) {
8309
+ return { status: "builtin", command };
8310
+ }
8311
+ const { entries, conflicts } = await collectEntries(workspace);
8312
+ const match = entries.find((entry) => entry.command.command === command);
8313
+ if (!match) {
8314
+ const related = conflicts.filter((conflict) => conflict.command === command);
8315
+ if (related.length) {
8316
+ return {
8317
+ status: "conflict",
8318
+ command,
8319
+ message: related.map((conflict) => conflict.message).join("\n"),
8320
+ conflicts: related
8321
+ };
8322
+ }
8323
+ return {
8324
+ status: "unknown",
8325
+ command,
8326
+ available: entries.map((entry) => entry.command.command)
8327
+ };
8328
+ }
8329
+ return {
8330
+ status: "ok",
8331
+ command: match.command,
8332
+ skill: match.skill,
8333
+ args: parsed.args,
8334
+ prompt: buildSkillCommandPrompt(match.skill, match.command, parsed.args)
8335
+ };
8336
+ }
8337
+ function formatSkillCommandHelp(index) {
8338
+ if (!index.commands.length) return "";
8339
+ const lines = index.commands.map((command) => {
8340
+ const invocation = command.argumentHint ? `/${command.command} ${command.argumentHint}` : `/${command.command}`;
8341
+ const description = command.description || `Run the ${command.skillName} skill`;
8342
+ return ` ${invocation.padEnd(22)}${description} (skill: ${command.skillName})`;
8343
+ });
8344
+ return ["Skill commands:", ...lines].join("\n");
8345
+ }
8346
+
8085
8347
  // src/tools/workspace/skill-tools.ts
8086
8348
  var SkillInput = z17.object({
8087
8349
  name: z17.string().min(1).describe("Name of the skill to load, as shown in the skills list.")
@@ -8128,6 +8390,26 @@ var listSkillsTool = {
8128
8390
  };
8129
8391
  }
8130
8392
  };
8393
+ var CreateSkillInput = z17.object({
8394
+ name: z17.string().min(1).max(80),
8395
+ description: z17.string().trim().min(1).max(512),
8396
+ body: z17.string().trim().min(1).max(6e4),
8397
+ command: z17.string().min(1).max(64).optional(),
8398
+ argumentHint: z17.string().max(256).optional()
8399
+ });
8400
+ var createSkillTool = {
8401
+ name: "create_skill",
8402
+ description: "Create a project SKILL.md with instructions; never overwrites. Trust is required before loading project skills.",
8403
+ permissions: ["write"],
8404
+ inputSchema: CreateSkillInput,
8405
+ async execute(input, context) {
8406
+ const command = input.command ? normalizeCommandName(input.command) : void 0;
8407
+ if (command && isBuiltinSlashCommand(command)) throw new Error(`/${command} is reserved.`);
8408
+ if (input.argumentHint && (!command || /[\r\n]/.test(input.argumentHint))) throw new Error("Argument hint requires a command and a single line.");
8409
+ const skill = await createSkill(context.workspace, { ...input, command, scope: "project" });
8410
+ return { changed: true, content: `Created ${skill.name}: ${skill.path}. Project skills require workspace trust before use.`, data: { name: skill.name, path: skill.path } };
8411
+ }
8412
+ };
8131
8413
 
8132
8414
  // src/tools/workspace/todo-tools.ts
8133
8415
  import { z as z18 } from "zod";
@@ -9121,18 +9403,18 @@ function isConcurrencySafe(tool) {
9121
9403
  }
9122
9404
  function planToolBatches(calls, tools) {
9123
9405
  const batches = [];
9124
- let open9;
9406
+ let open11;
9125
9407
  for (const call of calls) {
9126
9408
  if (!isConcurrencySafe(tools.get(call.tool))) {
9127
9409
  batches.push([call]);
9128
- open9 = void 0;
9410
+ open11 = void 0;
9129
9411
  continue;
9130
9412
  }
9131
- if (!open9) {
9132
- open9 = [];
9133
- batches.push(open9);
9413
+ if (!open11) {
9414
+ open11 = [];
9415
+ batches.push(open11);
9134
9416
  }
9135
- open9.push(call);
9417
+ open11.push(call);
9136
9418
  }
9137
9419
  return batches;
9138
9420
  }
@@ -9627,7 +9909,7 @@ async function acquireLock(directory, waitMs) {
9627
9909
  };
9628
9910
  } catch (error) {
9629
9911
  if (error.code !== "EEXIST" || Date.now() >= deadline) throw error;
9630
- await new Promise((resolve15) => setTimeout(resolve15, 10));
9912
+ await new Promise((resolve17) => setTimeout(resolve17, 10));
9631
9913
  }
9632
9914
  }
9633
9915
  }
@@ -9675,6 +9957,8 @@ function readSessionContextSnapshot(session) {
9675
9957
  const snapshot = session.contextSnapshot;
9676
9958
  if (!snapshot || typeof snapshot !== "object" || snapshot.version !== 1 || !["native", "text", "run"].includes(snapshot.kind) || !Number.isSafeInteger(snapshot.completedSteps) || snapshot.completedSteps < 0 || !Array.isArray(snapshot.messages) || snapshot.messages.some((message) => {
9677
9959
  if (!message || typeof message !== "object" || !["user", "assistant", "system", "tool"].includes(message.role) || typeof message.content !== "string") return true;
9960
+ const image = message.image;
9961
+ if (image !== void 0 && (message.role !== "user" || !image || typeof image.url !== "string" || image.url.length > 7 * 1024 * 1024 || !/^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]+={0,2}$/.test(image.url))) return true;
9678
9962
  const native = message;
9679
9963
  return snapshot.kind !== "native" && (message.role === "system" || native.toolCalls !== void 0 || native.toolCallId !== void 0) || native.toolCalls !== void 0 && message.role !== "assistant" || native.toolCallId !== void 0 && message.role !== "tool" || snapshot.kind === "native" && message.role === "tool" && typeof native.toolCallId !== "string" || native.toolCallId !== void 0 && typeof native.toolCallId !== "string" || native.toolCalls !== void 0 && (!Array.isArray(native.toolCalls) || native.toolCalls.some((call) => !call || typeof call.id !== "string" || typeof call.name !== "string" || typeof call.arguments !== "string"));
9680
9964
  }) || !completeContextUnits(snapshot.messages) || (readSessionUserConstraints(session) ?? [{ content: session.task }]).some((constraint) => !snapshot.messages.some((message) => message.role === "user" && message.content === constraint.content))) throw new Error(`Session ${session.id} has an invalid saved context snapshot and cannot be continued safely.`);
@@ -10916,10 +11200,10 @@ async function withHardTimeout(promise, ms, onTimeout) {
10916
11200
  (error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) })
10917
11201
  );
10918
11202
  let timer;
10919
- const expiry = new Promise((resolve15) => {
11203
+ const expiry = new Promise((resolve17) => {
10920
11204
  timer = setTimeout(() => {
10921
11205
  onTimeout?.();
10922
- resolve15(void 0);
11206
+ resolve17(void 0);
10923
11207
  }, ms);
10924
11208
  });
10925
11209
  try {
@@ -11351,8 +11635,8 @@ function tokenizeCommand(command) {
11351
11635
  function renderCommandLine(plan) {
11352
11636
  return [plan.command, ...plan.args].join(" ");
11353
11637
  }
11354
- function parseTestOutput(runner, stdout6, stderr, workspace) {
11355
- const combined = [stdout6, stderr].filter(Boolean).join("\n");
11638
+ function parseTestOutput(runner, stdout6, stderr2, workspace) {
11639
+ const combined = [stdout6, stderr2].filter(Boolean).join("\n");
11356
11640
  if (runner === "vitest" || runner === "jest") {
11357
11641
  const report = extractJestStyleReport(stdout6) ?? extractJestStyleReport(combined);
11358
11642
  if (report) return parseJestStyleReport(report, workspace);
@@ -12076,8 +12360,8 @@ function findFileInText(text, workspace) {
12076
12360
  for (const match of text.matchAll(stackFrame)) {
12077
12361
  const candidate = match[1];
12078
12362
  if (candidate.includes("node_modules") || candidate.includes("/internal/")) continue;
12079
- const relative2 = relativizeFile(candidate, workspace);
12080
- if (relative2 !== "unknown") return relative2;
12363
+ const relative3 = relativizeFile(candidate, workspace);
12364
+ if (relative3 !== "unknown") return relative3;
12081
12365
  }
12082
12366
  return null;
12083
12367
  }
@@ -12565,12 +12849,12 @@ async function sendRequest(url, input, approved, signal) {
12565
12849
  // The socket goes to a pinned address; TLS still verifies the name.
12566
12850
  servername: secure && !isIP(hostname) ? hostname : void 0
12567
12851
  };
12568
- return await new Promise((resolve15, reject) => {
12852
+ return await new Promise((resolve17, reject) => {
12569
12853
  const request = secure ? httpsRequest(options) : httpRequest(options);
12570
12854
  request.on("response", (response) => {
12571
12855
  response.on("error", () => {
12572
12856
  });
12573
- resolve15({
12857
+ resolve17({
12574
12858
  status: response.statusCode ?? 0,
12575
12859
  statusText: response.statusMessage ?? "",
12576
12860
  ok: (response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) < 300,
@@ -12798,64 +13082,67 @@ function decodeCodePoint(value, fallback) {
12798
13082
  // src/tools/workspace.ts
12799
13083
  function createDefaultToolRegistry() {
12800
13084
  const { todoWriteTool, todoReadTool } = createTodoTools();
12801
- return new ToolRegistry().register(listFilesTool).register(readFileTool).register(readManyFilesTool).register(searchFilesTool).register(astSearchTool).register(inspectProjectTool).register(inspectArtifactTool).register(hexDumpTool).register(extractStringsTool).register(scanSecretsTool).register(fileTimelineTool).register(repoMapTool).register(repoHandbookTool).register(contextBundleTool).register(verificationPlanTool).register(verificationReportTool).register(writeFileTool).register(replaceInFileTool).register(multiEditTool).register(applyPatchTool).register(gitStatusTool).register(gitWorktreeSummaryTool).register(gitDiffTool).register(gitLogTool).register(gitShowTool).register(gitBlameTool).register(gitCommitTool).register(gitPushTool).register(runPackageScriptTool).register(runTestsTool).register(diagnosticsTool).register(listSessionsTool).register(readSessionTool).register(searchSessionsTool).register(fetchUrlTool).register(runShellTool).register(listProcessesTool).register(memoryWriteTool).register(memoryReadTool).register(memoryListTool).register(memorySearchTool).register(memoryDeleteTool).register(skillTool).register(listSkillsTool).register(todoWriteTool).register(todoReadTool).register(taskTool).register(askUserTool);
13085
+ return new ToolRegistry().register(listFilesTool).register(readFileTool).register(readManyFilesTool).register(searchFilesTool).register(astSearchTool).register(inspectProjectTool).register(inspectArtifactTool).register(hexDumpTool).register(extractStringsTool).register(scanSecretsTool).register(fileTimelineTool).register(repoMapTool).register(repoHandbookTool).register(contextBundleTool).register(verificationPlanTool).register(verificationReportTool).register(writeFileTool).register(replaceInFileTool).register(multiEditTool).register(applyPatchTool).register(gitStatusTool).register(gitWorktreeSummaryTool).register(gitDiffTool).register(gitLogTool).register(gitShowTool).register(gitBlameTool).register(gitCommitTool).register(gitPushTool).register(runPackageScriptTool).register(runTestsTool).register(diagnosticsTool).register(listSessionsTool).register(readSessionTool).register(searchSessionsTool).register(fetchUrlTool).register(runShellTool).register(listProcessesTool).register(memoryWriteTool).register(memoryReadTool).register(memoryListTool).register(memorySearchTool).register(memoryDeleteTool).register(skillTool).register(listSkillsTool).register(createSkillTool).register(todoWriteTool).register(todoReadTool).register(taskTool).register(askUserTool);
12802
13086
  }
12803
13087
 
12804
13088
  // src/commands/doctor.ts
12805
13089
  function registerDoctorCommand(program2) {
12806
13090
  program2.command("doctor").description("Check the local ultrac setup").option("-w, --workspace <path>", "Workspace path", ".").option("--json", "Print JSON output", false).action(async (options) => {
12807
- const workspace = resolve7(options.workspace);
12808
- const workspaceReady = await stat10(workspace).then((entry) => entry.isDirectory(), () => false);
12809
- const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split(".").map(Number);
12810
- const nodeReady = nodeMajor > 22 || nodeMajor === 22 && nodeMinor >= 12;
12811
- const config = await loadConfig(workspace).catch(() => null);
12812
- const configExists = await hasConfig(workspace);
12813
- const isGitRepo = await pathExists(resolve7(workspace, ".git"));
12814
- const ultracCredentials = await resolveUltracCredentials();
12815
- const baseUrl = ultracCredentials ? { baseUrl: ultracCredentials.baseUrl, source: ultracCredentials.baseUrlSource } : resolveBaseUrl();
12816
- const tools = createDefaultToolRegistry().list();
12817
- const toolSchemasReady = tools.every((tool) => tool.inputSchema && typeof tool.inputSchema === "object");
12818
- const checks = [
12819
- { label: "node", value: `${process.versions.node} (requires >=22.12.0)`, ok: nodeReady },
12820
- {
12821
- label: "workspace",
12822
- value: workspaceReady ? workspace : `${workspace} - must be an accessible directory`,
12823
- ok: workspaceReady
12824
- },
12825
- { label: "git repository", value: isGitRepo ? "found" : "not found", ok: isGitRepo },
12826
- {
12827
- label: "config",
12828
- value: config ? configExists ? configPath(workspace) : "using defaults" : `unable to load configuration - check ${configPath(workspace)} and local trust settings`,
12829
- ok: config !== null
12830
- },
12831
- {
12832
- label: "tool schemas",
12833
- value: `${tools.length} exported`,
12834
- ok: toolSchemasReady
12835
- },
12836
- { label: "provider", value: config?.provider ?? "unavailable", ok: config !== null },
12837
- { label: "model", value: config?.model ?? "unavailable", ok: config !== null },
12838
- {
12839
- label: "Ultrac sign-in",
12840
- value: ultracCredentials ? `signed in via ${describeKeySource(ultracCredentials)}` : "not signed in - run `ultrac login` or set ULTRAC_API_KEY",
12841
- ok: config?.provider !== "ultrac" || Boolean(ultracCredentials)
12842
- },
12843
- {
12844
- label: "Ultrac API base URL",
12845
- value: `${baseUrl.baseUrl} via ${describeBaseUrlSource(baseUrl.source)}`,
12846
- ok: true
12847
- }
12848
- ];
12849
- if (options.json) {
12850
- console.log(JSON.stringify({ ok: checks.every((check) => check.ok), checks }, null, 2));
12851
- } else {
12852
- checks.forEach((check) => printCheck(check));
12853
- }
12854
- if (checks.some((check) => !check.ok)) {
12855
- process.exitCode = 1;
12856
- }
13091
+ await runDoctor(options.workspace, options.json);
12857
13092
  });
12858
13093
  }
13094
+ async function runDoctor(workspacePath, json = false, setExitCode = true) {
13095
+ const workspace = resolve7(workspacePath);
13096
+ const workspaceReady = await stat10(workspace).then((entry) => entry.isDirectory(), () => false);
13097
+ const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split(".").map(Number);
13098
+ const nodeReady = nodeMajor > 22 || nodeMajor === 22 && nodeMinor >= 12;
13099
+ const config = await loadConfig(workspace).catch(() => null);
13100
+ const configExists = await hasConfig(workspace);
13101
+ const isGitRepo = await pathExists(resolve7(workspace, ".git"));
13102
+ const ultracCredentials = await resolveUltracCredentials();
13103
+ const baseUrl = ultracCredentials ? { baseUrl: ultracCredentials.baseUrl, source: ultracCredentials.baseUrlSource } : resolveBaseUrl();
13104
+ const tools = createDefaultToolRegistry().list();
13105
+ const toolSchemasReady = tools.every((tool) => tool.inputSchema && typeof tool.inputSchema === "object");
13106
+ const checks = [
13107
+ { label: "node", value: `${process.versions.node} (requires >=22.12.0)`, ok: nodeReady },
13108
+ {
13109
+ label: "workspace",
13110
+ value: workspaceReady ? workspace : `${workspace} - must be an accessible directory`,
13111
+ ok: workspaceReady
13112
+ },
13113
+ { label: "git repository", value: isGitRepo ? "found" : "not found", ok: isGitRepo },
13114
+ {
13115
+ label: "config",
13116
+ value: config ? configExists ? configPath(workspace) : "using defaults" : `unable to load configuration - check ${configPath(workspace)} and local trust settings`,
13117
+ ok: config !== null
13118
+ },
13119
+ {
13120
+ label: "tool schemas",
13121
+ value: `${tools.length} exported`,
13122
+ ok: toolSchemasReady
13123
+ },
13124
+ { label: "provider", value: config?.provider ?? "unavailable", ok: config !== null },
13125
+ { label: "model", value: config?.model ?? "unavailable", ok: config !== null },
13126
+ {
13127
+ label: "Ultrac sign-in",
13128
+ value: ultracCredentials ? `signed in via ${describeKeySource(ultracCredentials)}` : "not signed in - run `ultrac login` or set ULTRAC_API_KEY",
13129
+ ok: config?.provider !== "ultrac" || Boolean(ultracCredentials)
13130
+ },
13131
+ {
13132
+ label: "Ultrac API base URL",
13133
+ value: `${baseUrl.baseUrl} via ${describeBaseUrlSource(baseUrl.source)}`,
13134
+ ok: true
13135
+ }
13136
+ ];
13137
+ if (json) {
13138
+ console.log(JSON.stringify({ ok: checks.every((check) => check.ok), checks }, null, 2));
13139
+ } else {
13140
+ checks.forEach((check) => printCheck(check));
13141
+ }
13142
+ if (setExitCode && checks.some((check) => !check.ok)) {
13143
+ process.exitCode = 1;
13144
+ }
13145
+ }
12859
13146
  async function pathExists(path) {
12860
13147
  try {
12861
13148
  await access2(path);
@@ -13046,8 +13333,8 @@ var LOOPBACK_TIMEOUT_MS = 10 * 60 * 1e3;
13046
13333
  async function startLoopbackServer(options) {
13047
13334
  const timeoutMs = options.timeoutMs ?? LOOPBACK_TIMEOUT_MS;
13048
13335
  let settle;
13049
- const wait = new Promise((resolve15) => {
13050
- settle = resolve15;
13336
+ const wait = new Promise((resolve17) => {
13337
+ settle = resolve17;
13051
13338
  });
13052
13339
  const server = http.createServer((request, response) => {
13053
13340
  const host = request.headers.host ?? "127.0.0.1";
@@ -13094,10 +13381,10 @@ async function startLoopbackServer(options) {
13094
13381
  settle?.({ ok: false, error: "server_error", description: error.message });
13095
13382
  settle = void 0;
13096
13383
  });
13097
- const port = await new Promise((resolve15, reject) => {
13384
+ const port = await new Promise((resolve17, reject) => {
13098
13385
  server.listen(0, "127.0.0.1", () => {
13099
13386
  const address = server.address();
13100
- if (address && typeof address === "object") resolve15(address.port);
13387
+ if (address && typeof address === "object") resolve17(address.port);
13101
13388
  else reject(new Error("loopback server did not bind a port"));
13102
13389
  });
13103
13390
  server.once("error", reject);
@@ -13116,9 +13403,9 @@ async function startLoopbackServer(options) {
13116
13403
  port,
13117
13404
  redirectUri: `http://127.0.0.1:${port}${LOOPBACK_PATH}`,
13118
13405
  wait,
13119
- close: () => new Promise((resolve15) => {
13406
+ close: () => new Promise((resolve17) => {
13120
13407
  clearTimeout(timer);
13121
- server.close(() => resolve15());
13408
+ server.close(() => resolve17());
13122
13409
  })
13123
13410
  };
13124
13411
  }
@@ -13232,6 +13519,22 @@ function errorMessage(error) {
13232
13519
  }
13233
13520
  return String(error);
13234
13521
  }
13522
+ async function readAccountPlan(credentials, fetchImpl = (input, init) => fetch(input, init)) {
13523
+ if (!credentials) return void 0;
13524
+ try {
13525
+ const response = await fetchImpl(modelsUrl(credentials.baseUrl), {
13526
+ headers: { authorization: `Bearer ${credentials.apiKey}`, accept: "application/json" },
13527
+ redirect: "error",
13528
+ signal: AbortSignal.timeout(2e3)
13529
+ });
13530
+ if (!response.ok) return void 0;
13531
+ const body = await response.json();
13532
+ const plans = { free: "Free", pro: "Pro", team: "Team", ultra: "Ultra" };
13533
+ return typeof body.account?.plan === "string" && Object.hasOwn(plans, body.account.plan) ? plans[body.account.plan] : void 0;
13534
+ } catch {
13535
+ return void 0;
13536
+ }
13537
+ }
13235
13538
 
13236
13539
  // src/auth/browser-login.ts
13237
13540
  var BROWSER_LOGIN_REQUEST_TIMEOUT_MS = 15e3;
@@ -13429,7 +13732,7 @@ function silenceTimeoutMs() {
13429
13732
  return Number.isFinite(override) && override > 0 ? override : DEFAULT_NON_TTY_SILENCE_TIMEOUT_MS;
13430
13733
  }
13431
13734
  function ask(rl, question, onSettle) {
13432
- return new Promise((resolve15) => {
13735
+ return new Promise((resolve17) => {
13433
13736
  let settled = false;
13434
13737
  let silenceTimer;
13435
13738
  const keepAwake = () => {
@@ -13443,7 +13746,7 @@ function ask(rl, question, onSettle) {
13443
13746
  stdin2.off("data", keepAwake);
13444
13747
  rl.close();
13445
13748
  onSettle?.();
13446
- resolve15(value.trim());
13749
+ resolve17(value.trim());
13447
13750
  };
13448
13751
  if (!stdin2.isTTY) {
13449
13752
  silenceTimer = setTimeout(() => finish(""), silenceTimeoutMs());
@@ -13795,9 +14098,9 @@ var JsonRpcEndpoint = class {
13795
14098
  if (signal?.aborted) {
13796
14099
  return Promise.reject(new JsonRpcRequestAbortedError(method, void 0, false));
13797
14100
  }
13798
- return new Promise((resolve15, reject) => {
14101
+ return new Promise((resolve17, reject) => {
13799
14102
  const limit = timeoutMs ?? this.defaultTimeoutMs;
13800
- const entry = { method, resolve: resolve15, reject, signal, dispatched: false };
14103
+ const entry = { method, resolve: resolve17, reject, signal, dispatched: false };
13801
14104
  if (signal) {
13802
14105
  entry.abortHandler = () => {
13803
14106
  this.settle(id, () => reject(new JsonRpcRequestAbortedError(method, id, entry.dispatched)));
@@ -14480,13 +14783,13 @@ function renderMcpStatuses(statuses) {
14480
14783
  }).join("\n");
14481
14784
  }
14482
14785
  function withTimeout(promise, ms, message) {
14483
- return new Promise((resolve15, reject) => {
14786
+ return new Promise((resolve17, reject) => {
14484
14787
  const timer = setTimeout(() => reject(new Error(`${message} within ${ms}ms.`)), ms);
14485
14788
  timer.unref?.();
14486
14789
  promise.then(
14487
14790
  (value) => {
14488
14791
  clearTimeout(timer);
14489
- resolve15(value);
14792
+ resolve17(value);
14490
14793
  },
14491
14794
  (error) => {
14492
14795
  clearTimeout(timer);
@@ -14885,14 +15188,14 @@ function redact(secret) {
14885
15188
  // src/notify/channel.ts
14886
15189
  function sleep(ms, signal) {
14887
15190
  if (signal?.aborted) return Promise.resolve(false);
14888
- return new Promise((resolve15) => {
15191
+ return new Promise((resolve17) => {
14889
15192
  const timer = setTimeout(() => {
14890
15193
  signal?.removeEventListener("abort", onAbort);
14891
- resolve15(true);
15194
+ resolve17(true);
14892
15195
  }, ms);
14893
15196
  const onAbort = () => {
14894
15197
  clearTimeout(timer);
14895
- resolve15(false);
15198
+ resolve17(false);
14896
15199
  };
14897
15200
  signal?.addEventListener("abort", onAbort, { once: true });
14898
15201
  });
@@ -15140,7 +15443,7 @@ ${question}${optionLines}`, signal);
15140
15443
  await Promise.allSettled(this.channels.map((channel) => channel.call?.(spoken, wait.signal)));
15141
15444
  })();
15142
15445
  const timeout = sleep(this.timing.timeoutMs, wait.signal).then(() => null);
15143
- const cancelled = new Promise((resolve15) => wait.signal.addEventListener("abort", () => resolve15(null), { once: true }));
15446
+ const cancelled = new Promise((resolve17) => wait.signal.addEventListener("abort", () => resolve17(null), { once: true }));
15144
15447
  const reply = await Promise.race([...replies, timeout, cancelled]);
15145
15448
  return reply === null ? null : pickOption(reply, options);
15146
15449
  } finally {
@@ -15277,7 +15580,7 @@ function registerNotifyCommand(program2) {
15277
15580
 
15278
15581
  // src/commands/repl.ts
15279
15582
  import { createInterface as createInterface3, emitKeypressEvents } from "readline";
15280
- import { stdin as stdin5, stdout as stdout5 } from "process";
15583
+ import { stdin as stdin5, stdout as stdout5, stderr } from "process";
15281
15584
  import chalk11 from "chalk";
15282
15585
 
15283
15586
  // src/config/budget.ts
@@ -15451,6 +15754,80 @@ function registerTrustCommand(program2) {
15451
15754
  });
15452
15755
  }
15453
15756
 
15757
+ // src/core/image-attachments.ts
15758
+ import { constants as constants6 } from "fs";
15759
+ import { open as open9 } from "fs/promises";
15760
+ import { homedir as homedir2 } from "os";
15761
+ import { isAbsolute as isAbsolute4 } from "path";
15762
+ import { fileURLToPath as fileURLToPath2 } from "url";
15763
+ import sharp from "sharp";
15764
+ var MAX_SOURCE_BYTES = 20 * 1024 * 1024;
15765
+ function imageDrops(text) {
15766
+ const drops = [];
15767
+ const tokens = /"(?:\\.|[^"\\])*"|'[^']*'|(?:\\.|[^\s])+/g;
15768
+ for (const match of text.matchAll(tokens)) {
15769
+ const token = match[0];
15770
+ let path = token.startsWith("'") ? token.slice(1, -1) : token.startsWith('"') ? token.slice(1, -1).replace(/\\([\\"$`])/g, "$1") : token.replace(/\\(.)/g, "$1");
15771
+ if (path.startsWith("file://")) {
15772
+ try {
15773
+ path = fileURLToPath2(path);
15774
+ } catch {
15775
+ continue;
15776
+ }
15777
+ }
15778
+ if (path.startsWith("~/")) path = `${homedir2()}/${path.slice(2)}`;
15779
+ if (!isAbsolute4(path) || !/\.(png|jpe?g|webp)$/i.test(path)) continue;
15780
+ drops.push({ start: match.index, end: match.index + token.length, path, label: `[Image ${drops.length + 1}]` });
15781
+ }
15782
+ return drops;
15783
+ }
15784
+ function formatImageInput(text, cursor = text.length) {
15785
+ let display = "";
15786
+ let previous = 0;
15787
+ let shift = 0;
15788
+ let adjusted = cursor;
15789
+ for (const drop of imageDrops(text)) {
15790
+ display += text.slice(previous, drop.start) + drop.label;
15791
+ if (cursor >= drop.end) adjusted += drop.label.length - (drop.end - drop.start);
15792
+ else if (cursor > drop.start) adjusted = drop.start + shift + Math.min(drop.label.length, cursor - drop.start);
15793
+ shift += drop.label.length - (drop.end - drop.start);
15794
+ previous = drop.end;
15795
+ }
15796
+ return { text: display + text.slice(previous), cursor: Math.max(0, adjusted) };
15797
+ }
15798
+ function hasDroppedImage(text) {
15799
+ return imageDrops(text).length > 0;
15800
+ }
15801
+ async function prepareImageInput(text) {
15802
+ const drops = imageDrops(text);
15803
+ if (!drops.length) return { text };
15804
+ if (drops.length > 1) throw new Error("Attach one image per message. Cosmic currently accepts one image in its active context.");
15805
+ const drop = drops[0];
15806
+ const file = await open9(drop.path, constants6.O_RDONLY | constants6.O_NOFOLLOW | constants6.O_NONBLOCK);
15807
+ let bytes;
15808
+ try {
15809
+ const stat11 = await file.stat();
15810
+ if (!stat11.isFile() || stat11.size > MAX_SOURCE_BYTES) throw new Error("Image must be a regular file no larger than 20 MiB.");
15811
+ const buffer = Buffer.alloc(Math.min(stat11.size, MAX_SOURCE_BYTES) + 1);
15812
+ let used = 0;
15813
+ while (used < buffer.length) {
15814
+ const read = await file.read(buffer, used, buffer.length - used, null);
15815
+ if (!read.bytesRead) break;
15816
+ used += read.bytesRead;
15817
+ }
15818
+ if (used > MAX_SOURCE_BYTES) throw new Error("Image exceeds 20 MiB.");
15819
+ bytes = buffer.subarray(0, used);
15820
+ } finally {
15821
+ await file.close();
15822
+ }
15823
+ const image = sharp(bytes, { limitInputPixels: 4e7, animated: false });
15824
+ const metadata = await image.metadata();
15825
+ if (!["png", "jpeg", "webp"].includes(metadata.format ?? "") || (metadata.pages ?? 1) > 1) throw new Error("Attach a single PNG, JPEG or WebP image.");
15826
+ const normalized = await image.rotate().resize({ width: 1024, height: 1024, fit: "inside", withoutEnlargement: true }).png().toBuffer();
15827
+ if (normalized.length > 5 * 1024 * 1024) throw new Error("Normalized image exceeds the upload limit.");
15828
+ return { text: formatImageInput(text).text, image: { url: `data:image/png;base64,${normalized.toString("base64")}` } };
15829
+ }
15830
+
15454
15831
  // src/core/manual-compaction.ts
15455
15832
  async function compactManually(messages, pinned, provider, model, focus = "", signal) {
15456
15833
  const retained = new Set(pinned);
@@ -15728,7 +16105,8 @@ Acknowledge that you have the context and wait for the user's first request.`
15728
16105
  async ask(input, handlers = {}) {
15729
16106
  if (!this.started) await this.start();
15730
16107
  if (!this.firstTask) this.firstTask = input;
15731
- const asked = { role: "user", content: input };
16108
+ if (handlers.image) for (const message of this.messages) delete message.image;
16109
+ const asked = { role: "user", content: input, ...handlers.image ? { image: handlers.image } : {} };
15732
16110
  this.messages.push(asked);
15733
16111
  this.pinnedMessages.push(asked);
15734
16112
  this.userConstraints.push({ kind: "message", content: input });
@@ -16044,7 +16422,7 @@ Acknowledge that you have the context and wait for the user's first request.`
16044
16422
  constraintVersion: 1,
16045
16423
  userConstraints: [...this.userConstraints],
16046
16424
  mcpResultArchiveVersion: 1,
16047
- ...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "text", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
16425
+ ...this.messages.some((message) => isCompactionSummary(message) || message.image) ? { contextSnapshot: { version: 1, kind: "text", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
16048
16426
  // How the last turn ended, so a saved interactive transcript says whether
16049
16427
  // its final message is an answer or the place it ran out of room - the
16050
16428
  // same question `ultrac run` already answers for a one-shot run.
@@ -16259,7 +16637,10 @@ ${context}`
16259
16637
  async ask(input, handlers = {}) {
16260
16638
  if (!this.started) await this.start();
16261
16639
  if (!this.firstTask) this.firstTask = input;
16262
- const asked = { role: "user", content: input };
16640
+ if (handlers.image) {
16641
+ for (const message of this.messages) if (message.role === "user" || message.role === "system") delete message.image;
16642
+ }
16643
+ const asked = { role: "user", content: input, ...handlers.image ? { image: handlers.image } : {} };
16263
16644
  this.messages.push(asked);
16264
16645
  this.pinnedMessages.push(asked);
16265
16646
  this.userConstraints.push({ kind: "message", content: input });
@@ -16609,7 +16990,7 @@ ${guidance.join("\n")}` : ""}`;
16609
16990
  constraintVersion: 1,
16610
16991
  userConstraints: [...this.userConstraints],
16611
16992
  mcpResultArchiveVersion: 1,
16612
- ...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "native", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
16993
+ ...this.messages.some((message) => isCompactionSummary(message) || (message.role === "user" || message.role === "system") && message.image) ? { contextSnapshot: { version: 1, kind: "native", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
16613
16994
  // How the last turn ended, so a saved interactive transcript says whether
16614
16995
  // its final message is an answer or the place it ran out of room - the
16615
16996
  // same question `ultrac run` already answers for a one-shot run.
@@ -16633,6 +17014,152 @@ function noteNativeCompactionFallback(reason) {
16633
17014
  );
16634
17015
  }
16635
17016
 
17017
+ // src/core/autonomous.ts
17018
+ import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
17019
+ var AUTONOMOUS_CONTINUE_TASK = "Continue the task from where the previous round stopped. Check what is already done, finish the rest, and verify it.";
17020
+ var DEFAULT_AUTONOMOUS_MAX_ROUNDS = 5;
17021
+ var AUTONOMOUS_MAX_STEPS = 200;
17022
+ var DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS = 6 * 60 * 60 * 1e3;
17023
+ var DEFAULT_AUTONOMOUS_BASE_DELAY_MS = 1e3;
17024
+ var DEFAULT_AUTONOMOUS_MAX_DELAY_MS = 3e4;
17025
+ var DEFAULT_AUTONOMOUS_STALL_ROUNDS = 3;
17026
+ function shouldResume(stopReason) {
17027
+ return stopReason === "max-steps" || stopReason === "error";
17028
+ }
17029
+ function boundedInteger(value, name, min, max) {
17030
+ if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${name} must be an integer from ${min} to ${max}.`);
17031
+ return value;
17032
+ }
17033
+ function abortableSleep(milliseconds, signal) {
17034
+ signal.throwIfAborted();
17035
+ return new Promise((resolve17, reject) => {
17036
+ const timer = setTimeout(() => {
17037
+ signal.removeEventListener("abort", abort);
17038
+ resolve17();
17039
+ }, milliseconds);
17040
+ const abort = () => {
17041
+ clearTimeout(timer);
17042
+ signal.removeEventListener("abort", abort);
17043
+ reject(signal.reason);
17044
+ };
17045
+ signal.addEventListener("abort", abort, { once: true });
17046
+ if (signal.aborted) abort();
17047
+ });
17048
+ }
17049
+ function progress(session, startStep = 0) {
17050
+ const hash = createHash7("sha256");
17051
+ let completedTools = 0;
17052
+ hash.update(session.finalMessage ?? "");
17053
+ for (const step of (session.steps ?? []).slice(startStep)) {
17054
+ hash.update(step.modelText ?? "");
17055
+ for (const tool of step.toolResults ?? []) {
17056
+ completedTools += 1;
17057
+ hash.update(tool.call.tool);
17058
+ hash.update(tool.result.ok ? "ok" : "failed");
17059
+ hash.update(tool.result.content ?? "");
17060
+ try {
17061
+ hash.update(JSON.stringify(tool.call.input));
17062
+ } catch {
17063
+ hash.update("unserializable-input");
17064
+ }
17065
+ }
17066
+ }
17067
+ return { fingerprint: hash.digest("hex"), completedTools };
17068
+ }
17069
+ async function runAutonomousLoop(options) {
17070
+ boundedInteger(options.maxRounds, "maxRounds", 1, 1e3);
17071
+ const maxElapsedMs = boundedInteger(options.maxElapsedMs ?? DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS, "maxElapsedMs", 1, 7 * 24 * 60 * 60 * 1e3);
17072
+ const baseDelay = boundedInteger(options.baseDelayMs ?? DEFAULT_AUTONOMOUS_BASE_DELAY_MS, "baseDelayMs", 0, 3e5);
17073
+ const maxDelay = boundedInteger(options.maxDelayMs ?? DEFAULT_AUTONOMOUS_MAX_DELAY_MS, "maxDelayMs", baseDelay, 3e5);
17074
+ const stallLimit = boundedInteger(options.stallRounds ?? DEFAULT_AUTONOMOUS_STALL_ROUNDS, "stallRounds", 1, 1e3);
17075
+ const now = options.now ?? (() => performance.now());
17076
+ const started = now();
17077
+ const elapsed = () => Math.max(0, now() - started);
17078
+ const budget = new AbortController();
17079
+ const deadline = setTimeout(() => budget.abort(new DOMException("Autonomous elapsed budget reached.", "TimeoutError")), maxElapsedMs);
17080
+ deadline.unref?.();
17081
+ const signal = options.signal ? AbortSignal.any([options.signal, budget.signal]) : budget.signal;
17082
+ const sleep2 = options.sleep ?? abortableSleep;
17083
+ const id = randomUUID5();
17084
+ let previous;
17085
+ let lastFingerprint;
17086
+ let lastSessionId;
17087
+ let lastStepCount = 0;
17088
+ let stalls = 0;
17089
+ let consecutiveErrors = 0;
17090
+ try {
17091
+ signal.throwIfAborted();
17092
+ for (let round = 1; ; round += 1) {
17093
+ const roundStart = now();
17094
+ diagnosticEvent("round.start", { id, round, maxRounds: options.maxRounds, elapsedMs: elapsed() });
17095
+ let session;
17096
+ try {
17097
+ session = await options.runRound(round, previous, signal);
17098
+ } catch (error) {
17099
+ diagnosticEvent("round.end", { id, round, reason: signal.aborted ? "aborted" : "exception", elapsedMs: elapsed(), durationMs: Math.max(0, now() - roundStart) });
17100
+ throw error;
17101
+ }
17102
+ const stepCount = session.steps?.length ?? 0;
17103
+ const startStep = session.id === lastSessionId && stepCount > lastStepCount ? lastStepCount : 0;
17104
+ const current = progress(session, startStep);
17105
+ lastSessionId = session.id;
17106
+ lastStepCount = stepCount;
17107
+ const unchanged = lastFingerprint !== void 0 && current.fingerprint === lastFingerprint;
17108
+ stalls = unchanged ? stalls + 1 : 0;
17109
+ lastFingerprint = current.fingerprint;
17110
+ const classification = session.stopReason === "error" ? options.classifyFailure?.(session) : void 0;
17111
+ consecutiveErrors = session.stopReason === "error" ? consecutiveErrors + 1 : 0;
17112
+ let haltReason;
17113
+ if (session.stopReason === "completed") haltReason = "completed";
17114
+ else if (budget.signal.aborted || elapsed() >= maxElapsedMs) haltReason = "elapsed_budget";
17115
+ else if (signal.aborted || session.stopReason === "aborted") haltReason = "aborted";
17116
+ else if (classification && !classification.retryable) haltReason = "permanent_error";
17117
+ else if (!shouldResume(session.stopReason)) haltReason = "stopped";
17118
+ else if (stalls >= stallLimit) haltReason = "stalled";
17119
+ else if (round >= options.maxRounds) haltReason = "max_rounds";
17120
+ const retryDelay = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
17121
+ if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay >= maxElapsedMs) haltReason = "elapsed_budget";
17122
+ diagnosticEvent("round.end", {
17123
+ id,
17124
+ round,
17125
+ sessionId: session.id,
17126
+ reason: haltReason ?? session.stopReason,
17127
+ failureKind: classification?.kind,
17128
+ attempt: consecutiveErrors,
17129
+ completedTools: current.completedTools,
17130
+ elapsedMs: elapsed(),
17131
+ durationMs: Math.max(0, now() - roundStart)
17132
+ });
17133
+ if (haltReason === "stalled") diagnosticEvent("round.stall", { id, round, repeats: stalls, completedTools: current.completedTools, elapsedMs: elapsed() });
17134
+ await options.onRoundEnd?.(round, session, !haltReason);
17135
+ if (haltReason) return { session, rounds: round, haltReason };
17136
+ if (session.stopReason === "error") {
17137
+ const delayMs = retryDelay;
17138
+ if (elapsed() + delayMs >= maxElapsedMs) return { session, rounds: round, haltReason: "elapsed_budget" };
17139
+ diagnosticEvent("round.retry", {
17140
+ id,
17141
+ round,
17142
+ nextRound: round + 1,
17143
+ attempt: consecutiveErrors,
17144
+ failureKind: classification?.kind ?? "unclassified",
17145
+ delayMs,
17146
+ elapsedMs: elapsed()
17147
+ });
17148
+ try {
17149
+ await sleep2(delayMs, signal);
17150
+ } catch (error) {
17151
+ if (!signal.aborted) throw error;
17152
+ return { session, rounds: round, haltReason: budget.signal.aborted ? "elapsed_budget" : "aborted" };
17153
+ }
17154
+ }
17155
+ if (signal.aborted || elapsed() >= maxElapsedMs) return { session, rounds: round, haltReason: budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted" };
17156
+ previous = session;
17157
+ }
17158
+ } finally {
17159
+ clearTimeout(deadline);
17160
+ }
17161
+ }
17162
+
16636
17163
  // src/commands/session-checkpoint.ts
16637
17164
  var SESSION_CHECKPOINT_FAILURE_WARNING = "Warning: the latest session checkpoint could not be saved; progress since the previous checkpoint may be lost.";
16638
17165
  function createSessionCheckpointWriter(workspace, onFailure) {
@@ -16690,7 +17217,7 @@ function printSession(session, sessionPath2, options = {}) {
16690
17217
  }
16691
17218
  for (const toolResult of step.toolResults) {
16692
17219
  const status = toolResult.result.ok ? chalk8.green("ok") : chalk8.red("failed");
16693
- console.log(chalk8.cyan(toolResult.call.tool), status);
17220
+ console.log(chalk8.white(toolResult.call.tool), status);
16694
17221
  console.log(chalk8.gray(indent2(formatToolOutput(toolResult.result.content, { verbose: options.verboseTools, failed: !toolResult.result.ok, hint: "Use --verbose-tools or inspect the saved session for full output." }))));
16695
17222
  }
16696
17223
  }
@@ -16710,13 +17237,13 @@ function printTools(tools) {
16710
17237
  for (const tool of tools) {
16711
17238
  const permissionText = tool.permissions.length ? chalk8.yellow(` [${tool.permissions.join(", ")}]`) : "";
16712
17239
  const optionalText = tool.optionalPermissions.length ? chalk8.gray(` [optional: ${tool.optionalPermissions.join(", ")}]`) : "";
16713
- console.log(`${chalk8.cyan(tool.name)}${permissionText}${optionalText}`);
17240
+ console.log(`${chalk8.white(tool.name)}${permissionText}${optionalText}`);
16714
17241
  console.log(chalk8.gray(indent2(tool.description)));
16715
17242
  }
16716
17243
  }
16717
17244
  function printToolResult(result, options = {}) {
16718
17245
  const status = result.ok ? chalk8.green("ok") : chalk8.red("failed");
16719
- console.log(`${chalk8.cyan(result.tool)} ${status}`);
17246
+ console.log(`${chalk8.white(result.tool)} ${status}`);
16720
17247
  console.log(chalk8.gray(indent2(formatToolOutput(result.content, { verbose: options.verboseTools, failed: !result.ok, hint: "Use --verbose-tools or --result-json for full output." }))));
16721
17248
  }
16722
17249
  function indent2(value) {
@@ -16734,222 +17261,21 @@ function formatToolOutput(content, options = {}) {
16734
17261
  \u2026 ${hidden ? `${hidden} more lines` : "Long lines shortened"}. ${options.hint ?? "Full output remains available in the saved session."}`;
16735
17262
  }
16736
17263
 
16737
- // src/skills/commands.ts
16738
- var BUILTIN_SLASH_COMMANDS = [
16739
- "logs",
16740
- "ghost",
16741
- "output",
16742
- "usage",
16743
- "max-steps",
16744
- "resume",
16745
- "deny",
16746
- "plan",
16747
- "review",
16748
- "coding",
16749
- "status",
16750
- "context",
16751
- "compact",
16752
- "diff",
16753
- "sessions",
16754
- "save",
16755
- "test",
16756
- "init",
16757
- "new",
16758
- "allow",
16759
- "clear",
16760
- "exit",
16761
- "help",
16762
- "memory",
16763
- "mode",
16764
- "model",
16765
- "permissions",
16766
- "quit",
16767
- "skills",
16768
- "tools",
16769
- "whoami"
16770
- ];
16771
- var COMMAND_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
16772
- var MAX_COMMAND_LENGTH = 64;
16773
- function parseSlashInput(input) {
16774
- const trimmed = input.trim();
16775
- if (!trimmed.startsWith("/")) return null;
16776
- const rest = trimmed.slice(1);
16777
- const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
16778
- if (!match) return null;
16779
- return { command: match[1], args: (match[2] ?? "").trim() };
16780
- }
16781
- function isBuiltinSlashCommand(command) {
16782
- return BUILTIN_SLASH_COMMANDS.includes(command.toLowerCase());
16783
- }
16784
- function normalizeCommandName(value) {
16785
- const candidate = value.trim().replace(/^\/+/, "").trim();
16786
- if (!candidate) {
16787
- throw new Error("Slash command name is empty; expected kebab-case, for example: open-pr.");
16788
- }
16789
- if (candidate.length > MAX_COMMAND_LENGTH) {
16790
- throw new Error(
16791
- `Slash command name "${candidate}" is longer than ${MAX_COMMAND_LENGTH} characters.`
16792
- );
16793
- }
16794
- if (!COMMAND_NAME_RE.test(candidate)) {
16795
- throw new Error(
16796
- `Invalid slash command name "${candidate}": use kebab-case - lowercase letters, digits, and single hyphens, no spaces or slashes (for example: open-pr).`
16797
- );
17264
+ // src/ui/progress.ts
17265
+ import { stdout as stdout3 } from "process";
17266
+ import chalk9 from "chalk";
17267
+ var PENDING_GLYPH = "\u25CB";
17268
+ var DONE_GLYPH = "\u25CF";
17269
+ var REPAINT_MS = 100;
17270
+ function formatTokens(tokens) {
17271
+ if (!Number.isFinite(tokens) || tokens <= 0) return "0";
17272
+ if (tokens < 1e3) return String(Math.floor(tokens));
17273
+ if (tokens < 1e6) {
17274
+ const tenths2 = Math.floor(tokens / 100);
17275
+ return `${Math.floor(tenths2 / 10)}.${tenths2 % 10}k`;
16798
17276
  }
16799
- return candidate;
16800
- }
16801
- async function collectEntries(workspace) {
16802
- const skills = await discoverSkills(workspace);
16803
- const claimed = /* @__PURE__ */ new Map();
16804
- const conflicts = [];
16805
- for (const skill of skills) {
16806
- let declaration;
16807
- try {
16808
- declaration = skill.command ?? null;
16809
- } catch (error) {
16810
- conflicts.push({
16811
- command: "",
16812
- skillName: skill.name,
16813
- scope: skill.scope,
16814
- path: skill.path,
16815
- reason: "invalid",
16816
- message: `Skill "${skill.name}" (${skill.scope}) was skipped: ${error instanceof Error ? error.message : String(error)}`
16817
- });
16818
- continue;
16819
- }
16820
- if (!declaration) continue;
16821
- let command;
16822
- try {
16823
- command = normalizeCommandName(declaration.raw);
16824
- } catch (error) {
16825
- conflicts.push({
16826
- command: declaration.raw,
16827
- skillName: skill.name,
16828
- scope: skill.scope,
16829
- path: skill.path,
16830
- reason: "invalid",
16831
- message: `Skill "${skill.name}" (${skill.scope}) declares a malformed command: ${error instanceof Error ? error.message : String(error)}`
16832
- });
16833
- continue;
16834
- }
16835
- if (isBuiltinSlashCommand(command)) {
16836
- conflicts.push({
16837
- command,
16838
- skillName: skill.name,
16839
- scope: skill.scope,
16840
- path: skill.path,
16841
- reason: "builtin",
16842
- message: `Skill "${skill.name}" (${skill.scope}) declares /${command}, which is a built-in command. Built-ins always win - rename the command: key in ${skill.path}.`
16843
- });
16844
- continue;
16845
- }
16846
- const entry = {
16847
- skill,
16848
- command: {
16849
- command,
16850
- skillName: skill.name,
16851
- description: skill.description,
16852
- argumentHint: declaration.argumentHint,
16853
- scope: skill.scope,
16854
- path: skill.path
16855
- }
16856
- };
16857
- const existing = claimed.get(command);
16858
- if (!existing) {
16859
- claimed.set(command, entry);
16860
- continue;
16861
- }
16862
- const winner = existing.skill.scope === "global" && skill.scope === "project" ? entry : existing;
16863
- const loser = winner === entry ? existing : entry;
16864
- claimed.set(command, winner);
16865
- conflicts.push({
16866
- command,
16867
- skillName: loser.skill.name,
16868
- scope: loser.skill.scope,
16869
- path: loser.skill.path,
16870
- reason: "duplicate",
16871
- message: `/${command} is declared by both "${winner.skill.name}" (${winner.skill.scope}) and "${loser.skill.name}" (${loser.skill.scope}). "${winner.skill.name}" wins; rename the command: key in ${loser.skill.path}.`
16872
- });
16873
- }
16874
- const entries = [...claimed.values()].sort((a, b) => a.command.command.localeCompare(b.command.command));
16875
- return { entries, conflicts };
16876
- }
16877
- async function listSkillCommands(workspace) {
16878
- const { entries, conflicts } = await collectEntries(workspace);
16879
- return { commands: entries.map((entry) => entry.command), conflicts };
16880
- }
16881
- function buildSkillCommandPrompt(skill, command, args) {
16882
- const argumentLine = command.argumentHint ? `Arguments (${command.argumentHint}): ${args || "(none provided)"}` : `Arguments: ${args || "(none provided)"}`;
16883
- const header = [
16884
- `The user ran the /${command.command} slash command, which runs the "${skill.name}" skill (${skill.scope}).`,
16885
- skill.description ? `Description: ${skill.description}` : void 0,
16886
- argumentLine,
16887
- skill.permissions.length ? `Permissions needed: ${skill.permissions.join(", ")}` : void 0,
16888
- "Follow the instructions below now, applying the arguments where they are relevant."
16889
- ].filter(Boolean).join("\n");
16890
- return `${header}
16891
-
16892
- --- instructions ---
16893
- ${skill.body}`;
16894
- }
16895
- async function resolveSkillCommand(workspace, input) {
16896
- const parsed = parseSlashInput(input);
16897
- if (!parsed) return { status: "not-a-command" };
16898
- const command = parsed.command.toLowerCase();
16899
- if (isBuiltinSlashCommand(command)) {
16900
- return { status: "builtin", command };
16901
- }
16902
- const { entries, conflicts } = await collectEntries(workspace);
16903
- const match = entries.find((entry) => entry.command.command === command);
16904
- if (!match) {
16905
- const related = conflicts.filter((conflict) => conflict.command === command);
16906
- if (related.length) {
16907
- return {
16908
- status: "conflict",
16909
- command,
16910
- message: related.map((conflict) => conflict.message).join("\n"),
16911
- conflicts: related
16912
- };
16913
- }
16914
- return {
16915
- status: "unknown",
16916
- command,
16917
- available: entries.map((entry) => entry.command.command)
16918
- };
16919
- }
16920
- return {
16921
- status: "ok",
16922
- command: match.command,
16923
- skill: match.skill,
16924
- args: parsed.args,
16925
- prompt: buildSkillCommandPrompt(match.skill, match.command, parsed.args)
16926
- };
16927
- }
16928
- function formatSkillCommandHelp(index) {
16929
- if (!index.commands.length) return "";
16930
- const lines = index.commands.map((command) => {
16931
- const invocation = command.argumentHint ? `/${command.command} ${command.argumentHint}` : `/${command.command}`;
16932
- const description = command.description || `Run the ${command.skillName} skill`;
16933
- return ` ${invocation.padEnd(22)}${description} (skill: ${command.skillName})`;
16934
- });
16935
- return ["Skill commands:", ...lines].join("\n");
16936
- }
16937
-
16938
- // src/ui/progress.ts
16939
- import { stdout as stdout3 } from "process";
16940
- import chalk9 from "chalk";
16941
- var PENDING_GLYPH = "\u25CB";
16942
- var DONE_GLYPH = "\u25CF";
16943
- var REPAINT_MS = 100;
16944
- function formatTokens(tokens) {
16945
- if (!Number.isFinite(tokens) || tokens <= 0) return "0";
16946
- if (tokens < 1e3) return String(Math.floor(tokens));
16947
- if (tokens < 1e6) {
16948
- const tenths2 = Math.floor(tokens / 100);
16949
- return `${Math.floor(tenths2 / 10)}.${tenths2 % 10}k`;
16950
- }
16951
- const tenths = Math.floor(tokens / 1e5);
16952
- return `${Math.floor(tenths / 10)}.${tenths % 10}M`;
17277
+ const tenths = Math.floor(tokens / 1e5);
17278
+ return `${Math.floor(tenths / 10)}.${tenths % 10}M`;
16953
17279
  }
16954
17280
  function formatElapsed(ms) {
16955
17281
  if (!Number.isFinite(ms) || ms <= 0) return "0s";
@@ -16964,12 +17290,12 @@ function formatElapsed(ms) {
16964
17290
  }
16965
17291
  function formatProgressLine(snapshot) {
16966
17292
  const finished = snapshot.done >= snapshot.total;
16967
- const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.cyan(PENDING_GLYPH);
17293
+ const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.white(PENDING_GLYPH);
16968
17294
  const noun = snapshot.total === 1 ? "agent" : "agents";
16969
17295
  const stats = [`${snapshot.done}/${snapshot.total} ${noun} done`, formatElapsed(snapshot.elapsedMs)];
16970
17296
  if (snapshot.tokens !== void 0) stats.push(`\u2193 ${formatTokens(snapshot.tokens)} tokens`);
16971
17297
  const rendered = stats.map((stat11) => chalk9.dim(stat11)).join(chalk9.dim(" \xB7 "));
16972
- return ` ${glyph} ${chalk9.cyan(snapshot.label)} ${snapshot.description} ${rendered}`;
17298
+ return ` ${glyph} ${chalk9.white(snapshot.label)} ${snapshot.description} ${rendered}`;
16973
17299
  }
16974
17300
  var ProgressLine = class {
16975
17301
  constructor(label, description, total) {
@@ -17095,53 +17421,8 @@ function attachProgressUi(options = {}) {
17095
17421
  };
17096
17422
  }
17097
17423
 
17098
- // src/ui/spinner.ts
17099
- import { stdout as stdout4 } from "process";
17100
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
17101
- var Spinner = class {
17102
- constructor(label = "thinking") {
17103
- this.label = label;
17104
- }
17105
- label;
17106
- timer;
17107
- index = 0;
17108
- holdWhile;
17109
- held = false;
17110
- start(holdWhile) {
17111
- this.holdWhile = holdWhile;
17112
- if (!stdout4.isTTY || this.timer) return;
17113
- this.render();
17114
- this.timer = setInterval(() => this.render(), 90);
17115
- }
17116
- update(label) {
17117
- this.label = label;
17118
- }
17119
- stop() {
17120
- if (this.timer) {
17121
- clearInterval(this.timer);
17122
- this.timer = void 0;
17123
- }
17124
- if (stdout4.isTTY) {
17125
- stdout4.clearLine(0);
17126
- stdout4.cursorTo(0);
17127
- }
17128
- }
17129
- render() {
17130
- if (this.holdWhile?.()) {
17131
- if (!this.held) {
17132
- stdout4.clearLine(0);
17133
- stdout4.cursorTo(0);
17134
- this.held = true;
17135
- }
17136
- return;
17137
- }
17138
- this.held = false;
17139
- this.index = (this.index + 1) % FRAMES.length;
17140
- stdout4.clearLine(0);
17141
- stdout4.cursorTo(0);
17142
- stdout4.write(` ${FRAMES[this.index]} ${this.label}\u2026`);
17143
- }
17144
- };
17424
+ // src/ui/activity.ts
17425
+ import { stripVTControlCharacters } from "util";
17145
17426
 
17146
17427
  // src/ui/tool-activity.ts
17147
17428
  var VERBS = {
@@ -17203,19 +17484,956 @@ function compact2(value, max = 64) {
17203
17484
  if (!value) return "";
17204
17485
  return truncate2(value.replace(/\s+/g, " "), max);
17205
17486
  }
17206
- function quote(value, max = 48) {
17207
- return value ? `\u201C${compact2(value, max)}\u201D` : "";
17487
+ function quote(value, max = 48) {
17488
+ return value ? `\u201C${compact2(value, max)}\u201D` : "";
17489
+ }
17490
+ function truncate2(value, max) {
17491
+ return value.length > max ? `${value.slice(0, Math.max(1, max - 1))}\u2026` : value;
17492
+ }
17493
+ function humanize(value) {
17494
+ const words = value.replace(/[_-]+/g, " ").trim();
17495
+ return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Tool";
17496
+ }
17497
+
17498
+ // src/ui/spinner.ts
17499
+ import { stdout as stdout4 } from "process";
17500
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
17501
+ var Spinner = class {
17502
+ constructor(label = "thinking") {
17503
+ this.label = label;
17504
+ }
17505
+ label;
17506
+ timer;
17507
+ index = 0;
17508
+ holdWhile;
17509
+ held = false;
17510
+ startedAt;
17511
+ start(holdWhile, startedAt = performance.now()) {
17512
+ this.holdWhile = holdWhile;
17513
+ if (this.startedAt !== void 0) return;
17514
+ this.startedAt = startedAt;
17515
+ this.held = false;
17516
+ if (!stdout4.isTTY) return;
17517
+ this.render();
17518
+ this.timer = setInterval(() => this.render(), 90);
17519
+ }
17520
+ update(label) {
17521
+ this.label = label;
17522
+ }
17523
+ stop(completion) {
17524
+ const elapsed = this.startedAt === void 0 ? void 0 : performance.now() - this.startedAt;
17525
+ this.startedAt = void 0;
17526
+ if (this.timer) {
17527
+ clearInterval(this.timer);
17528
+ this.timer = void 0;
17529
+ }
17530
+ if (stdout4.isTTY && elapsed !== void 0) {
17531
+ stdout4.clearLine(0);
17532
+ stdout4.cursorTo(0);
17533
+ if (completion && elapsed >= 1e3) stdout4.write(` ${completion} ${formatSeconds(elapsed)}.
17534
+ `);
17535
+ }
17536
+ }
17537
+ render() {
17538
+ if (this.holdWhile?.()) {
17539
+ if (!this.held) {
17540
+ stdout4.clearLine(0);
17541
+ stdout4.cursorTo(0);
17542
+ this.held = true;
17543
+ }
17544
+ return;
17545
+ }
17546
+ this.held = false;
17547
+ this.index = (this.index + 1) % FRAMES.length;
17548
+ stdout4.clearLine(0);
17549
+ stdout4.cursorTo(0);
17550
+ const milliseconds = performance.now() - (this.startedAt ?? performance.now());
17551
+ const elapsed = milliseconds >= 1e3 ? formatElapsed(milliseconds) : "";
17552
+ const limit = Math.max(0, (stdout4.columns ?? 80) - elapsed.length - 10);
17553
+ let label = "";
17554
+ let cells2 = 0;
17555
+ for (const character of this.label.replace(/[\x00-\x1f\x7f]/g, " ")) {
17556
+ const size = character.codePointAt(0) > 127 ? 2 : 1;
17557
+ if (cells2 + size > limit) break;
17558
+ label += character;
17559
+ cells2 += size;
17560
+ }
17561
+ stdout4.write(` ${FRAMES[this.index]} ${label}\u2026${elapsed ? ` \xB7 ${elapsed}` : ""}`);
17562
+ }
17563
+ };
17564
+ function formatSeconds(milliseconds) {
17565
+ const seconds = Math.max(0, milliseconds) / 1e3;
17566
+ const count3 = seconds.toFixed(1).replace(/\.0$/, "");
17567
+ return `${count3} ${count3 === "1" ? "second" : "seconds"}`;
17568
+ }
17569
+
17570
+ // src/ui/activity.ts
17571
+ var sink;
17572
+ function setActivitySink(value) {
17573
+ sink = value;
17574
+ }
17575
+ function getActivitySink() {
17576
+ return sink;
17577
+ }
17578
+ var clean = (text) => stripVTControlCharacters(text).replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
17579
+ function activityEntry(call, result, durationMs) {
17580
+ const fields = ["command", "script", "path", "paths", "query", "pattern", "url", "cwd"];
17581
+ const request = fields.flatMap((key) => {
17582
+ const value = call.input[key];
17583
+ return typeof value === "string" ? [`${key}: ${value}`] : Array.isArray(value) && value.every((item) => typeof item === "string") ? [`${key}: ${value.join(", ")}`] : [];
17584
+ }).join(" \xB7 ");
17585
+ return {
17586
+ label: clean(toolActivityLabel(call)),
17587
+ request: clean(request).slice(0, 4e3),
17588
+ output: clean(result.content).slice(0, 1e3),
17589
+ ok: result.ok,
17590
+ durationMs
17591
+ };
17592
+ }
17593
+ function activityRows(snapshot, expanded, selected, end) {
17594
+ const failures = snapshot.entries.filter((entry) => !entry.ok).length;
17595
+ const elapsed = snapshot.thinkingMs >= 1e3 ? ` \xB7 Thought for ${formatSeconds(snapshot.thinkingMs)}` : "";
17596
+ const summary = `${expanded ? "\u25BE" : "\u25B8"} \u25CF ${snapshot.entries.length} actions${failures ? ` \xB7 ${failures} failed` : ""}${snapshot.finished ? elapsed : ""} \xB7 Ctrl+O details`;
17597
+ const rows = [{ text: summary, ok: failures || snapshot.failed ? false : snapshot.finished ? true : void 0 }];
17598
+ if (!expanded) return rows;
17599
+ const last = Math.min(snapshot.entries.length, end);
17600
+ for (let index = Math.max(0, last - 5); index < last; index += 1) {
17601
+ const entry = snapshot.entries[index];
17602
+ const duration = entry.durationMs >= 1e3 ? ` \xB7 Worked for ${formatSeconds(entry.durationMs)}` : "";
17603
+ rows.push({ text: ` ${selected === index ? "\u25BE" : "\u25B8"} \u25CF ${entry.label}${duration}`, ok: entry.ok, entry: index });
17604
+ if (selected === index) {
17605
+ if (entry.request) rows.push({ text: ` ${entry.request}`, entry: index });
17606
+ if (entry.output) rows.push({ text: ` ${entry.output}`, ok: entry.ok, entry: index });
17607
+ }
17608
+ }
17609
+ if (snapshot.entries.length > 5) rows.push({ text: " Scroll to browse actions \xB7 /output for full saved results" });
17610
+ return rows;
17611
+ }
17612
+
17613
+ // src/ui/interactive-choice.ts
17614
+ var MOUSE_ON = "\x1B[?1006h\x1B[?1000h";
17615
+ var MOUSE_OFF = "\x1B[?1000l\x1B[?1006l";
17616
+ function displayLabel(value, width2) {
17617
+ const clean2 = value.replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|.)/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
17618
+ let result = "";
17619
+ let used = 0;
17620
+ for (const character of clean2) {
17621
+ const size = /[\p{Mark}\u200d\ufe0e\ufe0f]/u.test(character) ? 0 : character.codePointAt(0) < 128 ? 1 : 2;
17622
+ if (used + size > width2 - 1) return `${result}\u2026`;
17623
+ result += character;
17624
+ used += size;
17625
+ }
17626
+ return result;
17627
+ }
17628
+ function attachInteractiveChoices(readline, options, settings = {}) {
17629
+ const rl = readline;
17630
+ const output = settings.output ?? rl.output;
17631
+ const prompt = settings.prompt ?? " \u203A ";
17632
+ let resolveReady;
17633
+ const ready = new Promise((resolve17) => {
17634
+ resolveReady = resolve17;
17635
+ });
17636
+ const original = rl._ttyWrite;
17637
+ const priorPrompt = rl.getPrompt();
17638
+ const priorLine = rl.line;
17639
+ const priorCursor = rl.cursor;
17640
+ const priorHistory = rl.history?.slice();
17641
+ let injected = false;
17642
+ let selected = 0;
17643
+ let disposed = false;
17644
+ let mouse = false;
17645
+ let optionRow = null;
17646
+ let reportPending = false;
17647
+ let reportTimer;
17648
+ let sequenceTimer;
17649
+ let sequence = "";
17650
+ const maxRow = () => settings.maxRow?.() ?? output?.rows ?? 1e4;
17651
+ const columns = () => Math.max(16, output?.columns ?? 80);
17652
+ const write = (text) => {
17653
+ output?.write(text);
17654
+ };
17655
+ const labels = () => options.map((option, index) => {
17656
+ const prefix = ` ${selected === index ? "\u203A" : " "} ${index + 1}. `;
17657
+ const text = `${prefix}${displayLabel(option, columns() - 9)}`;
17658
+ return selected === index ? `\x1B[7m${text}\x1B[0m` : text;
17659
+ });
17660
+ const restoreLine = () => {
17661
+ if (!original || !rl.terminal || rl.closed) return;
17662
+ if (rl.line) {
17663
+ original.call(rl, void 0, { name: "end" });
17664
+ original.call(rl, void 0, { name: "u", ctrl: true });
17665
+ }
17666
+ if (priorLine) {
17667
+ original.call(rl, priorLine, {});
17668
+ for (const _character of Array.from(priorLine.slice(priorCursor))) original.call(rl, void 0, { name: "left" });
17669
+ }
17670
+ };
17671
+ const cleanup = () => {
17672
+ if (disposed) return;
17673
+ disposed = true;
17674
+ clearTimeout(reportTimer);
17675
+ clearTimeout(sequenceTimer);
17676
+ if (rl._ttyWrite === intercept) rl._ttyWrite = original;
17677
+ rl.off("line", onLine);
17678
+ rl.off("close", cleanup);
17679
+ output?.off("resize", onResize);
17680
+ output?.off("error", cleanup);
17681
+ settings.signal?.removeEventListener("abort", cleanup);
17682
+ try {
17683
+ if (mouse) write(MOUSE_OFF);
17684
+ } catch {
17685
+ }
17686
+ mouse = false;
17687
+ rl.setPrompt(priorPrompt);
17688
+ if (injected && priorHistory && rl.history) rl.history.splice(0, rl.history.length, ...priorHistory);
17689
+ try {
17690
+ restoreLine();
17691
+ } catch {
17692
+ }
17693
+ resolveReady(false);
17694
+ };
17695
+ function onLine() {
17696
+ cleanup();
17697
+ }
17698
+ function redraw() {
17699
+ if (disposed || rl.line || options.length + 2 > maxRow()) return;
17700
+ write("\x1B7");
17701
+ if (optionRow !== null) write(`\x1B[${optionRow};1H`);
17702
+ else write(`\x1B[${options.length + 1}A\r`);
17703
+ for (const label of labels()) write(`\x1B[2K${label}
17704
+ \r`);
17705
+ write("\x1B8");
17706
+ }
17707
+ function choose(index) {
17708
+ if (disposed || !original || index < 0 || index >= options.length) return;
17709
+ selected = index;
17710
+ injected = true;
17711
+ if (rl.line) {
17712
+ original.call(rl, void 0, { name: "end" });
17713
+ original.call(rl, void 0, { name: "u", ctrl: true });
17714
+ }
17715
+ original.call(rl, String(index + 1), {});
17716
+ original.call(rl, "\r", { name: "return", sequence: "\r" });
17717
+ }
17718
+ function requestPosition() {
17719
+ if (disposed) return;
17720
+ optionRow = null;
17721
+ reportPending = true;
17722
+ clearTimeout(reportTimer);
17723
+ write("\x1B[6n");
17724
+ reportTimer = setTimeout(() => {
17725
+ reportPending = false;
17726
+ resolveReady(false);
17727
+ }, Math.max(1, settings.cursorReportTimeoutMs ?? 250));
17728
+ }
17729
+ function onResize() {
17730
+ if (disposed) return;
17731
+ if (mouse) write(MOUSE_OFF);
17732
+ mouse = false;
17733
+ optionRow = null;
17734
+ requestPosition();
17735
+ }
17736
+ function consumeControl(value) {
17737
+ const position = /^\x1b\[(\d+);(\d+)R$/.exec(value);
17738
+ if (position) {
17739
+ const row = Number(position[1]);
17740
+ const column = Number(position[2]);
17741
+ if (reportPending && row > options.length + 1 && row <= maxRow() && column >= 1 && column <= columns()) {
17742
+ optionRow = row - options.length - 1;
17743
+ reportPending = false;
17744
+ clearTimeout(reportTimer);
17745
+ write(MOUSE_ON);
17746
+ mouse = true;
17747
+ resolveReady(true);
17748
+ }
17749
+ return true;
17750
+ }
17751
+ const click = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(value);
17752
+ if (click) {
17753
+ const button = Number(click[1]);
17754
+ const x = Number(click[2]);
17755
+ const y = Number(click[3]);
17756
+ if (mouse && optionRow !== null && click[4] === "M" && button < 32 && (button & 3) === 0 && x >= 1 && x <= columns() && y <= maxRow()) choose(y - optionRow);
17757
+ return true;
17758
+ }
17759
+ return false;
17760
+ }
17761
+ function intercept(text, key) {
17762
+ if (disposed) {
17763
+ original?.call(rl, text, key);
17764
+ return;
17765
+ }
17766
+ const value = key?.sequence ?? text ?? "";
17767
+ if (sequence || value.startsWith("\x1B[<") || /^\x1b\[\d/.test(value)) {
17768
+ sequence += value;
17769
+ clearTimeout(sequenceTimer);
17770
+ if (consumeControl(sequence)) {
17771
+ sequence = "";
17772
+ return;
17773
+ }
17774
+ if (/^\x1b\[(?:<[0-9;]*|[0-9;]*)$/.test(sequence) && sequence.length < 64) {
17775
+ sequenceTimer = setTimeout(() => {
17776
+ sequence = "";
17777
+ }, 250);
17778
+ return;
17779
+ }
17780
+ const unrecognized = sequence;
17781
+ sequence = "";
17782
+ original?.call(rl, unrecognized, key);
17783
+ return;
17784
+ }
17785
+ if (!rl.line && (key?.name === "up" || key?.name === "down")) {
17786
+ selected = (selected + (key.name === "down" ? 1 : options.length - 1)) % options.length;
17787
+ redraw();
17788
+ return;
17789
+ }
17790
+ if (!rl.line && (key?.name === "return" || key?.name === "enter")) {
17791
+ choose(selected);
17792
+ return;
17793
+ }
17794
+ if (key?.ctrl && key.name === "c") cleanup();
17795
+ if (key?.ctrl && key.name === "d" && !rl.line) {
17796
+ original?.call(rl, text, key);
17797
+ cleanup();
17798
+ return;
17799
+ }
17800
+ original?.call(rl, text, key);
17801
+ }
17802
+ if (settings.signal?.aborted) {
17803
+ disposed = true;
17804
+ resolveReady(false);
17805
+ return { cleanup, ready };
17806
+ }
17807
+ if (!output || !rl.terminal || !output.isTTY || process.env.TERM === "dumb" || !original || !options.length) {
17808
+ options.forEach((option, index) => write(` ${index + 1}. ${displayLabel(option, columns() - 6)}
17809
+ `));
17810
+ write(prompt);
17811
+ resolveReady(false);
17812
+ disposed = true;
17813
+ return { cleanup, ready };
17814
+ }
17815
+ try {
17816
+ if (priorLine) {
17817
+ original.call(rl, void 0, { name: "end" });
17818
+ original.call(rl, void 0, { name: "u", ctrl: true });
17819
+ }
17820
+ rl.setPrompt(prompt);
17821
+ rl._ttyWrite = intercept;
17822
+ rl.once("line", onLine);
17823
+ rl.once("close", cleanup);
17824
+ output.on("resize", onResize);
17825
+ output.on("error", cleanup);
17826
+ settings.signal?.addEventListener("abort", cleanup, { once: true });
17827
+ for (const label of labels()) write(`${label}\r
17828
+ `);
17829
+ write(" \u2191/\u2193 and Enter, a number, or your own answer.\r\n");
17830
+ rl.prompt();
17831
+ requestPosition();
17832
+ } catch (error) {
17833
+ cleanup();
17834
+ throw error;
17835
+ }
17836
+ return { cleanup, ready };
17837
+ }
17838
+
17839
+ // src/ui/slash-completion.ts
17840
+ function builtinSlashEntries(commands, help) {
17841
+ const descriptions = /* @__PURE__ */ new Map();
17842
+ for (const line of help.split("\n")) {
17843
+ const match = /^(\/[^\n]+?)\s{2,}(.+)$/.exec(line.trim());
17844
+ if (!match) continue;
17845
+ for (const command of match[1].matchAll(/\/([a-z-]+)/g)) descriptions.set(command[1], match[2]);
17846
+ }
17847
+ return commands.map((command) => ({ text: `/${command}`, label: `/${command}`, description: descriptions.get(command) }));
17848
+ }
17849
+ function attachSlashCompletion(readline, loadEntries, render, initialEntries = []) {
17850
+ const rl = readline;
17851
+ const original = rl._ttyWrite;
17852
+ let entries = [...initialEntries];
17853
+ let matches = [];
17854
+ let selected = 0;
17855
+ let disposed = false;
17856
+ let suppressed = false;
17857
+ let opened = false;
17858
+ let epoch = 0;
17859
+ let handledEscape = false;
17860
+ const clear = () => {
17861
+ matches = [];
17862
+ selected = 0;
17863
+ render([], 0);
17864
+ };
17865
+ const refresh = () => {
17866
+ if (disposed || suppressed) return;
17867
+ const line = rl.line;
17868
+ const skillPrefix = /^\/skill\s+([a-z0-9-]*)$/i.exec(line);
17869
+ if (!/^\/[a-z0-9-]*$/i.test(line) && !skillPrefix) {
17870
+ opened = false;
17871
+ epoch += 1;
17872
+ clear();
17873
+ return;
17874
+ }
17875
+ if (!opened) {
17876
+ opened = true;
17877
+ const request = ++epoch;
17878
+ void loadEntries().then((items) => {
17879
+ if (disposed || request !== epoch || !opened) return;
17880
+ entries = items;
17881
+ refresh();
17882
+ }).catch(() => {
17883
+ if (!disposed && request === epoch) clear();
17884
+ });
17885
+ }
17886
+ const query = (skillPrefix?.[1] ?? line.slice(1)).toLowerCase();
17887
+ matches = entries.filter((entry) => (!skillPrefix || entry.skill) && (entry.text.toLowerCase().includes(query) || entry.label.toLowerCase().includes(query)));
17888
+ selected = Math.min(selected, Math.max(0, matches.length - 1));
17889
+ render(matches.map((entry) => `${entry.label}${entry.description ? ` ${entry.description}` : ""}`.replace(/[\x00-\x1f\x7f]/g, " ")), selected);
17890
+ };
17891
+ const wrapper = function(text, key) {
17892
+ if (!suppressed && matches.length) {
17893
+ if (key?.name === "escape") {
17894
+ handledEscape = true;
17895
+ setTimeout(() => {
17896
+ handledEscape = false;
17897
+ }, 0);
17898
+ opened = false;
17899
+ epoch += 1;
17900
+ clear();
17901
+ return;
17902
+ }
17903
+ if (key?.name === "up" || key?.name === "down") {
17904
+ selected = (selected + (key.name === "up" ? -1 : 1) + matches.length) % matches.length;
17905
+ render(matches.map((entry) => `${entry.label}${entry.description ? ` ${entry.description}` : ""}`.replace(/[\x00-\x1f\x7f]/g, " ")), selected);
17906
+ return;
17907
+ }
17908
+ if (key?.name === "tab" || key?.name === "return" || key?.name === "enter") {
17909
+ const choice = matches[selected];
17910
+ suppressed = true;
17911
+ try {
17912
+ rl.write(null, { ctrl: true, name: "u" });
17913
+ rl.write(`${choice.text} `);
17914
+ } finally {
17915
+ suppressed = false;
17916
+ opened = false;
17917
+ epoch += 1;
17918
+ clear();
17919
+ }
17920
+ return;
17921
+ }
17922
+ }
17923
+ original.call(this, text, key);
17924
+ refresh();
17925
+ };
17926
+ rl._ttyWrite = wrapper;
17927
+ return {
17928
+ handledEscape: () => handledEscape,
17929
+ suspend: () => {
17930
+ suppressed = true;
17931
+ opened = false;
17932
+ epoch += 1;
17933
+ clear();
17934
+ },
17935
+ resume: () => {
17936
+ suppressed = false;
17937
+ refresh();
17938
+ },
17939
+ cleanup: () => {
17940
+ disposed = true;
17941
+ epoch += 1;
17942
+ clear();
17943
+ if (rl._ttyWrite === wrapper) rl._ttyWrite = original;
17944
+ }
17945
+ };
17946
+ }
17947
+
17948
+ // src/ui/terminal-layout.ts
17949
+ import { Writable } from "stream";
17950
+ import { stripVTControlCharacters as stripVTControlCharacters2 } from "util";
17951
+ var CSI = "\x1B[";
17952
+ var RESET_REGION = `${CSI}r`;
17953
+ var MOUSE_OFF2 = `${CSI}?1000l${CSI}?1002l${CSI}?1003l${CSI}?1006l`;
17954
+ var BASE_FOOTER_ROWS = 4;
17955
+ var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
17956
+ var cells = (value) => [...segmenter.segment(value)].map(({ segment }) => {
17957
+ const code = segment.codePointAt(0) ?? 0;
17958
+ const wide = /\p{Extended_Pictographic}/u.test(segment) || code >= 4352 && (code <= 4447 || code === 9001 || code === 9002 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65135 || code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 131072);
17959
+ return { text: segment, width: /^\p{Mark}+$/u.test(segment) ? 0 : wide ? 2 : 1 };
17960
+ });
17961
+ var width = (text) => cells(text).reduce((sum, cell) => sum + cell.width, 0);
17962
+ function plain(value) {
17963
+ return stripVTControlCharacters2(value).replace(/[\x00-\x1f\x7f]/g, " ");
17964
+ }
17965
+ function fit(text, columns) {
17966
+ if (columns <= 0) return "";
17967
+ let result = "";
17968
+ let used = 0;
17969
+ for (const cell of cells(plain(text))) {
17970
+ if (used + cell.width > columns) break;
17971
+ result += cell.text;
17972
+ used += cell.width;
17973
+ }
17974
+ return result;
17975
+ }
17976
+ var cup = (row, column = 1) => `${CSI}${row};${column}H`;
17977
+ var ComposerOutput = class extends Writable {
17978
+ constructor(layout) {
17979
+ super();
17980
+ this.layout = layout;
17981
+ }
17982
+ layout;
17983
+ get isTTY() {
17984
+ return this.layout.enabled;
17985
+ }
17986
+ get columns() {
17987
+ return this.layout.columns;
17988
+ }
17989
+ get rows() {
17990
+ return this.layout.rows;
17991
+ }
17992
+ _write(chunk, _encoding, callback) {
17993
+ try {
17994
+ this.layout.writeComposer(chunk);
17995
+ callback();
17996
+ } catch (error) {
17997
+ callback(error instanceof Error ? error : new Error("Terminal output failed"));
17998
+ }
17999
+ }
18000
+ };
18001
+ var TerminalLayout = class {
18002
+ outputProxy;
18003
+ enabled;
18004
+ output;
18005
+ errorOutput;
18006
+ originalWrite;
18007
+ originalErrorWrite;
18008
+ rawWrite;
18009
+ details;
18010
+ rl;
18011
+ prompt = "\u203A ";
18012
+ inputFormatter;
18013
+ transcriptRow = 1;
18014
+ transcriptColumn = 1;
18015
+ wrapPending = false;
18016
+ savedTranscript;
18017
+ lastFooterTop = 0;
18018
+ suggestions = [];
18019
+ selected = 0;
18020
+ activity;
18021
+ activityExpanded = false;
18022
+ activitySelected = -1;
18023
+ activityEnd = 0;
18024
+ restoreActivityInput;
18025
+ installed = false;
18026
+ closed = false;
18027
+ suspended = 0;
18028
+ paintQueued = false;
18029
+ bindSignals;
18030
+ resize = () => {
18031
+ if (this.closed || !this.enabled) return;
18032
+ this.clearPreviousFooter();
18033
+ this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
18034
+ this.transcriptColumn = Math.min(this.transcriptColumn, this.columns);
18035
+ this.rawWrite(`${CSI}1;${this.contentRows}r`);
18036
+ this.outputProxy.emit("resize");
18037
+ this.paintFooter();
18038
+ };
18039
+ exit = () => {
18040
+ this.close();
18041
+ };
18042
+ constructor(options) {
18043
+ this.output = options.output;
18044
+ this.errorOutput = options.errorOutput;
18045
+ this.details = { ...options.details };
18046
+ this.bindSignals = options.bindSignals !== false;
18047
+ this.originalWrite = options.output.write;
18048
+ this.rawWrite = options.output.write.bind(options.output);
18049
+ this.originalErrorWrite = options.errorOutput?.write;
18050
+ this.enabled = Boolean(options.output.isTTY && (options.output.columns ?? 80) >= 12 && (options.output.rows ?? 24) >= 7);
18051
+ this.outputProxy = new ComposerOutput(this);
18052
+ }
18053
+ get columns() {
18054
+ return Math.max(12, this.output.columns ?? 80);
18055
+ }
18056
+ get rows() {
18057
+ return Math.max(7, this.output.rows ?? 24);
18058
+ }
18059
+ get suggestionRows() {
18060
+ return Math.min(8, this.suggestions.length, Math.max(0, this.rows - BASE_FOOTER_ROWS - 2));
18061
+ }
18062
+ get activityLines() {
18063
+ if (!this.activity) return [];
18064
+ return activityRows(this.activity, this.activityExpanded, this.activitySelected, this.activityEnd).slice(0, Math.max(1, Math.min(10, this.rows - BASE_FOOTER_ROWS - this.suggestionRows - 2)));
18065
+ }
18066
+ get contentRows() {
18067
+ return this.rows - BASE_FOOTER_ROWS - this.suggestionRows - this.activityLines.length;
18068
+ }
18069
+ get inputRow() {
18070
+ return this.rows - 2;
18071
+ }
18072
+ install() {
18073
+ if (this.installed || this.closed || !this.enabled) return this;
18074
+ this.installed = true;
18075
+ this.output.write = ((chunk, encoding, callback) => this.writeTranscript(chunk, encoding, callback));
18076
+ if (this.errorOutput && this.errorOutput !== this.output) this.errorOutput.write = this.output.write;
18077
+ this.output.on("resize", this.resize);
18078
+ this.output.on("error", this.exit);
18079
+ if (this.errorOutput !== this.output) this.errorOutput?.on("error", this.exit);
18080
+ process.once("exit", this.exit);
18081
+ if (this.bindSignals) {
18082
+ process.once("SIGINT", this.exit);
18083
+ process.once("SIGTERM", this.exit);
18084
+ }
18085
+ this.rawWrite(`${CSI}2J${cup(1)}${CSI}1;${this.contentRows}r`);
18086
+ this.paintFooter();
18087
+ return this;
18088
+ }
18089
+ attachReadline(rl) {
18090
+ this.rl = rl;
18091
+ rl.setPrompt(this.prompt);
18092
+ const reader = rl;
18093
+ const original = reader._ttyWrite;
18094
+ let sequence = "";
18095
+ let sequenceTimer;
18096
+ const wrapper = (text, key) => {
18097
+ if (this.activity && !this.suspended && key?.ctrl && key.name === "o") {
18098
+ this.toggleActivity();
18099
+ return;
18100
+ }
18101
+ const value = key?.sequence ?? text ?? "";
18102
+ if (sequence || value.startsWith("\x1B[<")) {
18103
+ sequence += value;
18104
+ clearTimeout(sequenceTimer);
18105
+ const mouse = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(sequence);
18106
+ if (mouse) {
18107
+ sequence = "";
18108
+ if (!this.suspended) this.activityMouse(Number(mouse[1]), Number(mouse[3]), mouse[4] === "M");
18109
+ return;
18110
+ }
18111
+ if (/^\x1b\[<[0-9;]*$/.test(sequence) && sequence.length < 64) {
18112
+ sequenceTimer = setTimeout(() => {
18113
+ sequence = "";
18114
+ }, 250);
18115
+ return;
18116
+ }
18117
+ const pending = sequence;
18118
+ sequence = "";
18119
+ original.call(reader, pending, key);
18120
+ return;
18121
+ }
18122
+ original.call(reader, text, key);
18123
+ };
18124
+ reader._ttyWrite = wrapper;
18125
+ this.restoreActivityInput = () => {
18126
+ clearTimeout(sequenceTimer);
18127
+ if (reader._ttyWrite === wrapper) reader._ttyWrite = original;
18128
+ };
18129
+ this.paintFooter();
18130
+ }
18131
+ setActivity(snapshot) {
18132
+ this.clearPreviousFooter();
18133
+ const previous = this.activity?.entries.length ?? 0;
18134
+ if (!snapshot.entries.length && !snapshot.finished) {
18135
+ this.activityExpanded = false;
18136
+ this.activitySelected = -1;
18137
+ }
18138
+ this.activity = snapshot;
18139
+ if (this.activityEnd >= previous || !previous) this.activityEnd = snapshot.entries.length;
18140
+ this.reflowActivity();
18141
+ }
18142
+ toggleActivity() {
18143
+ if (!this.activity) return;
18144
+ this.clearPreviousFooter();
18145
+ this.activityExpanded = !this.activityExpanded;
18146
+ this.reflowActivity();
18147
+ }
18148
+ reflowActivity() {
18149
+ if (!this.enabled || !this.installed || this.closed) return;
18150
+ this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
18151
+ this.rawWrite(`${CSI}1;${this.contentRows}r`);
18152
+ this.paintFooter();
18153
+ }
18154
+ activityMouse(button, row, press) {
18155
+ if (!this.activity || !press) return;
18156
+ if (button === 64 || button === 65) {
18157
+ if (!this.activityExpanded) return;
18158
+ this.clearPreviousFooter();
18159
+ this.activityEnd = Math.max(Math.min(5, this.activity.entries.length), Math.min(this.activity.entries.length, this.activityEnd + (button === 64 ? -1 : 1)));
18160
+ this.activitySelected = -1;
18161
+ this.reflowActivity();
18162
+ return;
18163
+ }
18164
+ if (button !== 0) return;
18165
+ const index = row - this.contentRows - 1;
18166
+ if (index === 0) {
18167
+ this.toggleActivity();
18168
+ return;
18169
+ }
18170
+ const entry = this.activityLines[index]?.entry;
18171
+ if (entry !== void 0) {
18172
+ this.clearPreviousFooter();
18173
+ this.activitySelected = this.activitySelected === entry ? -1 : entry;
18174
+ this.reflowActivity();
18175
+ }
18176
+ }
18177
+ setInputFormatter(formatter) {
18178
+ this.inputFormatter = formatter;
18179
+ this.paintFooter();
18180
+ }
18181
+ setPrompt(prompt) {
18182
+ this.prompt = prompt;
18183
+ this.rl?.setPrompt(prompt);
18184
+ this.paintFooter();
18185
+ }
18186
+ updateDetails(details) {
18187
+ this.details = { ...this.details, ...details };
18188
+ this.paintFooter();
18189
+ }
18190
+ setSuggestions(items, selectedIndex = 0) {
18191
+ if (this.enabled && this.installed && !this.closed) this.clearPreviousFooter();
18192
+ this.suggestions = [...items];
18193
+ this.selected = Math.max(0, Math.min(items.length - 1, selectedIndex));
18194
+ if (this.enabled && this.installed && !this.closed) {
18195
+ this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
18196
+ this.rawWrite(`${CSI}1;${this.contentRows}r`);
18197
+ this.paintFooter();
18198
+ }
18199
+ }
18200
+ /** Freeze the footer and focus choices in the transcript; its reserved rows stay visible. */
18201
+ suspend() {
18202
+ if (this.closed || !this.enabled) return;
18203
+ this.suspended += 1;
18204
+ this.rawWrite(cup(this.transcriptRow, this.transcriptColumn));
18205
+ }
18206
+ resume() {
18207
+ if (this.suspended > 0) this.suspended -= 1;
18208
+ if (!this.suspended) this.paintFooter();
18209
+ }
18210
+ writeComposer(chunk) {
18211
+ if (this.closed || !this.enabled || !this.installed) {
18212
+ this.rawWrite(chunk);
18213
+ return;
18214
+ }
18215
+ if (this.suspended) {
18216
+ this.writeTranscript(chunk);
18217
+ return;
18218
+ }
18219
+ if (!this.paintQueued) {
18220
+ this.paintQueued = true;
18221
+ queueMicrotask(() => {
18222
+ this.paintQueued = false;
18223
+ if (!this.closed && !this.suspended) this.paintFooter();
18224
+ });
18225
+ }
18226
+ }
18227
+ writeTranscript(chunk, encoding, callback) {
18228
+ if (this.closed || !this.enabled || !this.installed) return this.rawWrite(chunk, encoding, callback);
18229
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(typeof encoding === "string" ? encoding : "utf8");
18230
+ const formatted = text.replace(/(?<!\r)\n/g, "\r\n").replace(/\x1b\[([012]?)J/g, (_escape, mode) => {
18231
+ const saved = cup(this.transcriptRow, this.transcriptColumn);
18232
+ const first = mode === "2" || mode === "1" ? 1 : this.transcriptRow;
18233
+ const last = mode === "1" ? this.transcriptRow : this.contentRows;
18234
+ let erased = "";
18235
+ for (let row = first; row <= last; row += 1) erased += `${cup(row)}${CSI}2K`;
18236
+ return erased + saved;
18237
+ });
18238
+ if (this.wrapPending && !/^[\r\n]/.test(stripVTControlCharacters2(formatted))) {
18239
+ this.rawWrite(`${cup(this.transcriptRow, this.columns)}\r
18240
+ `);
18241
+ this.transcriptRow = Math.min(this.contentRows, this.transcriptRow + 1);
18242
+ this.transcriptColumn = 1;
18243
+ this.wrapPending = false;
18244
+ }
18245
+ this.rawWrite(cup(this.transcriptRow, this.transcriptColumn));
18246
+ const accepted = this.rawWrite(
18247
+ formatted,
18248
+ typeof encoding === "string" ? encoding : void 0,
18249
+ typeof encoding === "function" ? encoding : callback
18250
+ );
18251
+ this.trackTranscript(formatted);
18252
+ if (!this.suspended) this.placeInputCursor();
18253
+ return accepted;
18254
+ }
18255
+ trackTranscript(text) {
18256
+ const parts = text.split(/(\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07]*(?:\x07|\x1b\\)|\x1b.)/g);
18257
+ for (const part of parts) {
18258
+ if (part === "\x1B7") {
18259
+ this.savedTranscript = { row: this.transcriptRow, column: this.transcriptColumn, wrapPending: this.wrapPending };
18260
+ } else if (part === "\x1B8") {
18261
+ if (this.savedTranscript) {
18262
+ this.transcriptRow = this.savedTranscript.row;
18263
+ this.transcriptColumn = this.savedTranscript.column;
18264
+ this.wrapPending = this.savedTranscript.wrapPending;
18265
+ }
18266
+ } else if (part.startsWith("\x1B[")) {
18267
+ const match = /^\x1b\[([\d;]*)([A-GHf])$/.exec(part);
18268
+ if (!match) continue;
18269
+ const values = match[1].split(";").map((value) => Number(value) || 1);
18270
+ const n = values[0];
18271
+ this.wrapPending = false;
18272
+ if (match[2] === "A") this.transcriptRow -= n;
18273
+ else if (match[2] === "B") this.transcriptRow += n;
18274
+ else if (match[2] === "C") this.transcriptColumn += n;
18275
+ else if (match[2] === "D") this.transcriptColumn -= n;
18276
+ else if (match[2] === "G") this.transcriptColumn = n;
18277
+ else if (match[2] === "H" || match[2] === "f") {
18278
+ this.transcriptRow = n;
18279
+ this.transcriptColumn = values[1] ?? 1;
18280
+ }
18281
+ } else if (!part.startsWith("\x1B")) {
18282
+ for (const cell of cells(part)) {
18283
+ if (cell.text === "\r" || cell.text === "\r\n") {
18284
+ this.wrapPending = false;
18285
+ this.transcriptColumn = 1;
18286
+ if (cell.text === "\r\n") this.transcriptRow += 1;
18287
+ } else if (cell.text === "\n") {
18288
+ this.wrapPending = false;
18289
+ this.transcriptColumn = 1;
18290
+ this.transcriptRow += 1;
18291
+ } else if (cell.text === "\b") this.transcriptColumn -= 1;
18292
+ else if (cell.text === " ") this.transcriptColumn += 8 - (this.transcriptColumn - 1) % 8;
18293
+ else {
18294
+ if (this.wrapPending || this.transcriptColumn + cell.width - 1 > this.columns) {
18295
+ this.transcriptColumn = 1;
18296
+ this.transcriptRow += 1;
18297
+ this.wrapPending = false;
18298
+ }
18299
+ this.transcriptColumn += cell.width;
18300
+ if (this.transcriptColumn > this.columns) {
18301
+ this.transcriptColumn = this.columns;
18302
+ this.wrapPending = true;
18303
+ }
18304
+ }
18305
+ this.transcriptRow = Math.min(this.contentRows, this.transcriptRow);
18306
+ }
18307
+ }
18308
+ this.transcriptRow = Math.max(1, Math.min(this.contentRows, this.transcriptRow));
18309
+ this.transcriptColumn = Math.max(1, Math.min(this.columns, this.transcriptColumn));
18310
+ }
18311
+ }
18312
+ composer() {
18313
+ const available = this.columns - 4;
18314
+ const prompt = fit(this.rl?.getPrompt() ?? this.prompt, Math.min(24, Math.max(2, Math.floor(available / 3))));
18315
+ const raw = this.rl?.line ?? "";
18316
+ const formatted = this.inputFormatter?.(raw, this.rl?.cursor ?? 0) ?? { text: raw, cursor: this.rl?.cursor ?? 0 };
18317
+ const input = plain(formatted.text);
18318
+ const cursor = width(plain(formatted.text.slice(0, formatted.cursor)));
18319
+ const capacity = Math.max(1, available - width(prompt));
18320
+ const offset = Math.max(0, cursor - capacity + 1);
18321
+ let visible = "";
18322
+ let used = 0;
18323
+ let skipped = 0;
18324
+ for (const cell of cells(input)) {
18325
+ if (skipped + cell.width <= offset) {
18326
+ skipped += cell.width;
18327
+ continue;
18328
+ }
18329
+ if (used + cell.width > capacity) break;
18330
+ visible += cell.text;
18331
+ used += cell.width;
18332
+ }
18333
+ return { text: `${prompt}${visible}`, column: Math.min(this.columns - 1, 3 + width(prompt) + cursor - skipped) };
18334
+ }
18335
+ placeInputCursor() {
18336
+ const composer = this.composer();
18337
+ this.rawWrite(`${cup(this.inputRow, composer.column)}${CSI}?25h`);
18338
+ }
18339
+ clearPreviousFooter() {
18340
+ if (!this.lastFooterTop) return;
18341
+ let clear = "";
18342
+ for (let row = this.lastFooterTop; row <= this.rows; row += 1) clear += `${cup(row)}${CSI}2K`;
18343
+ this.rawWrite(clear);
18344
+ }
18345
+ paintFooter() {
18346
+ if (!this.enabled || !this.installed || this.closed || this.suspended) return;
18347
+ this.lastFooterTop = this.contentRows + 1;
18348
+ const inner = this.columns - 2;
18349
+ const border = "\u2500".repeat(inner);
18350
+ const composer = this.composer();
18351
+ const model = /^cosmic[- ]1$/i.test(this.details.model) ? "Cosmic 1" : this.details.model;
18352
+ const metadata = fit(model, this.columns);
18353
+ let frame = "";
18354
+ const activity = this.activityLines;
18355
+ if (activity.length && !this.suspended) frame += `${CSI}?1006h${CSI}?1000h`;
18356
+ activity.forEach((row, index) => {
18357
+ const text = fit(row.text, this.columns);
18358
+ const color = process.env.NO_COLOR || row.ok === void 0 ? "" : `${CSI}${row.ok ? 32 : 31}m`;
18359
+ const dot = text.indexOf("\u25CF");
18360
+ const rendered = dot >= 0 && color ? `${text.slice(0, dot)}${color}\u25CF${CSI}39m${text.slice(dot + 1)}` : text;
18361
+ frame += `${cup(this.contentRows + 1 + index)}${CSI}0m${CSI}2K${rendered}`;
18362
+ });
18363
+ const suggestions = this.suggestionRows;
18364
+ const first = Math.max(0, Math.min(this.selected - Math.floor(suggestions / 2), this.suggestions.length - suggestions));
18365
+ const commandWidth = Math.min(
18366
+ Math.max(18, ...this.suggestions.map((row) => width(plain(row).split(/ {2,}/)[0] ?? ""))),
18367
+ Math.max(8, Math.floor(this.columns / 3))
18368
+ );
18369
+ for (let i = 0; i < suggestions; i += 1) {
18370
+ const index = first + i;
18371
+ const selected = index === this.selected;
18372
+ const entry = plain(this.suggestions[index] ?? "");
18373
+ const match = /^(.+?) {2,}(.+)$/.exec(entry);
18374
+ const command = fit(match?.[1] ?? entry, commandWidth);
18375
+ const description = match?.[2] ?? "";
18376
+ const row = fit(`${selected ? "\u203A" : " "} ${command}${description ? `${" ".repeat(Math.max(0, commandWidth - width(command)) + 6)}${description}` : ""}`, this.columns);
18377
+ const style = process.env.NO_COLOR ? "" : selected ? `${CSI}1m${CSI}39m${CSI}49m` : `${CSI}22m${CSI}39m${CSI}49m`;
18378
+ frame += `${cup(this.contentRows + activity.length + 1 + i)}${CSI}0m${CSI}2K${style}${row}${" ".repeat(this.columns - width(row))}${CSI}0m`;
18379
+ }
18380
+ const inputText = fit(composer.text, this.columns - 4);
18381
+ const paddedInput = inputText + " ".repeat(Math.max(0, this.columns - 4 - width(inputText)));
18382
+ const dimBorder = (value) => process.env.NO_COLOR ? value : `${CSI}90m${value}${CSI}39m`;
18383
+ const lines = [dimBorder(`\u250C${border}\u2510`), `${dimBorder("\u2502")} ${paddedInput} ${dimBorder("\u2502")}`, dimBorder(`\u2514${border}\u2518`), metadata];
18384
+ lines.forEach((line, index) => {
18385
+ frame += `${cup(this.rows - 3 + index)}${CSI}2K${line}`;
18386
+ });
18387
+ this.rawWrite(`${frame}${cup(this.inputRow, composer.column)}${CSI}?25h`);
18388
+ }
18389
+ close() {
18390
+ if (this.closed) return;
18391
+ this.closed = true;
18392
+ this.restoreActivityInput?.();
18393
+ if (!this.installed) return;
18394
+ this.output.write = this.originalWrite;
18395
+ if (this.errorOutput && this.errorOutput !== this.output && this.originalErrorWrite) this.errorOutput.write = this.originalErrorWrite;
18396
+ this.output.off("resize", this.resize);
18397
+ this.output.off("error", this.exit);
18398
+ if (this.errorOutput !== this.output) this.errorOutput?.off("error", this.exit);
18399
+ process.off("exit", this.exit);
18400
+ process.off("SIGINT", this.exit);
18401
+ process.off("SIGTERM", this.exit);
18402
+ try {
18403
+ this.rawWrite(`${RESET_REGION}${MOUSE_OFF2}${CSI}?25h`);
18404
+ this.clearPreviousFooter();
18405
+ this.rawWrite(`${cup(this.transcriptRow, this.transcriptColumn)}\r
18406
+ `);
18407
+ } catch {
18408
+ }
18409
+ }
18410
+ };
18411
+ function createTerminalLayout(options) {
18412
+ return new TerminalLayout(options).install();
18413
+ }
18414
+
18415
+ // src/ui/welcome.ts
18416
+ import chalk10, { Chalk } from "chalk";
18417
+ import { homedir as homedir3 } from "os";
18418
+ import { sep } from "path";
18419
+
18420
+ // src/ui/provider-brand.ts
18421
+ import { stripVTControlCharacters as stripVTControlCharacters3 } from "util";
18422
+ var GOLD = "\x1B[38;2;202;168;103m";
18423
+ var DEFAULT_FOREGROUND = "\x1B[39m";
18424
+ function safeDisplayText(value) {
18425
+ return stripVTControlCharacters3(value).replace(/[\x00-\x1f\x7f]/g, " ");
17208
18426
  }
17209
- function truncate2(value, max) {
17210
- return value.length > max ? `${value.slice(0, Math.max(1, max - 1))}\u2026` : value;
18427
+ function providerDisplayName(provider) {
18428
+ const name = safeDisplayText(provider);
18429
+ return name.toLowerCase() === "ultrac" ? "spinalfluid" : name;
17211
18430
  }
17212
- function humanize(value) {
17213
- const words = value.replace(/[_-]+/g, " ").trim();
17214
- return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Tool";
18431
+ function paintProviderSpan(line, offset, provider, color) {
18432
+ if (!color || provider !== "spinalfluid" || offset < 0 || line.slice(offset, offset + provider.length) !== provider) return line;
18433
+ return `${line.slice(0, offset)}${GOLD}${provider}${DEFAULT_FOREGROUND}${line.slice(offset + provider.length)}`;
17215
18434
  }
17216
18435
 
17217
18436
  // src/ui/welcome.ts
17218
- import chalk10, { Chalk } from "chalk";
17219
18437
  var LOGO = [
17220
18438
  "\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557",
17221
18439
  "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
@@ -17227,29 +18445,25 @@ var LOGO = [
17227
18445
  function renderWelcome(info, options = {}) {
17228
18446
  const columns = normalizeColumns(options.columns);
17229
18447
  const paint = new Chalk({ level: options.color === false ? 0 : chalk10.level });
17230
- const cardWidth = Math.min(88, Math.max(42, columns));
17231
- const innerWidth = cardWidth - 4;
18448
+ const innerWidth = columns - 2;
17232
18449
  const wide = columns >= 64;
17233
18450
  const lines = [];
17234
18451
  if (wide) {
17235
18452
  lines.push("");
17236
18453
  for (let index = 0; index < LOGO.length; index += 1) {
17237
- const color = index < 2 ? paint.hex("#38BDF8") : index < 4 ? paint.hex("#22D3EE") : paint.hex("#A78BFA");
17238
- lines.push(` ${color(LOGO[index])}`);
18454
+ lines.push(` ${paint.bold.white(LOGO[index])}`);
17239
18455
  }
17240
18456
  } else {
17241
- lines.push("", ` ${paint.bold.hex("#22D3EE")("ULTRAC")} ${paint.gray(`v${info.version}`)}`);
17242
- }
17243
- const topLabel = wide ? ` ultrac v${info.version} ` : " session ";
17244
- const topFill = Math.max(0, cardWidth - topLabel.length - 2);
17245
- lines.push(paint.cyan(`\u256D${topLabel}${"\u2500".repeat(topFill)}\u256E`));
17246
- lines.push(cardLine(`${info.model} via ${info.providerName}`, innerWidth, paint.bold));
17247
- lines.push(cardLine(`${info.mode} \xB7 ${info.protocol}`, innerWidth, paint.white));
17248
- lines.push(cardLine(shortenMiddle(info.workspace, innerWidth), innerWidth, paint.gray));
17249
- lines.push(paint.cyan(`\u251C${"\u2500".repeat(cardWidth - 2)}\u2524`));
17250
- lines.push(cardLine(info.budget, innerWidth, paint.gray));
17251
- lines.push(cardLine(`logs ${shortenMiddle(info.diagnosticPath, Math.max(8, innerWidth - 5))}`, innerWidth, paint.gray));
17252
- lines.push(paint.cyan(`\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`));
18457
+ lines.push("", ` ${paint.bold.white("ULTRAC")} ${paint.gray(`v${info.version}`)}`);
18458
+ }
18459
+ const provider = providerDisplayName(info.providerName);
18460
+ const prefix = `${safeDisplayText(info.model)} \xB7 `;
18461
+ const account = `${provider}${info.planName ? ` ${safeDisplayText(info.planName)}` : ""}`;
18462
+ const modelLine = ` ${truncate3(`${prefix}${account}`, innerWidth)}`;
18463
+ lines.push("", paintProviderSpan(modelLine, 2 + prefix.length, provider, paint.level > 0));
18464
+ const home = homedir3();
18465
+ const workspace = info.workspace === home ? "~" : info.workspace.startsWith(`${home}${sep}`) ? `~${info.workspace.slice(home.length)}` : info.workspace;
18466
+ lines.push(paint.gray(` ${shortenMiddle(safeDisplayText(workspace), innerWidth)}`), "");
17253
18467
  lines.push(
17254
18468
  wide ? `${paint.gray(" /help commands")} ${paint.dim("\xB7")} ${paint.gray("Esc stop")} ${paint.dim("\xB7")} ${paint.gray("Ctrl-C exit")}` : paint.gray(" /help \xB7 Esc \xB7 Ctrl-C")
17255
18469
  );
@@ -17257,40 +18471,43 @@ function renderWelcome(info, options = {}) {
17257
18471
  }
17258
18472
  function renderComposer(columns = 80, color = true) {
17259
18473
  const paint = new Chalk({ level: color ? chalk10.level : 0 });
17260
- const width = Math.min(88, Math.max(28, normalizeColumns(columns)));
18474
+ const width2 = normalizeColumns(columns);
17261
18475
  const label = " message ";
17262
- const fill = Math.max(1, width - label.length - 2);
18476
+ const fill = Math.max(1, width2 - label.length - 2);
17263
18477
  return {
17264
18478
  frame: `
17265
- ${paint.cyan(`\u256D\u2500${label}${"\u2500".repeat(fill)}`)}
17266
- ${paint.cyan("\u2502")}`,
17267
- prompt: paint.bold.hex("#22D3EE")("\u2570\u2500\u276F ")
18479
+ ${paint.gray(`\u256D\u2500${label}${"\u2500".repeat(fill)}`)}
18480
+ ${paint.gray("\u2502")}`,
18481
+ prompt: paint.bold.white("\u2570\u2500\u276F ")
17268
18482
  };
17269
18483
  }
17270
- function cardLine(value, width, style) {
17271
- const clipped = truncate3(value, width);
17272
- return `\u2502 ${style(clipped)}${" ".repeat(Math.max(0, width - clipped.length))} \u2502`;
17273
- }
17274
18484
  function normalizeColumns(columns) {
17275
- return Number.isFinite(columns) ? Math.max(28, Math.floor(columns)) : 80;
18485
+ return Number.isFinite(columns) ? Math.max(16, Math.floor(columns)) : 80;
17276
18486
  }
17277
- function truncate3(value, width) {
17278
- if (value.length <= width) return value;
17279
- if (width <= 1) return "\u2026";
17280
- return `${value.slice(0, width - 1)}\u2026`;
18487
+ function truncate3(value, width2) {
18488
+ if (value.length <= width2) return value;
18489
+ if (width2 <= 1) return "\u2026";
18490
+ return `${value.slice(0, width2 - 1)}\u2026`;
17281
18491
  }
17282
- function shortenMiddle(value, width) {
17283
- if (value.length <= width) return value;
17284
- if (width < 9) return truncate3(value, width);
17285
- const side = Math.floor((width - 1) / 2);
17286
- return `${value.slice(0, side)}\u2026${value.slice(value.length - (width - side - 1))}`;
18492
+ function shortenMiddle(value, width2) {
18493
+ if (value.length <= width2) return value;
18494
+ if (width2 < 9) return truncate3(value, width2);
18495
+ const side = Math.floor((width2 - 1) / 2);
18496
+ return `${value.slice(0, side)}\u2026${value.slice(value.length - (width2 - side - 1))}`;
17287
18497
  }
17288
18498
 
18499
+ // src/ui/update-prompt.ts
18500
+ import { constants as constants7 } from "fs";
18501
+ import { lstat as lstat4, mkdir as mkdir15, open as open10, rename as rename4, unlink as unlink5 } from "fs/promises";
18502
+ import { randomBytes as randomBytes5 } from "crypto";
18503
+ import { homedir as homedir4 } from "os";
18504
+ import { join as join15, parse, resolve as resolve13 } from "path";
18505
+
17289
18506
  // src/update/check.ts
17290
18507
  import { spawn as spawn4 } from "child_process";
17291
18508
  import { randomBytes as randomBytes4 } from "crypto";
17292
18509
  import { readFile as readFile13, rename as rename3, unlink as unlink4, writeFile as writeFile12 } from "fs/promises";
17293
- import { join as join14 } from "path";
18510
+ import { isAbsolute as isAbsolute5, join as join14, relative as relative2, resolve as resolve12 } from "path";
17294
18511
  var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
17295
18512
  var QUERY_TIMEOUT_MS = 5e3;
17296
18513
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -17323,17 +18540,67 @@ async function queryLatestVersion(packageName) {
17323
18540
  return void 0;
17324
18541
  }
17325
18542
  }
17326
- async function installLatestVersion(packageName) {
18543
+ async function installLatestVersion(packageName, version = "latest") {
18544
+ if (version !== "latest" && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)) {
18545
+ throw new Error("Invalid npm release version.");
18546
+ }
17327
18547
  const result = await runNpm(
17328
- ["install", "--global", `${packageName}@latest`, "--no-audit", "--no-fund"],
18548
+ ["install", "--global", `${packageName}@${version}`, "--no-audit", "--no-fund"],
17329
18549
  { timeoutMs: INSTALL_TIMEOUT_MS, capture: false }
17330
18550
  );
17331
18551
  if (result.code !== 0) {
17332
18552
  throw new Error(
17333
- `npm could not install ${packageName}@latest. Confirm npm can reach the registry and install global packages.`
18553
+ `npm could not install ${packageName}@${version}. Confirm npm can reach the registry and install global packages.`
17334
18554
  );
17335
18555
  }
17336
18556
  }
18557
+ async function installVerifiedVersion(packageName, version) {
18558
+ await installLatestVersion(packageName, version);
18559
+ const result = await runNpm(["list", "--global", packageName, "--depth=0", "--json"], {
18560
+ timeoutMs: QUERY_TIMEOUT_MS,
18561
+ capture: true,
18562
+ maxOutputBytes: 4096
18563
+ });
18564
+ let installedVersion;
18565
+ try {
18566
+ const parsed = JSON.parse(result.stdout);
18567
+ if (typeof parsed === "object" && parsed !== null && "dependencies" in parsed && typeof parsed.dependencies === "object" && parsed.dependencies !== null) {
18568
+ const dependency = parsed.dependencies[packageName];
18569
+ if (typeof dependency === "object" && dependency !== null && "version" in dependency) {
18570
+ installedVersion = dependency.version;
18571
+ }
18572
+ }
18573
+ } catch {
18574
+ }
18575
+ if (result.code !== 0 || installedVersion !== version) {
18576
+ throw new Error("The update could not be verified. Run ultrac update and restart Ultrac.");
18577
+ }
18578
+ try {
18579
+ const globalRoot = await runNpm(["root", "--global"], {
18580
+ timeoutMs: QUERY_TIMEOUT_MS,
18581
+ capture: true,
18582
+ maxOutputBytes: 4096
18583
+ });
18584
+ const root = globalRoot.stdout.trim();
18585
+ if (globalRoot.code !== 0 || !isAbsolute5(root) || root.includes("\n")) throw new Error("Invalid npm root");
18586
+ const packageRoot = join14(root, packageName);
18587
+ const manifest = JSON.parse(await readFile13(join14(packageRoot, "package.json"), "utf8"));
18588
+ if (typeof manifest !== "object" || manifest === null || !("version" in manifest) || manifest.version !== version || !("bin" in manifest)) throw new Error("Invalid package");
18589
+ const bin = manifest.bin;
18590
+ const entry = typeof bin === "string" ? bin : typeof bin === "object" && bin !== null && "ultrac" in bin ? bin.ultrac : void 0;
18591
+ if (typeof entry !== "string") throw new Error("Missing executable");
18592
+ const executable = resolve12(packageRoot, entry);
18593
+ const withinPackage = relative2(packageRoot, executable);
18594
+ if (withinPackage.startsWith("..") || isAbsolute5(withinPackage)) throw new Error("Invalid executable");
18595
+ const probe = await runExecutable(process.execPath, [executable, "--version"], {
18596
+ timeoutMs: QUERY_TIMEOUT_MS,
18597
+ capture: true
18598
+ });
18599
+ if (probe.code !== 0 || probe.stdout.trim() !== version) throw new Error("Invalid executable version");
18600
+ } catch {
18601
+ throw new Error("The installed Ultrac executable could not be verified. Restart and run ultrac --version.");
18602
+ }
18603
+ }
17337
18604
  function isNewerVersion(candidate, current) {
17338
18605
  const left = parseVersion(candidate);
17339
18606
  const right = parseVersion(current);
@@ -17386,12 +18653,15 @@ async function writeCache(path, cache) {
17386
18653
  }
17387
18654
  }
17388
18655
  async function runNpm(args, options) {
18656
+ return runExecutable(process.platform === "win32" ? "npm.cmd" : "npm", args, options);
18657
+ }
18658
+ async function runExecutable(executable, args, options) {
17389
18659
  const env2 = { ...process.env };
17390
18660
  for (const key of Object.keys(env2)) {
17391
18661
  if (key.startsWith("ULTRAC_")) delete env2[key];
17392
18662
  }
17393
- return new Promise((resolve15) => {
17394
- const child = spawn4(process.platform === "win32" ? "npm.cmd" : "npm", args, {
18663
+ return new Promise((resolve17) => {
18664
+ const child = spawn4(executable, args, {
17395
18665
  env: env2,
17396
18666
  stdio: options.capture ? ["ignore", "pipe", "ignore"] : "inherit",
17397
18667
  windowsHide: true
@@ -17400,24 +18670,97 @@ async function runNpm(args, options) {
17400
18670
  if (options.capture) {
17401
18671
  child.stdout?.setEncoding("utf8");
17402
18672
  child.stdout?.on("data", (chunk) => {
17403
- if (stdout6.length <= 256) stdout6 += chunk;
18673
+ const limit = options.maxOutputBytes ?? 256;
18674
+ if (stdout6.length <= limit) stdout6 += chunk.slice(0, limit + 1 - stdout6.length);
17404
18675
  });
17405
18676
  }
17406
18677
  const timer = setTimeout(() => child.kill(), options.timeoutMs);
17407
18678
  child.once("error", () => {
17408
18679
  clearTimeout(timer);
17409
- resolve15({ code: 1, stdout: "" });
18680
+ resolve17({ code: 1, stdout: "" });
17410
18681
  });
17411
18682
  child.once("close", (code) => {
17412
18683
  clearTimeout(timer);
17413
- resolve15({ code: code ?? 1, stdout: stdout6 });
18684
+ resolve17({ code: code ?? 1, stdout: stdout6 });
17414
18685
  });
17415
18686
  });
17416
18687
  }
17417
18688
 
18689
+ // src/ui/update-prompt.ts
18690
+ var UPDATE_CHOICES = ["Update now", "Skip", "Skip until next version"];
18691
+ async function offerUpdatePrompt(status, options) {
18692
+ const version = status.latestVersion;
18693
+ if (!options.interactive || options.signal?.aborted || !status.updateAvailable || !version || !validVersion(version) || !validVersion(status.currentVersion) || !isNewerVersion(version, status.currentVersion) || await skippedVersion() === version) return "not_offered";
18694
+ const answer = await options.ask(
18695
+ `Ultrac ${version} is available. You are running ${status.currentVersion}.`,
18696
+ UPDATE_CHOICES,
18697
+ options.signal
18698
+ );
18699
+ if (options.signal?.aborted) return "skip";
18700
+ if (answer === UPDATE_CHOICES[0]) return "update_now";
18701
+ if (answer === UPDATE_CHOICES[2]) {
18702
+ await rememberSkippedVersion(version);
18703
+ return "skip_version";
18704
+ }
18705
+ return "skip";
18706
+ }
18707
+ function validVersion(version) {
18708
+ return version.length <= 128 && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version);
18709
+ }
18710
+ async function statePath(create) {
18711
+ const home = resolve13(ultracHomeDir());
18712
+ if (home === parse(home).root || home === resolve13(homedir4())) {
18713
+ throw new Error("Ultrac update preferences require a dedicated ULTRAC_HOME directory.");
18714
+ }
18715
+ if (create) await mkdir15(home, { recursive: true, mode: 448 });
18716
+ const info = await lstat4(home);
18717
+ if (!info.isDirectory() || info.isSymbolicLink()) {
18718
+ throw new Error("Ultrac update preferences require a real home directory.");
18719
+ }
18720
+ return join15(home, "update-preferences.json");
18721
+ }
18722
+ async function skippedVersion() {
18723
+ try {
18724
+ const path = await statePath(false);
18725
+ const file = await open10(path, constants7.O_RDONLY | constants7.O_NOFOLLOW);
18726
+ try {
18727
+ const info = await file.stat();
18728
+ if (!info.isFile() || info.size > 1024) return void 0;
18729
+ const parsed = JSON.parse(await file.readFile("utf8"));
18730
+ if (typeof parsed !== "object" || parsed === null || !("skippedVersion" in parsed)) return void 0;
18731
+ return typeof parsed.skippedVersion === "string" && validVersion(parsed.skippedVersion) ? parsed.skippedVersion : void 0;
18732
+ } finally {
18733
+ await file.close();
18734
+ }
18735
+ } catch {
18736
+ return void 0;
18737
+ }
18738
+ }
18739
+ async function rememberSkippedVersion(version) {
18740
+ const path = await statePath(true);
18741
+ const temporary = `${path}.${randomBytes5(8).toString("hex")}.tmp`;
18742
+ try {
18743
+ const file = await open10(temporary, constants7.O_WRONLY | constants7.O_CREAT | constants7.O_EXCL, 384);
18744
+ try {
18745
+ await file.writeFile(`${JSON.stringify({ skippedVersion: version })}
18746
+ `, "utf8");
18747
+ await file.sync();
18748
+ } finally {
18749
+ await file.close();
18750
+ }
18751
+ await rename4(temporary, path);
18752
+ } finally {
18753
+ await unlink5(temporary).catch(() => void 0);
18754
+ }
18755
+ }
18756
+
17418
18757
  // src/commands/repl.ts
17419
18758
  var HELP = `Commands:
17420
18759
  /help Show this help
18760
+ /doctor Check local setup
18761
+ /mcp Show connected MCP servers
18762
+ /continue [focus] Continue unfinished work
18763
+ /autonomous [on|off|status] Continue step-limited turns (up to five rounds)
17421
18764
  /model [name] Show or set the model
17422
18765
  /mode [name] Show or set the agent mode (${agentModeNames.join(", ")})
17423
18766
  /permissions [readonly|workspace|full] Show or set permissions
@@ -17441,9 +18784,10 @@ var HELP = `Commands:
17441
18784
  /whoami Show sign-in state
17442
18785
  /tools List available tools
17443
18786
  /logs Show the private diagnostic log path
17444
- /ghost [on|off|status] Quiet activity output; diagnostics stay enabled
18787
+ /ghost [on|off|status] Quiet activity output; diagnostics stay enabled
17445
18788
  /output [number|list] Inspect saved tool output (latest by default)
17446
18789
  /memory List and inspect stored memories
18790
+ /skill <name> [args] Run an installed skill
17447
18791
  /skills List installed skills
17448
18792
  /update Show how to install an available CLI update
17449
18793
  /clear Start a fresh conversation
@@ -17543,7 +18887,7 @@ async function runRepl(options, initialTask = "") {
17543
18887
  const hasFallbackChain = Boolean(config.fallback?.chain?.length);
17544
18888
  if (providerName === "ultrac" && !hasFallbackChain && !await resolveUltracCredentials()) {
17545
18889
  console.log(chalk11.yellow("You are not signed in to Ultrac."));
17546
- console.log(`Run ${chalk11.cyan("ultrac login")} first, or start with ${chalk11.cyan("--provider echo")}.`);
18890
+ console.log(`Run ${chalk11.white("ultrac login")} first, or start with ${chalk11.white("--provider echo")}.`);
17547
18891
  process.exitCode = EXIT_REPL_NOT_STARTED;
17548
18892
  return;
17549
18893
  }
@@ -17605,10 +18949,38 @@ async function runRepl(options, initialTask = "") {
17605
18949
  mode,
17606
18950
  permissions: basePermissions
17607
18951
  });
18952
+ const layout = stdin5.isTTY && stdout5.isTTY && process.env.TERM !== "dumb" ? createTerminalLayout({
18953
+ output: stdout5,
18954
+ errorOutput: stderr,
18955
+ bindSignals: false,
18956
+ details: { provider: provider.name, model: session.model, mode: session.mode, status: "Idle" }
18957
+ }) : void 0;
18958
+ setActivitySink(layout?.enabled ? (snapshot) => layout.setActivity(snapshot) : void 0);
18959
+ const refreshDetails = () => {
18960
+ if (!layout?.enabled) return;
18961
+ const steps = session.toSession().steps;
18962
+ const sum = (key) => {
18963
+ const values = steps.flatMap((step) => typeof step.usage?.[key] === "number" ? [step.usage[key]] : []);
18964
+ return values.length ? values.reduce((a, b) => a + b, 0) : void 0;
18965
+ };
18966
+ const context = session.contextStatus();
18967
+ layout.updateDetails({
18968
+ contextUsed: context.estimatedPromptTokens,
18969
+ contextMax: context.contextTokens,
18970
+ provider: provider.name,
18971
+ model: session.model,
18972
+ mode: session.mode,
18973
+ tokens: sum("totalTokens"),
18974
+ inputTokens: sum("inputTokens"),
18975
+ outputTokens: sum("outputTokens"),
18976
+ steps: steps.length
18977
+ });
18978
+ };
17608
18979
  const writeCheckpoint = createSessionCheckpointWriter(workspace, (message) => {
17609
18980
  console.error(chalk11.yellow(message));
17610
18981
  });
17611
18982
  const persistSession = async (target = session) => {
18983
+ refreshDetails();
17612
18984
  const snapshot = target.toSession();
17613
18985
  if (!hasSavedSessionContent(snapshot)) return null;
17614
18986
  return writeCheckpoint(snapshot);
@@ -17621,7 +18993,8 @@ async function runRepl(options, initialTask = "") {
17621
18993
  workspace,
17622
18994
  useNativeTools ? "native tools" : "json protocol",
17623
18995
  describeBudget(mode, toolSelection, registry),
17624
- diagnosticPath2
18996
+ diagnosticPath2,
18997
+ provider.name.toLowerCase() === "ultrac" ? await readAccountPlan(await resolveUltracCredentials()) : void 0
17625
18998
  );
17626
18999
  if (mcpRuntime && mcpRuntime.statuses.length > 0) {
17627
19000
  console.log(chalk11.bold("\nMCP servers"));
@@ -17630,29 +19003,66 @@ async function runRepl(options, initialTask = "") {
17630
19003
  try {
17631
19004
  await session.start();
17632
19005
  } catch (error) {
19006
+ layout?.close();
17633
19007
  throw new Error(`Could not start the session: ${errorMessage2(error)}`, { cause: error });
17634
19008
  }
17635
19009
  const update = await updateCheck;
17636
- if (update?.updateAvailable && update.latestVersion) {
17637
- console.log(chalk11.yellow(`
17638
- \u2191 Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
17639
- console.log(chalk11.gray(" Run `ultrac update` after this session."));
17640
- }
17641
- const rl = createInterface3({ input: stdin5, output: stdout5 });
19010
+ if (!stdin5.isTTY && update?.updateAvailable && update.latestVersion) {
19011
+ console.log(chalk11.yellow(`Update available: ${update.currentVersion} \u2192 ${update.latestVersion}. Run ultrac update.`));
19012
+ }
19013
+ const rl = createInterface3({ input: stdin5, output: layout?.enabled ? layout.outputProxy : stdout5 });
19014
+ layout?.attachReadline(rl);
19015
+ layout?.setInputFormatter(formatImageInput);
19016
+ refreshDetails();
19017
+ const builtinEntries = builtinSlashEntries(BUILTIN_SLASH_COMMANDS, HELP);
19018
+ const slashCompletion = layout?.enabled ? attachSlashCompletion(rl, async () => {
19019
+ const [commands, skills] = await Promise.all([listSkillCommands(workspace), listSkills(workspace)]);
19020
+ const declared = new Set(commands.commands.map((command) => command.skillName));
19021
+ return [
19022
+ ...builtinEntries,
19023
+ ...commands.commands.map((command) => ({ text: `/${command.command}`, label: `/${command.command}`, description: command.description, skill: true })),
19024
+ ...skills.filter((skill) => !declared.has(skill.name)).map((skill) => ({ text: `/skill ${skill.name}`, label: `/skill ${skill.name}`, description: skill.description, skill: true }))
19025
+ ];
19026
+ }, (rows, index) => layout.setSuggestions(rows, index), builtinEntries) : void 0;
17642
19027
  const reader = new LineReader(rl);
17643
19028
  const question = async (prompt, signal) => {
17644
- stdout5.write(prompt);
17645
- return reader.nextFresh(signal);
19029
+ slashCompletion?.suspend();
19030
+ try {
19031
+ if (layout?.enabled) {
19032
+ console.log(prompt);
19033
+ layout.setPrompt("\u203A ");
19034
+ } else stdout5.write(prompt);
19035
+ return await reader.nextFresh(signal);
19036
+ } finally {
19037
+ layout?.setPrompt("\u203A ");
19038
+ slashCompletion?.resume();
19039
+ }
17646
19040
  };
17647
19041
  const drainQueuedMessages = () => reader.drainQueuedMessages();
19042
+ let autonomousRounds = 1;
17648
19043
  setAskUserRuntime(async (text, options2, signal) => {
17649
- console.log(chalk11.cyan(`
19044
+ console.log(chalk11.white(`
17650
19045
  ? ${text}`));
17651
- if (options2?.length) {
17652
- options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
17653
- console.log(chalk11.gray(" (type a number, or answer in your own words)"));
19046
+ let rawAnswer;
19047
+ if (options2?.length && layout?.enabled) {
19048
+ slashCompletion?.suspend();
19049
+ layout.setPrompt("\u203A ");
19050
+ let choices;
19051
+ try {
19052
+ choices = attachInteractiveChoices(rl, options2, { signal, output: stdout5, prompt: "\u203A ", maxRow: () => layout.contentRows });
19053
+ rawAnswer = await reader.nextFresh(signal);
19054
+ } finally {
19055
+ choices?.cleanup();
19056
+ layout.setPrompt("\u203A ");
19057
+ slashCompletion?.resume();
19058
+ }
19059
+ } else {
19060
+ if (options2?.length) {
19061
+ options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
19062
+ console.log(chalk11.gray(" (type a number, or answer in your own words)"));
19063
+ }
19064
+ rawAnswer = await question(chalk11.white(" \u203A "), signal);
17654
19065
  }
17655
- const rawAnswer = await question(chalk11.cyan(" \u203A "), signal);
17656
19066
  if (signal?.aborted) return null;
17657
19067
  const answer = (rawAnswer ?? "").trim();
17658
19068
  const picked = Number.parseInt(answer, 10);
@@ -17672,7 +19082,7 @@ async function runRepl(options, initialTask = "") {
17672
19082
  rl.on("SIGINT", exitSession);
17673
19083
  const onKeypress = (_input, key) => {
17674
19084
  if (!key || key.name !== "escape" || key.ctrl || key.meta) return;
17675
- handleReplEscape(turnAbort);
19085
+ if (!slashCompletion?.handledEscape()) handleReplEscape(turnAbort);
17676
19086
  };
17677
19087
  if (stdin5.isTTY) {
17678
19088
  emitKeypressEvents(stdin5, rl);
@@ -17686,31 +19096,51 @@ async function runRepl(options, initialTask = "") {
17686
19096
  process.on("SIGTERM", terminate);
17687
19097
  const withInterrupt = async (run) => {
17688
19098
  turnAbort = new AbortController();
19099
+ layout?.updateDetails({ status: "Running" });
17689
19100
  try {
17690
19101
  return await run(turnAbort.signal);
17691
19102
  } finally {
17692
19103
  turnAbort = void 0;
19104
+ refreshDetails();
19105
+ layout?.updateDetails({ status: "Idle" });
17693
19106
  }
17694
19107
  };
17695
19108
  try {
19109
+ if (update) {
19110
+ const decision = await withInterrupt((signal) => offerUpdatePrompt(update, {
19111
+ interactive: Boolean(stdin5.isTTY && stdout5.isTTY),
19112
+ signal,
19113
+ ask: (text, choices, askSignal) => getAskUserRuntime()(text, [...choices], askSignal)
19114
+ }));
19115
+ if (decision === "update_now") {
19116
+ layout?.close();
19117
+ rl.close();
19118
+ await installVerifiedVersion(packageInfo.name, update.latestVersion);
19119
+ console.log(`Updated to ${update.latestVersion}. Run ultrac again to use the new version.`);
19120
+ return;
19121
+ }
19122
+ }
17696
19123
  if (initialTask) {
17697
- console.log(chalk11.cyan(`
17698
- \u203A ${initialTask}`));
17699
- await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing));
19124
+ console.log(chalk11.white(`
19125
+ \u203A ${formatImageInput(initialTask).text}`));
19126
+ await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
17700
19127
  }
17701
19128
  while (running) {
17702
- if (stdout5.isTTY) {
19129
+ if (layout?.enabled) {
19130
+ layout.setPrompt("\u203A ");
19131
+ } else if (stdout5.isTTY) {
17703
19132
  const composer = renderComposer(stdout5.columns, true);
17704
19133
  console.log(composer.frame);
17705
19134
  stdout5.write(composer.prompt);
17706
19135
  } else {
17707
- stdout5.write(chalk11.cyan("\n\u203A "));
19136
+ stdout5.write(chalk11.white("\n\u203A "));
17708
19137
  }
17709
19138
  const line = await reader.next();
17710
19139
  if (line === null) break;
19140
+ if (layout?.enabled && line.trim()) console.log(`\u203A ${formatImageInput(line).text.replace(/[\x00-\x1f\x7f]/g, " ")}`);
17711
19141
  const raw = line.trim();
17712
19142
  if (!raw) continue;
17713
- if (raw.startsWith("/")) {
19143
+ if (raw.startsWith("/") && !hasDroppedImage(raw)) {
17714
19144
  const previousSession = session;
17715
19145
  const slashCommand = raw.slice(1).trim().split(/\s+/, 1)[0]?.toLowerCase();
17716
19146
  const handled = await withInterrupt(async (signal) => {
@@ -17723,7 +19153,7 @@ async function runRepl(options, initialTask = "") {
17723
19153
  resetContextBudgetNotices();
17724
19154
  console.log(chalk11.gray("Started a fresh conversation."));
17725
19155
  }, async (prompt) => {
17726
- await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing);
19156
+ await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds, false);
17727
19157
  }, signal, async (id) => {
17728
19158
  const lineage = await loadSessionLineage(workspace, id);
17729
19159
  const restored = createSession({ model: session.model, mode: session.mode, permissions: session.currentPermissions });
@@ -17732,7 +19162,11 @@ async function runRepl(options, initialTask = "") {
17732
19162
  session = restored;
17733
19163
  resetContextBudgetNotices();
17734
19164
  console.log(`Restored ${lineage.sessions.length} saved session(s). Permissions were kept from the current session.`);
17735
- });
19165
+ }, { mcpStatus: () => mcpRuntime?.statuses ?? [], autonomous: (setting) => {
19166
+ if (setting === "on") autonomousRounds = 5;
19167
+ if (setting === "off") autonomousRounds = 1;
19168
+ return autonomousRounds;
19169
+ } });
17736
19170
  });
17737
19171
  if (handled === "exit") {
17738
19172
  running = false;
@@ -17741,7 +19175,9 @@ async function runRepl(options, initialTask = "") {
17741
19175
  setSubagentRuntime({ provider, model: session.model, mode: session.mode, registry });
17742
19176
  if (handled === "unhandled") {
17743
19177
  try {
17744
- await runSkillCommand(session, raw, workspace, question);
19178
+ await withInterrupt((signal) => runSkillCommand(session, raw, workspace, question, async (prompt) => {
19179
+ await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds, false);
19180
+ }));
17745
19181
  } finally {
17746
19182
  await persistSession();
17747
19183
  }
@@ -17752,26 +19188,51 @@ async function runRepl(options, initialTask = "") {
17752
19188
  }
17753
19189
  continue;
17754
19190
  }
17755
- await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing));
19191
+ await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
17756
19192
  }
17757
19193
  } finally {
19194
+ slashCompletion?.cleanup();
17758
19195
  process.off("SIGTERM", terminate);
17759
19196
  if (stdin5.isTTY) stdin5.off("keypress", onKeypress);
17760
19197
  setAskUserRuntime(void 0);
19198
+ setActivitySink(void 0);
17761
19199
  rl.close();
17762
- await saveTranscript(session, writeCheckpoint);
17763
- mcpRuntime?.close();
19200
+ try {
19201
+ await saveTranscript(session, writeCheckpoint);
19202
+ } finally {
19203
+ mcpRuntime?.close();
19204
+ layout?.close();
19205
+ }
17764
19206
  }
17765
19207
  console.log(chalk11.gray("Goodbye."));
17766
19208
  }
17767
- async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing) {
19209
+ async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop = true) {
19210
+ const activity = isGhostEnabled(session) ? void 0 : getActivitySink();
19211
+ const entries = [];
19212
+ let thinkingMs = 0;
19213
+ let thinkingStarted;
19214
+ let turnFailed = false;
19215
+ const refreshActivity = (finished = false) => activity?.({ entries: [...entries], thinkingMs, finished, failed: turnFailed });
19216
+ refreshActivity();
17768
19217
  const spinner = new Spinner();
19218
+ const workSpinner = new Spinner("working");
19219
+ const activeTools = /* @__PURE__ */ new Map();
19220
+ let permissionPrompts = 0;
19221
+ const renderWorking = () => {
19222
+ workSpinner.stop();
19223
+ const active = [...activeTools.entries()].at(-1);
19224
+ if (!active || quiet || permissionPrompts || [...activeTools.keys()].some((call) => call.tool === "ask_user")) return;
19225
+ workSpinner.update(`working \xB7 ${toolActivityLabel(active[0])}${activeTools.size > 1 ? ` (+${activeTools.size - 1} active)` : ""}`);
19226
+ workSpinner.start(isComposing, active[1]);
19227
+ };
17769
19228
  const quiet = isGhostEnabled(session);
17770
19229
  const detachProgress = quiet ? () => {
17771
19230
  } : attachProgressUi();
17772
19231
  let deltaChars = 0;
17773
19232
  try {
17774
- await session.ask(input, {
19233
+ const prepared = allowImageDrop ? await prepareImageInput(input) : { text: input };
19234
+ await session.ask(prepared.text, {
19235
+ ...prepared.image ? { image: prepared.image } : {},
17775
19236
  ...signal ? { signal } : {},
17776
19237
  ...persist ? {
17777
19238
  onCheckpoint: async () => {
@@ -17784,13 +19245,22 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
17784
19245
  },
17785
19246
  onModelStart: () => {
17786
19247
  deltaChars = 0;
17787
- if (!quiet) spinner.start(isComposing);
19248
+ thinkingStarted = performance.now();
19249
+ if (!quiet) {
19250
+ spinner.update("thinking");
19251
+ spinner.start(isComposing);
19252
+ }
17788
19253
  },
17789
19254
  onModelDelta: (text) => {
17790
19255
  deltaChars += text.length;
17791
19256
  if (!quiet) spinner.update(`thinking (${deltaChars} chars)`);
17792
19257
  },
17793
- onModelSettled: () => spinner.stop(),
19258
+ onModelSettled: () => {
19259
+ if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
19260
+ thinkingStarted = void 0;
19261
+ spinner.stop(activity ? void 0 : signal?.aborted ? "Thinking stopped after" : "Thought for");
19262
+ refreshActivity();
19263
+ },
17794
19264
  onAssistantMessage: (message) => {
17795
19265
  if (message.trim()) {
17796
19266
  console.log(`
@@ -17798,9 +19268,24 @@ ${message.trim()}`);
17798
19268
  }
17799
19269
  },
17800
19270
  onToolStart: (call) => {
17801
- if (!quiet) console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
19271
+ activeTools.set(call, performance.now());
19272
+ renderWorking();
19273
+ if (!stdout5.isTTY && !quiet) console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
19274
+ },
19275
+ onToolResult: (call, result) => {
19276
+ workSpinner.stop();
19277
+ const started = activeTools.get(call);
19278
+ activeTools.delete(call);
19279
+ const duration = started === void 0 ? 0 : performance.now() - started;
19280
+ entries.push(activityEntry(call, result, duration));
19281
+ refreshActivity();
19282
+ if (!activity && !quiet) {
19283
+ const dot = result.ok ? chalk11.green("\u25CF") : chalk11.red("\u25CF");
19284
+ console.log(` ${dot} ${toolActivityLabel(call)}${duration >= 1e3 ? ` \xB7 Worked for ${formatSeconds(duration)}` : ""}`);
19285
+ }
19286
+ printToolResult2(result, Boolean(activity));
19287
+ renderWorking();
17802
19288
  },
17803
- onToolResult: (_call, result) => printToolResult2(result),
17804
19289
  ...drainQueuedMessages ? {
17805
19290
  drainQueuedMessages,
17806
19291
  onQueuedMessage: (message) => {
@@ -17824,35 +19309,65 @@ ${message.trim()}`);
17824
19309
  console.log(chalk11.gray(` ${hint}`));
17825
19310
  },
17826
19311
  requestPermission: async (call, permission) => {
17827
- const prompt = permission === "shell" && call.tool === "run_shell" ? shellApprovalPrompt(call, session.toSession().workspace) : permission === "shell" ? ` ${call.tool} wants shell access. [o]nce (this call only) / [a]lways (all shell tools for this session) / [n]o (this call) / n[e]ver (this session) \u203A ` : ` ${call.tool} wants ${permission} access. [o]nce / [a]lways / [n]o / n[e]ver \u203A `;
17828
- const rawAnswer = await question(chalk11.yellow(prompt), signal);
17829
- if (signal?.aborted || rawAnswer === null) return "deny";
17830
- const answer = rawAnswer.trim().toLowerCase();
17831
- if (answer === "a" || answer === "always") return "always";
17832
- if (answer === "o" || answer === "once" || answer === "y" || answer === "yes") return "once";
17833
- if (answer === "e" || answer === "never") {
17834
- console.log(
17835
- chalk11.gray(
17836
- ` ${permission} refused for the rest of the session \u2014 nothing will ask again. /allow ${permission} undoes it.`
17837
- )
17838
- );
17839
- return "never";
19312
+ permissionPrompts += 1;
19313
+ workSpinner.stop();
19314
+ try {
19315
+ const prompt = permission === "shell" && call.tool === "run_shell" ? shellApprovalPrompt(call, session.toSession().workspace) : permission === "shell" ? ` ${call.tool} wants shell access. [o]nce (this call only) / [a]lways (all shell tools for this session) / [n]o (this call) / n[e]ver (this session) \u203A ` : ` ${call.tool} wants ${permission} access. [o]nce / [a]lways / [n]o / n[e]ver \u203A `;
19316
+ const rawAnswer = await question(chalk11.yellow(prompt), signal);
19317
+ if (signal?.aborted || rawAnswer === null) return "deny";
19318
+ const answer = rawAnswer.trim().toLowerCase();
19319
+ if (answer === "a" || answer === "always") return "always";
19320
+ if (answer === "o" || answer === "once" || answer === "y" || answer === "yes") return "once";
19321
+ if (answer === "e" || answer === "never") {
19322
+ console.log(
19323
+ chalk11.gray(
19324
+ ` ${permission} refused for the rest of the session \u2014 nothing will ask again. /allow ${permission} undoes it.`
19325
+ )
19326
+ );
19327
+ return "never";
19328
+ }
19329
+ return "deny";
19330
+ } finally {
19331
+ permissionPrompts -= 1;
19332
+ renderWorking();
17840
19333
  }
17841
- return "deny";
17842
19334
  }
17843
19335
  });
17844
19336
  return true;
17845
19337
  } catch (error) {
19338
+ turnFailed = true;
17846
19339
  spinner.stop();
17847
19340
  printTurnError(error);
17848
19341
  return false;
17849
19342
  } finally {
19343
+ spinner.stop();
19344
+ workSpinner.stop();
19345
+ activeTools.clear();
19346
+ if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
19347
+ refreshActivity(true);
17850
19348
  detachProgress();
17851
19349
  }
17852
19350
  }
17853
- async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing) {
19351
+ async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing, maxRounds = 1, allowImageDrop = true) {
17854
19352
  try {
17855
- return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing);
19353
+ if (maxRounds === 1) return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop);
19354
+ let successful = true;
19355
+ const result = await runAutonomousLoop({
19356
+ maxRounds,
19357
+ signal,
19358
+ classifyFailure: () => ({ kind: "unknown", retryable: false, failoverable: false, reason: "Interactive provider retries already exhausted." }),
19359
+ runRound: async (round, _previous, roundSignal) => {
19360
+ successful = await runTurn(session, round === 1 ? input : AUTONOMOUS_CONTINUE_TASK, question, roundSignal, drainQueuedMessages, persist, isComposing, allowImageDrop);
19361
+ await persist(session);
19362
+ const snapshot = session.toSession();
19363
+ return successful ? snapshot : { ...snapshot, stopReason: roundSignal.aborted ? "aborted" : "error" };
19364
+ },
19365
+ onRoundEnd: (round, _snapshot, willResume) => {
19366
+ if (willResume) console.log(chalk11.gray(`Continuing unfinished work: round ${round + 1}/${maxRounds}.`));
19367
+ }
19368
+ });
19369
+ if (result.haltReason !== "completed" && result.haltReason !== "aborted") console.log(chalk11.gray(`Automatic continuation stopped: ${result.haltReason}. Session preserved.`));
19370
+ return successful;
17856
19371
  } finally {
17857
19372
  await persist(session);
17858
19373
  }
@@ -17919,11 +19434,41 @@ function slashCommandChangesSession(raw) {
17919
19434
  function errorMessage2(error) {
17920
19435
  return error instanceof Error ? error.message : String(error);
17921
19436
  }
17922
- async function handleSlashCommand(raw, session, providerName, workspace, onClear, runPrompt, signal, resume) {
19437
+ async function handleSlashCommand(raw, session, providerName, workspace, onClear, runPrompt, signal, resume, services) {
17923
19438
  const [name, ...rest] = raw.slice(1).split(/\s+/);
17924
19439
  const command = (name ?? "").toLowerCase();
17925
19440
  const argument = rest.join(" ").trim();
17926
19441
  switch (command) {
19442
+ case "doctor":
19443
+ await runDoctor(workspace, false, false);
19444
+ return "ok";
19445
+ case "mcp": {
19446
+ const statuses = services?.mcpStatus?.() ?? [];
19447
+ console.log(statuses.length ? renderMcpStatuses(statuses) : "No connected MCP servers. Configure with ultrac mcp add, approve with ultrac trust, then restart the session.");
19448
+ return "ok";
19449
+ }
19450
+ case "autonomous": {
19451
+ const setting = argument || "status";
19452
+ if (setting !== "on" && setting !== "off" && setting !== "status") {
19453
+ console.log("Usage: /autonomous on|off|status");
19454
+ return "ok";
19455
+ }
19456
+ if (!services?.autonomous) {
19457
+ console.log("Automatic continuation is available in the interactive session.");
19458
+ return "ok";
19459
+ }
19460
+ const rounds = services.autonomous(setting);
19461
+ console.log(`Automatic continuation ${rounds > 1 ? "on" : "off"}: up to ${rounds} round(s), with checkpoint, elapsed and stall limits.`);
19462
+ return "ok";
19463
+ }
19464
+ case "continue": {
19465
+ if (!runPrompt || !hasSavedSessionContent(session.toSession())) {
19466
+ console.log("No current task to continue. Start a task or use /resume <id>.");
19467
+ return "ok";
19468
+ }
19469
+ await runPrompt(`${AUTONOMOUS_CONTINUE_TASK}${argument ? ` Focus: ${argument}` : ""}`);
19470
+ return "ok";
19471
+ }
17927
19472
  case "logs": {
17928
19473
  console.log(currentDiagnosticLogPath() ?? "Diagnostic logging has not been configured for this process.");
17929
19474
  return "ok";
@@ -18133,6 +19678,8 @@ ${extra}`);
18133
19678
  }
18134
19679
  const entry = results[selected - 1];
18135
19680
  console.log(`${selected}. ${toolActivityLabel(entry.call)} (${entry.result.ok ? "ok" : "failed"})`);
19681
+ const detail = activityEntry(entry.call, entry.result, 0);
19682
+ if (detail.request) console.log(detail.request);
18136
19683
  console.log(entry.result.content);
18137
19684
  for (const warning of getMcpArchiveWarnings(entry.result)) console.log(chalk11.yellow(`Warning: ${warning}`));
18138
19685
  return "ok";
@@ -18158,6 +19705,20 @@ ${extra}`);
18158
19705
  }
18159
19706
  return "ok";
18160
19707
  }
19708
+ case "skill": {
19709
+ const [skillName, ...args] = argument.split(/\s+/);
19710
+ if (!skillName || !runPrompt) {
19711
+ console.log("Usage: /skill <name> [args]");
19712
+ return "ok";
19713
+ }
19714
+ const skill = await readSkill(workspace, skillName);
19715
+ if (!skill) {
19716
+ console.log(`No available skill named ${skillName}. Use /skills to list them.`);
19717
+ return "ok";
19718
+ }
19719
+ await runPrompt(buildSkillCommandPrompt(skill, { command: "skill", skillName: skill.name, description: skill.description, scope: skill.scope, path: skill.path }, args.join(" ")));
19720
+ return "ok";
19721
+ }
18161
19722
  case "skills": {
18162
19723
  const skills = await listSkills(workspace);
18163
19724
  if (!skills.length) {
@@ -18190,13 +19751,14 @@ ${extra}`);
18190
19751
  return "unhandled";
18191
19752
  }
18192
19753
  }
18193
- async function runSkillCommand(session, raw, workspace, question) {
19754
+ async function runSkillCommand(session, raw, workspace, question, runPrompt) {
18194
19755
  const resolved = await resolveSkillCommand(workspace, raw);
18195
19756
  if (resolved.status === "ok") {
18196
19757
  console.log(
18197
19758
  chalk11.gray(` /${resolved.command.command} \u2192 skill ${resolved.command.skillName} (${resolved.command.scope})`)
18198
19759
  );
18199
- await runTurn(session, resolved.prompt, question);
19760
+ if (runPrompt) await runPrompt(resolved.prompt);
19761
+ else await runTurn(session, resolved.prompt, question, void 0, void 0, void 0, void 0, false);
18200
19762
  return;
18201
19763
  }
18202
19764
  if (resolved.status === "conflict") {
@@ -18261,7 +19823,7 @@ var LineReader = class {
18261
19823
  const messages = [];
18262
19824
  while (this.queue.length) {
18263
19825
  const next = this.queue[0].trim();
18264
- if (next.startsWith("/")) break;
19826
+ if (next.startsWith("/") || hasDroppedImage(next)) break;
18265
19827
  this.queue.shift();
18266
19828
  if (next) messages.push(next);
18267
19829
  }
@@ -18274,8 +19836,8 @@ var LineReader = class {
18274
19836
  if (buffered !== void 0) return Promise.resolve(buffered);
18275
19837
  }
18276
19838
  if (this.closed) return Promise.resolve(null);
18277
- return new Promise((resolve15) => {
18278
- const waiter = { resolve: resolve15, ...signal ? { signal } : {} };
19839
+ return new Promise((resolve17) => {
19840
+ const waiter = { resolve: resolve17, ...signal ? { signal } : {} };
18279
19841
  if (signal) {
18280
19842
  waiter.onAbort = () => {
18281
19843
  const index = this.waiters.indexOf(waiter);
@@ -18308,11 +19870,12 @@ function handleReplExit(activeTurn) {
18308
19870
  activeTurn?.abort();
18309
19871
  return "exit-session";
18310
19872
  }
18311
- function printBanner(version, providerName, model, mode, workspace, protocol, budget, diagnosticPath2) {
19873
+ function printBanner(version, providerName, model, mode, workspace, protocol, budget, diagnosticPath2, planName) {
18312
19874
  console.log(renderWelcome(
18313
19875
  {
18314
19876
  version,
18315
19877
  providerName,
19878
+ planName,
18316
19879
  model,
18317
19880
  mode,
18318
19881
  workspace,
@@ -18337,9 +19900,9 @@ function describeAdvertisedTools(mode, selection, registry = createDefaultToolRe
18337
19900
  names: advertised.map((tool) => tool.name)
18338
19901
  };
18339
19902
  }
18340
- function printToolResult2(result) {
19903
+ function printToolResult2(result, collapsed = false) {
18341
19904
  const failure = toolFailurePreview(result);
18342
- if (failure) console.log(chalk11.red(` Failed: ${failure}`));
19905
+ if (failure && !collapsed) console.log(chalk11.red(` Failed: ${failure}`));
18343
19906
  for (const warning of getMcpArchiveWarnings(result)) {
18344
19907
  console.log(chalk11.yellow(indent3(`Warning: ${warning}`)));
18345
19908
  }
@@ -18370,148 +19933,6 @@ function indent3(value) {
18370
19933
  // src/commands/run.ts
18371
19934
  import { basename as basename3 } from "path";
18372
19935
  import chalk12 from "chalk";
18373
-
18374
- // src/core/autonomous.ts
18375
- import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
18376
- var AUTONOMOUS_CONTINUE_TASK = "Continue the task from where the previous round stopped. Check what is already done, finish the rest, and verify it.";
18377
- var DEFAULT_AUTONOMOUS_MAX_ROUNDS = 5;
18378
- var AUTONOMOUS_MAX_STEPS = 200;
18379
- var DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS = 6 * 60 * 60 * 1e3;
18380
- var DEFAULT_AUTONOMOUS_BASE_DELAY_MS = 1e3;
18381
- var DEFAULT_AUTONOMOUS_MAX_DELAY_MS = 3e4;
18382
- var DEFAULT_AUTONOMOUS_STALL_ROUNDS = 3;
18383
- function shouldResume(stopReason) {
18384
- return stopReason === "max-steps" || stopReason === "error";
18385
- }
18386
- function boundedInteger(value, name, min, max) {
18387
- if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${name} must be an integer from ${min} to ${max}.`);
18388
- return value;
18389
- }
18390
- function abortableSleep(milliseconds, signal) {
18391
- signal.throwIfAborted();
18392
- return new Promise((resolve15, reject) => {
18393
- const timer = setTimeout(() => {
18394
- signal.removeEventListener("abort", abort);
18395
- resolve15();
18396
- }, milliseconds);
18397
- const abort = () => {
18398
- clearTimeout(timer);
18399
- signal.removeEventListener("abort", abort);
18400
- reject(signal.reason);
18401
- };
18402
- signal.addEventListener("abort", abort, { once: true });
18403
- if (signal.aborted) abort();
18404
- });
18405
- }
18406
- function progress(session) {
18407
- const hash = createHash7("sha256");
18408
- let completedTools = 0;
18409
- hash.update(session.finalMessage ?? "");
18410
- for (const step of session.steps ?? []) {
18411
- hash.update(step.modelText ?? "");
18412
- for (const tool of step.toolResults ?? []) {
18413
- completedTools += 1;
18414
- hash.update(tool.call.tool);
18415
- hash.update(tool.result.ok ? "ok" : "failed");
18416
- hash.update(tool.result.content ?? "");
18417
- try {
18418
- hash.update(JSON.stringify(tool.call.input));
18419
- } catch {
18420
- hash.update("unserializable-input");
18421
- }
18422
- }
18423
- }
18424
- return { fingerprint: hash.digest("hex"), completedTools };
18425
- }
18426
- async function runAutonomousLoop(options) {
18427
- boundedInteger(options.maxRounds, "maxRounds", 1, 1e3);
18428
- const maxElapsedMs = boundedInteger(options.maxElapsedMs ?? DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS, "maxElapsedMs", 1, 7 * 24 * 60 * 60 * 1e3);
18429
- const baseDelay = boundedInteger(options.baseDelayMs ?? DEFAULT_AUTONOMOUS_BASE_DELAY_MS, "baseDelayMs", 0, 3e5);
18430
- const maxDelay = boundedInteger(options.maxDelayMs ?? DEFAULT_AUTONOMOUS_MAX_DELAY_MS, "maxDelayMs", baseDelay, 3e5);
18431
- const stallLimit = boundedInteger(options.stallRounds ?? DEFAULT_AUTONOMOUS_STALL_ROUNDS, "stallRounds", 1, 1e3);
18432
- const now = options.now ?? (() => performance.now());
18433
- const started = now();
18434
- const elapsed = () => Math.max(0, now() - started);
18435
- const budget = new AbortController();
18436
- const deadline = setTimeout(() => budget.abort(new DOMException("Autonomous elapsed budget reached.", "TimeoutError")), maxElapsedMs);
18437
- deadline.unref?.();
18438
- const signal = options.signal ? AbortSignal.any([options.signal, budget.signal]) : budget.signal;
18439
- const sleep2 = options.sleep ?? abortableSleep;
18440
- const id = randomUUID5();
18441
- let previous;
18442
- let lastFingerprint;
18443
- let stalls = 0;
18444
- let consecutiveErrors = 0;
18445
- try {
18446
- signal.throwIfAborted();
18447
- for (let round = 1; ; round += 1) {
18448
- const roundStart = now();
18449
- diagnosticEvent("round.start", { id, round, maxRounds: options.maxRounds, elapsedMs: elapsed() });
18450
- let session;
18451
- try {
18452
- session = await options.runRound(round, previous, signal);
18453
- } catch (error) {
18454
- diagnosticEvent("round.end", { id, round, reason: signal.aborted ? "aborted" : "exception", elapsedMs: elapsed(), durationMs: Math.max(0, now() - roundStart) });
18455
- throw error;
18456
- }
18457
- const current = progress(session);
18458
- const unchanged = lastFingerprint !== void 0 && current.fingerprint === lastFingerprint;
18459
- stalls = unchanged ? stalls + 1 : 0;
18460
- lastFingerprint = current.fingerprint;
18461
- const classification = session.stopReason === "error" ? options.classifyFailure?.(session) : void 0;
18462
- consecutiveErrors = session.stopReason === "error" ? consecutiveErrors + 1 : 0;
18463
- let haltReason;
18464
- if (session.stopReason === "completed") haltReason = "completed";
18465
- else if (budget.signal.aborted || elapsed() >= maxElapsedMs) haltReason = "elapsed_budget";
18466
- else if (signal.aborted || session.stopReason === "aborted") haltReason = "aborted";
18467
- else if (classification && !classification.retryable) haltReason = "permanent_error";
18468
- else if (!shouldResume(session.stopReason)) haltReason = "stopped";
18469
- else if (stalls >= stallLimit) haltReason = "stalled";
18470
- else if (round >= options.maxRounds) haltReason = "max_rounds";
18471
- const retryDelay = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
18472
- if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay >= maxElapsedMs) haltReason = "elapsed_budget";
18473
- diagnosticEvent("round.end", {
18474
- id,
18475
- round,
18476
- sessionId: session.id,
18477
- reason: haltReason ?? session.stopReason,
18478
- failureKind: classification?.kind,
18479
- attempt: consecutiveErrors,
18480
- completedTools: current.completedTools,
18481
- elapsedMs: elapsed(),
18482
- durationMs: Math.max(0, now() - roundStart)
18483
- });
18484
- if (haltReason === "stalled") diagnosticEvent("round.stall", { id, round, repeats: stalls, completedTools: current.completedTools, elapsedMs: elapsed() });
18485
- await options.onRoundEnd?.(round, session, !haltReason);
18486
- if (haltReason) return { session, rounds: round, haltReason };
18487
- if (session.stopReason === "error") {
18488
- const delayMs = retryDelay;
18489
- if (elapsed() + delayMs >= maxElapsedMs) return { session, rounds: round, haltReason: "elapsed_budget" };
18490
- diagnosticEvent("round.retry", {
18491
- id,
18492
- round,
18493
- nextRound: round + 1,
18494
- attempt: consecutiveErrors,
18495
- failureKind: classification?.kind ?? "unclassified",
18496
- delayMs,
18497
- elapsedMs: elapsed()
18498
- });
18499
- try {
18500
- await sleep2(delayMs, signal);
18501
- } catch (error) {
18502
- if (!signal.aborted) throw error;
18503
- return { session, rounds: round, haltReason: budget.signal.aborted ? "elapsed_budget" : "aborted" };
18504
- }
18505
- }
18506
- if (signal.aborted || elapsed() >= maxElapsedMs) return { session, rounds: round, haltReason: budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted" };
18507
- previous = session;
18508
- }
18509
- } finally {
18510
- clearTimeout(deadline);
18511
- }
18512
- }
18513
-
18514
- // src/commands/run.ts
18515
19936
  function parsePositiveInt(value, flag) {
18516
19937
  const parsed = Number.parseInt(value, 10);
18517
19938
  if (!Number.isInteger(parsed) || parsed < 1 || String(parsed) !== value.trim()) {
@@ -18757,7 +20178,7 @@ ${summary}` : ""}`).catch((error) => console.error(chalk12.yellow(`Could not sen
18757
20178
  }
18758
20179
 
18759
20180
  // src/commands/sessions.ts
18760
- import { resolve as resolve12 } from "path";
20181
+ import { resolve as resolve14 } from "path";
18761
20182
  function registerSessionsCommand(program2) {
18762
20183
  const sessions = program2.command("sessions").description("Inspect saved Ultrac session transcripts");
18763
20184
  sessions.command("list").description("List saved sessions").option("-w, --workspace <path>", "Workspace path", ".").option("--json", "Print JSON output", false).option("--max-results <number>", "Maximum sessions to show", "20").action(async (options) => {
@@ -18774,7 +20195,7 @@ function registerSessionsCommand(program2) {
18774
20195
  );
18775
20196
  });
18776
20197
  sessions.command("export").description("Export a saved session as an auditable episode package").argument("<id>", "Session id").option("-w, --workspace <path>", "Workspace path", ".").option("--format <format>", "Output format: markdown or json", "markdown").option("--max-chars <number>", "Maximum characters per model/tool text block in markdown", "4000").option("--outcome <outcome>", "Outcome status: pass, fail, blocked, or unknown", "unknown").option("--notes <text>", "Outcome notes to include in the episode package").option("--out <path>", "Write export to a path inside the workspace instead of stdout").action(async (id, options) => {
18777
- const workspace = resolve12(options.workspace);
20198
+ const workspace = resolve14(options.workspace);
18778
20199
  const format3 = parseExportFormat(options.format);
18779
20200
  const maxChars = parsePositiveInt2(options.maxChars);
18780
20201
  const outcome = parseOutcomeStatus(options.outcome);
@@ -18791,7 +20212,7 @@ function registerSessionsCommand(program2) {
18791
20212
  });
18792
20213
  }
18793
20214
  async function runSessionTool(tool, input, options) {
18794
- const workspace = resolve12(options.workspace);
20215
+ const workspace = resolve14(options.workspace);
18795
20216
  const result = await createDefaultToolRegistry().execute(
18796
20217
  { tool, input },
18797
20218
  { workspace, permissions: { write: false, shell: false, network: false } }
@@ -18967,11 +20388,11 @@ function collectToolArtifacts(session, toolName) {
18967
20388
  }
18968
20389
 
18969
20390
  // src/commands/skills.ts
18970
- import { resolve as resolve13 } from "path";
20391
+ import { resolve as resolve15 } from "path";
18971
20392
  function registerSkillsCommand(program2) {
18972
20393
  const skills = program2.command("skills").description("Manage Ultrac skills (SKILL.md playbooks)");
18973
20394
  skills.command("list").description("List installed skills").option("-w, --workspace <path>", "Workspace path", ".").action(async (options) => {
18974
- const installed = await listSkills(resolve13(options.workspace));
20395
+ const installed = await listSkills(resolve15(options.workspace));
18975
20396
  if (!installed.length) {
18976
20397
  console.log("No skills installed. Create one with: ultrac skills create <name> --description <text>");
18977
20398
  return;
@@ -18982,7 +20403,7 @@ function registerSkillsCommand(program2) {
18982
20403
  }
18983
20404
  });
18984
20405
  skills.command("show").description("Show a skill's full instructions").argument("<name>", "Skill name").option("-w, --workspace <path>", "Workspace path", ".").action(async (name, options) => {
18985
- const skill = await readSkill(resolve13(options.workspace), name);
20406
+ const skill = await readSkill(resolve15(options.workspace), name);
18986
20407
  if (!skill) throw new Error(`No skill named "${name}".`);
18987
20408
  console.log(`# ${skill.name} (${skill.scope})`);
18988
20409
  console.log(`Path: ${skill.path}`);
@@ -18997,7 +20418,7 @@ function registerSkillsCommand(program2) {
18997
20418
  if (options.argumentHint && !command) throw new Error("--argument-hint requires --command.");
18998
20419
  const argumentHint = options.argumentHint?.trim();
18999
20420
  if (argumentHint && (argumentHint.length > 256 || /[\r\n]/.test(argumentHint))) throw new Error("Argument hints must be one line, at most 256 characters.");
19000
- const skill = await createSkill(resolve13(options.workspace), {
20421
+ const skill = await createSkill(resolve15(options.workspace), {
19001
20422
  name,
19002
20423
  description: options.description,
19003
20424
  whenToUse: options.when,
@@ -19012,7 +20433,7 @@ function registerSkillsCommand(program2) {
19012
20433
  }
19013
20434
  );
19014
20435
  skills.command("path").description("Print the skills directory path for a scope").option("-w, --workspace <path>", "Workspace path", ".").option("--scope <scope>", "global or project", "project").action((options) => {
19015
- console.log(skillsDir(parseScope2(options.scope), resolve13(options.workspace)));
20436
+ console.log(skillsDir(parseScope2(options.scope), resolve15(options.workspace)));
19016
20437
  });
19017
20438
  }
19018
20439
  function parseScope2(scope) {
@@ -19314,13 +20735,13 @@ var FollowAbortedError = class extends Error {
19314
20735
  }
19315
20736
  };
19316
20737
  function defaultSleep2(ms, signal) {
19317
- return new Promise((resolve15) => {
19318
- if (signal?.aborted) return resolve15();
20738
+ return new Promise((resolve17) => {
20739
+ if (signal?.aborted) return resolve17();
19319
20740
  const timer = setTimeout(done, ms);
19320
20741
  function done() {
19321
20742
  signal?.removeEventListener("abort", done);
19322
20743
  clearTimeout(timer);
19323
- resolve15();
20744
+ resolve17();
19324
20745
  }
19325
20746
  signal?.addEventListener("abort", done, { once: true });
19326
20747
  });
@@ -19816,11 +21237,11 @@ async function interrupted(r, runId, options, printed) {
19816
21237
  }
19817
21238
  async function resolveDeps(options, deps) {
19818
21239
  if (options.baseUrl) setBaseUrlFlag(options.baseUrl);
19819
- const resolve15 = deps.credentials ?? (async () => {
21240
+ const resolve17 = deps.credentials ?? (async () => {
19820
21241
  const credentials2 = await resolveUltracCredentials();
19821
21242
  return credentials2 ? { apiKey: credentials2.apiKey, baseUrl: credentials2.baseUrl } : null;
19822
21243
  });
19823
- const credentials = await resolve15();
21244
+ const credentials = await resolve17();
19824
21245
  if (!credentials) {
19825
21246
  throw new Error("Not signed in to Ultrac. Run `ultrac login` (or set ULTRAC_API_KEY).");
19826
21247
  }
@@ -19919,7 +21340,7 @@ ${run.output}${run.output.endsWith("\n") ? "" : "\n"}`;
19919
21340
  }
19920
21341
 
19921
21342
  // src/commands/tools.ts
19922
- import { resolve as resolve14 } from "path";
21343
+ import { resolve as resolve16 } from "path";
19923
21344
  import chalk14 from "chalk";
19924
21345
  function registerToolsCommand(program2) {
19925
21346
  const toolsCommand = program2.command("tools");
@@ -19954,7 +21375,7 @@ ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${ser
19954
21375
  const result = await createDefaultToolRegistry().execute(
19955
21376
  { tool, input },
19956
21377
  {
19957
- workspace: resolve14(options.workspace),
21378
+ workspace: resolve16(options.workspace),
19958
21379
  permissions: {
19959
21380
  write: options.allowWrite,
19960
21381
  shell: options.allowShell,