@spinalfluid/ultrac 0.1.11 → 0.1.18

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 +30 -10
  2. package/dist/cli.js +1047 -284
  3. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -399,6 +399,7 @@ function modePermissionAccessError(mode, toolName, permission) {
399
399
  var EchoProvider = class {
400
400
  name = "echo";
401
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 });
402
403
  const lastUserMessage = [...request.messages].reverse().find((message) => message.role === "user");
403
404
  return {
404
405
  text: JSON.stringify(
@@ -1136,6 +1137,9 @@ function pinCurrentTurn(units, isUser) {
1136
1137
  }
1137
1138
  }
1138
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
+ }
1139
1143
  return JSON.stringify(message)?.length ?? 0;
1140
1144
  }
1141
1145
  var noticeSink = (message) => {
@@ -1143,8 +1147,8 @@ var noticeSink = (message) => {
1143
1147
  `);
1144
1148
  };
1145
1149
  var noticed = false;
1146
- function setContextBudgetNotice(sink) {
1147
- noticeSink = sink;
1150
+ function setContextBudgetNotice(sink2) {
1151
+ noticeSink = sink2;
1148
1152
  }
1149
1153
  function resetContextBudgetNotices() {
1150
1154
  noticed = false;
@@ -1170,7 +1174,7 @@ var COMPACTION_MARKER = "[compacted-context]";
1170
1174
  var SUMMARY_PREAMBLE = "The earlier part of this session was removed to fit the context window and replaced by the record below. It is a record of what happened, not a new instruction. Continue the work it describes.";
1171
1175
  var DEFAULT_MAX_SUMMARY_CHARS = 8e3;
1172
1176
  var MIN_SUMMARY_CHARS = 400;
1173
- var DEFAULT_SUMMARY_TIMEOUT_MS = 3e4;
1177
+ var DEFAULT_SUMMARY_TIMEOUT_MS = 12e4;
1174
1178
  var DEFAULT_MAX_TRANSCRIPT_CHARS = 6e4;
1175
1179
  var DEFAULT_MAX_MESSAGE_CHARS = 4e3;
1176
1180
  function isCompactionSummary(message) {
@@ -2044,6 +2048,10 @@ var UltracProvider = class {
2044
2048
  if (chunk.choices[0]?.finish_reason) finishReason = chunk.choices[0].finish_reason;
2045
2049
  const delta = chunk.choices[0]?.delta;
2046
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?.();
2047
2055
  if (delta.content) content += delta.content;
2048
2056
  for (const part of delta.tool_calls ?? []) {
2049
2057
  const call = calls.get(part.index) ?? { id: "", name: "", arguments: "" };
@@ -2199,8 +2207,8 @@ async function validateSnapshotWithin(snapshot, timeoutMs) {
2199
2207
  try {
2200
2208
  return await Promise.race([
2201
2209
  snapshot.validate(),
2202
- new Promise((resolve15) => {
2203
- timer = setTimeout(() => resolve15(void 0), timeoutMs);
2210
+ new Promise((resolve17) => {
2211
+ timer = setTimeout(() => resolve17(void 0), timeoutMs);
2204
2212
  })
2205
2213
  ]);
2206
2214
  } finally {
@@ -2264,8 +2272,8 @@ async function settleWithin(promise, timeoutMs) {
2264
2272
  try {
2265
2273
  await Promise.race([
2266
2274
  Promise.resolve(promise).catch(() => void 0),
2267
- new Promise((resolve15) => {
2268
- timer = setTimeout(resolve15, timeoutMs);
2275
+ new Promise((resolve17) => {
2276
+ timer = setTimeout(resolve17, timeoutMs);
2269
2277
  })
2270
2278
  ]);
2271
2279
  } finally {
@@ -2309,10 +2317,11 @@ function toChatMessages(request) {
2309
2317
  ];
2310
2318
  }
2311
2319
  function toChatMessage(message) {
2312
- return {
2313
- role: message.role === "assistant" ? "assistant" : "user",
2314
- content: message.content
2315
- };
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 };
2316
2325
  }
2317
2326
  function toNativeChatMessage(message) {
2318
2327
  if (message.role === "tool") {
@@ -2332,6 +2341,10 @@ function toNativeChatMessage(message) {
2332
2341
  }))
2333
2342
  };
2334
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
+ ] };
2335
2348
  return { role: message.role, content: message.content };
2336
2349
  }
2337
2350
  function toOpenAiTool(tool) {
@@ -2482,7 +2495,7 @@ var FallbackConfigSchema = z3.object({
2482
2495
  backoffFactor: z3.number().min(1).max(10).default(2),
2483
2496
  /** Randomize backoff to avoid synchronized retries. */
2484
2497
  jitter: z3.boolean().default(true),
2485
- /** 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. */
2486
2499
  requestTimeoutMs: z3.number().int().min(0).max(6e5).default(12e4)
2487
2500
  });
2488
2501
  var ProviderTimeoutError = class extends Error {
@@ -2571,6 +2584,8 @@ function classifyProviderError(error) {
2571
2584
  return transient("timeout", "Provider request timed out.");
2572
2585
  }
2573
2586
  const status = extractStatus(error);
2587
+ const busy = [...errorChain(error)].some((current) => readString(current, "code") === "upstream_busy");
2588
+ if (busy) return { ...transient("server", "Model capacity or admission is temporarily unavailable (HTTP 503).", status ?? 503), code: "upstream_busy", retryAfterMs: retryDelay(error) ?? 5e3 };
2574
2589
  if (hasNotConfiguredCode(error)) {
2575
2590
  return terminal(
2576
2591
  "not_configured",
@@ -2585,8 +2600,8 @@ function classifyProviderError(error) {
2585
2600
  }
2586
2601
  if (typeof status === "number") {
2587
2602
  if (status === 408) return transient("timeout", "HTTP 408 request timeout.", status);
2588
- if (status === 429) return transient("rate_limit", "HTTP 429 rate limited.", status);
2589
- if (status >= 500) return transient("server", `HTTP ${status} server error.`, status);
2603
+ if (status === 429) return { ...transient("rate_limit", "HTTP 429 rate limited.", status), retryAfterMs: retryDelay(error) };
2604
+ if (status >= 500) return { ...transient("server", `HTTP ${status} server error.`, status), retryAfterMs: retryDelay(error) };
2590
2605
  if (status === 401 || status === 403) {
2591
2606
  return terminal("auth", `HTTP ${status} authentication failure.`, status);
2592
2607
  }
@@ -2627,9 +2642,24 @@ function safeClassification(classification) {
2627
2642
  ...status === void 0 ? {} : { status },
2628
2643
  retryable: classification.retryable,
2629
2644
  failoverable: classification.failoverable,
2630
- reason
2645
+ reason: classification.code === "upstream_busy" ? "Model capacity or admission is temporarily unavailable (HTTP 503)." : reason,
2646
+ ...classification.code ? { code: classification.code } : {},
2647
+ ...classification.retryAfterMs === void 0 ? {} : { retryAfterMs: classification.retryAfterMs }
2631
2648
  };
2632
2649
  }
2650
+ function retryDelay(error) {
2651
+ for (const current of errorChain(error)) {
2652
+ if (!isRecord2(current)) continue;
2653
+ const headers = current.headers;
2654
+ if (typeof headers?.get !== "function") continue;
2655
+ const raw = headers.get("retry-after");
2656
+ if (!raw) continue;
2657
+ const seconds = Number(raw);
2658
+ const ms = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(raw) - Date.now();
2659
+ if (Number.isFinite(ms) && ms >= 0) return Math.min(ms, 3e5);
2660
+ }
2661
+ return void 0;
2662
+ }
2633
2663
  function classificationReason(kind, status) {
2634
2664
  switch (kind) {
2635
2665
  case "aborted":
@@ -2723,17 +2753,17 @@ function computeBackoffDelay(attempt, options) {
2723
2753
  return Math.round(capped * (0.5 + random() * 0.5));
2724
2754
  }
2725
2755
  function defaultSleep(ms, cancel) {
2726
- return new Promise((resolve15) => {
2756
+ return new Promise((resolve17) => {
2727
2757
  if (cancel?.aborted) {
2728
- resolve15();
2758
+ resolve17();
2729
2759
  return;
2730
2760
  }
2731
- const timer = setTimeout(resolve15, ms);
2761
+ const timer = setTimeout(resolve17, ms);
2732
2762
  cancel?.addEventListener(
2733
2763
  "abort",
2734
2764
  () => {
2735
2765
  clearTimeout(timer);
2736
- resolve15();
2766
+ resolve17();
2737
2767
  },
2738
2768
  { once: true }
2739
2769
  );
@@ -2808,10 +2838,19 @@ var FallbackProvider = class {
2808
2838
  );
2809
2839
  }
2810
2840
  async chatWithTools(request) {
2811
- return this.run("chatWithTools", request.signal, request.model, (entry, model, signal) => {
2841
+ return this.run("chatWithTools", request.signal, request.model, (entry, model, signal, touch) => {
2812
2842
  if (entry.provider.supportsToolCalling && entry.provider.chatWithTools) {
2813
- return entry.provider.chatWithTools({ ...request, model, signal });
2843
+ return entry.provider.chatWithTools({
2844
+ ...request,
2845
+ model,
2846
+ signal,
2847
+ onProgress: () => {
2848
+ touch();
2849
+ request.onProgress?.();
2850
+ }
2851
+ });
2814
2852
  }
2853
+ if (request.messages.some((message) => "image" in message && message.image)) throw Object.assign(new Error("This fallback provider cannot inspect image attachments."), { status: 400 });
2815
2854
  return this.chatThroughToolProtocol(entry, model, request, signal);
2816
2855
  });
2817
2856
  }
@@ -2896,7 +2935,8 @@ var FallbackProvider = class {
2896
2935
  const result = await this.withDeadline(
2897
2936
  entry.name,
2898
2937
  signal,
2899
- (attemptSignal) => invoke(entry, model, attemptSignal)
2938
+ (attemptSignal, touch) => invoke(entry, model, attemptSignal, touch),
2939
+ operation === "chatWithTools"
2900
2940
  );
2901
2941
  this.active = target;
2902
2942
  trace.end({ outcome: "ok" });
@@ -3020,7 +3060,7 @@ var FallbackProvider = class {
3020
3060
  }
3021
3061
  }
3022
3062
  async waitBeforeRetry(operation, target, attempt, classification, signal) {
3023
- const delayMs = computeBackoffDelay(attempt, this.backoff);
3063
+ const delayMs = Math.max(computeBackoffDelay(attempt, this.backoff), classification.retryAfterMs ?? 0);
3024
3064
  const safe = safeClassification(classification);
3025
3065
  this.onRetry?.({
3026
3066
  operation,
@@ -3116,10 +3156,10 @@ var FallbackProvider = class {
3116
3156
  error.name = "AbortError";
3117
3157
  throw error;
3118
3158
  }
3119
- async withDeadline(provider, signal, run) {
3120
- const deadline = this.createDeadline(provider, signal);
3159
+ async withDeadline(provider, signal, run, inactivity = false) {
3160
+ const deadline = this.createDeadline(provider, signal, inactivity);
3121
3161
  try {
3122
- return await deadline.race(run(deadline.signal));
3162
+ return await deadline.race(run(deadline.signal, deadline.touch));
3123
3163
  } finally {
3124
3164
  deadline.clear();
3125
3165
  }
@@ -3129,31 +3169,55 @@ var FallbackProvider = class {
3129
3169
  * only ever rejects while the deadline is live, so clearing it can never leave
3130
3170
  * an unhandled rejection behind.
3131
3171
  */
3132
- createDeadline(provider, signal) {
3172
+ createDeadline(provider, signal, inactivity = false) {
3133
3173
  if (this.requestTimeoutMs <= 0) {
3134
- return { signal, race: (promise) => promise, clear: () => void 0 };
3174
+ return { signal, race: (promise) => promise, clear: () => void 0, touch: () => void 0 };
3135
3175
  }
3136
3176
  const controller = new AbortController();
3137
- const forwardAbort = () => controller.abort(signal?.reason);
3177
+ let cleared = false;
3178
+ let idleTimer;
3179
+ const hardTimer = new AbortController();
3180
+ let rejectExpired;
3181
+ const expired = new Promise((_, reject) => {
3182
+ rejectExpired = reject;
3183
+ });
3184
+ const forwardAbort = () => {
3185
+ const reason = signal?.reason ?? Object.assign(new Error("Request was aborted."), { name: "AbortError" });
3186
+ rejectExpired(reason);
3187
+ controller.abort(reason);
3188
+ };
3189
+ const expire = (timeoutMs) => {
3190
+ const error = new ProviderTimeoutError(timeoutMs, provider);
3191
+ rejectExpired(error);
3192
+ controller.abort(error);
3193
+ };
3194
+ const touch = () => {
3195
+ if (cleared || controller.signal.aborted) return;
3196
+ idleTimer?.abort();
3197
+ const timer = new AbortController();
3198
+ idleTimer = timer;
3199
+ void this.sleep(this.requestTimeoutMs, timer.signal).then(() => {
3200
+ if (!cleared && !timer.signal.aborted) expire(this.requestTimeoutMs);
3201
+ });
3202
+ };
3203
+ touch();
3204
+ if (inactivity) {
3205
+ void this.sleep(3e5, hardTimer.signal).then(() => {
3206
+ if (!cleared && !hardTimer.signal.aborted) expire(3e5);
3207
+ });
3208
+ }
3138
3209
  if (signal) {
3139
3210
  if (signal.aborted) forwardAbort();
3140
3211
  else signal.addEventListener("abort", forwardAbort, { once: true });
3141
3212
  }
3142
- let cleared = false;
3143
- const cancelDeadline = new AbortController();
3144
- const expired = new Promise((_, reject) => {
3145
- void this.sleep(this.requestTimeoutMs, cancelDeadline.signal).then(() => {
3146
- if (cleared) return;
3147
- controller.abort();
3148
- reject(new ProviderTimeoutError(this.requestTimeoutMs, provider));
3149
- });
3150
- });
3151
3213
  return {
3152
3214
  signal: controller.signal,
3153
3215
  race: (promise) => Promise.race([promise, expired]),
3216
+ touch: inactivity ? touch : () => void 0,
3154
3217
  clear: () => {
3155
3218
  cleared = true;
3156
- cancelDeadline.abort();
3219
+ idleTimer?.abort();
3220
+ hardTimer.abort();
3157
3221
  signal?.removeEventListener("abort", forwardAbort);
3158
3222
  }
3159
3223
  };
@@ -4066,7 +4130,7 @@ function runProcess(command, args, options) {
4066
4130
  });
4067
4131
  }
4068
4132
  const { env: env2, redactedEnvKeys } = createSanitizedProcessEnv();
4069
- return new Promise((resolve15, reject) => {
4133
+ return new Promise((resolve17, reject) => {
4070
4134
  const child = spawn(command, args, {
4071
4135
  cwd: options.cwd,
4072
4136
  stdio: ["pipe", "pipe", "pipe"],
@@ -4151,7 +4215,7 @@ function runProcess(command, args, options) {
4151
4215
  child.on("error", (error) => {
4152
4216
  finish(() => {
4153
4217
  if (termination === "aborted") {
4154
- resolve15({
4218
+ resolve17({
4155
4219
  stdout: stdout6.text(),
4156
4220
  stderr: stderr2.text(),
4157
4221
  started,
@@ -4173,7 +4237,7 @@ function runProcess(command, args, options) {
4173
4237
  const stdoutTruncation = stdout6.truncation();
4174
4238
  const stderrTruncation = stderr2.truncation();
4175
4239
  finish(
4176
- () => resolve15({
4240
+ () => resolve17({
4177
4241
  stdout: stdout6.text(),
4178
4242
  stderr: stderr2.text(),
4179
4243
  started,
@@ -5185,7 +5249,7 @@ function topCounts(values) {
5185
5249
  for (const value of values) {
5186
5250
  counts.set(value, (counts.get(value) ?? 0) + 1);
5187
5251
  }
5188
- return [...counts.entries()].map(([name, count4]) => ({ name, count: count4 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
5252
+ return [...counts.entries()].map(([name, count3]) => ({ name, count: count3 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
5189
5253
  }
5190
5254
  async function collectGitContext(workspace, signal) {
5191
5255
  const statusResult = await runGit(["status", "--short", "--branch"], { cwd: workspace, signal });
@@ -5377,7 +5441,7 @@ var diagnosticsTool = {
5377
5441
  const byFile = summarizeByFile(matched);
5378
5442
  const records = matched.slice(0, input.maxRecords);
5379
5443
  const checkerExit = runs[0].exitCode;
5380
- const clean = matched.length === 0;
5444
+ const clean2 = matched.length === 0;
5381
5445
  const unparsed = matched.length === 0 && runs.some((run) => run.exitCode !== 0);
5382
5446
  const rawTail = unparsed ? tail(runs.find((run) => run.exitCode !== 0)?.output ?? "", 2e3) : void 0;
5383
5447
  const lines = [
@@ -5394,9 +5458,9 @@ var diagnosticsTool = {
5394
5458
  ...byFile.length ? byFile.slice(0, input.maxFiles).map((entry) => `- ${entry.file}: ${entry.errors} error(s), ${entry.warnings} warning(s)`) : ["- none"],
5395
5459
  byFile.length > input.maxFiles ? `- ...${byFile.length - input.maxFiles} more file(s)` : void 0,
5396
5460
  "",
5397
- clean ? unparsed ? "No diagnostics parsed, but the checker exited non-zero. Raw output tail:" : "No diagnostics." : `Diagnostics (showing ${records.length} of ${matched.length}):`,
5398
- ...clean ? rawTail ? [rawTail] : [] : records.map(renderDiagnostic),
5399
- !clean && matched.length > records.length ? `...${matched.length - records.length} more suppressed` : void 0
5461
+ clean2 ? unparsed ? "No diagnostics parsed, but the checker exited non-zero. Raw output tail:" : "No diagnostics." : `Diagnostics (showing ${records.length} of ${matched.length}):`,
5462
+ ...clean2 ? rawTail ? [rawTail] : [] : records.map(renderDiagnostic),
5463
+ !clean2 && matched.length > records.length ? `...${matched.length - records.length} more suppressed` : void 0
5400
5464
  ].filter((line) => line !== void 0);
5401
5465
  return {
5402
5466
  ok: !runs.some((run) => run.aborted) && (matched.length > 0 || runs.every((run) => run.exitCode === 0)),
@@ -5676,8 +5740,8 @@ function normalizeFilters(workspace, paths) {
5676
5740
  }
5677
5741
  function normalizeDiagnosticPath(workspace, diagnostic) {
5678
5742
  try {
5679
- const relative2 = toWorkspaceRelative(workspace, resolveInsideWorkspace(workspace, diagnostic.file));
5680
- return { ...diagnostic, file: relative2.replaceAll("\\", "/") };
5743
+ const relative3 = toWorkspaceRelative(workspace, resolveInsideWorkspace(workspace, diagnostic.file));
5744
+ return { ...diagnostic, file: relative3.replaceAll("\\", "/") };
5681
5745
  } catch {
5682
5746
  return { ...diagnostic, file: diagnostic.file.replaceAll("\\", "/") };
5683
5747
  }
@@ -6090,6 +6154,21 @@ var listFilesTool = {
6090
6154
  }
6091
6155
  };
6092
6156
  var RANGE_MARKER_RESERVE_CHARS = 300;
6157
+ async function resolveReadableFile(workspace, path) {
6158
+ try {
6159
+ const target = await resolveReadablePath(workspace, path);
6160
+ const fileStat = await stat4(target);
6161
+ if (!fileStat.isFile()) throw new Error(`Not a file: ${path}`);
6162
+ return { target, fileStat };
6163
+ } catch (error) {
6164
+ const code = error.code;
6165
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
6166
+ throw new Error(
6167
+ `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.`,
6168
+ { cause: error }
6169
+ );
6170
+ }
6171
+ }
6093
6172
  async function readFilePrefix(target, maxBytes) {
6094
6173
  const handle = await open3(target, "r");
6095
6174
  try {
@@ -6148,11 +6227,7 @@ var readFileTool = {
6148
6227
  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.",
6149
6228
  inputSchema: ReadFileInput,
6150
6229
  async execute(input, context) {
6151
- const target = await resolveReadablePath(context.workspace, input.path);
6152
- const fileStat = await stat4(target);
6153
- if (!fileStat.isFile()) {
6154
- throw new Error(`Not a file: ${input.path}`);
6155
- }
6230
+ const { target } = await resolveReadableFile(context.workspace, input.path);
6156
6231
  const { buffer: clipped, totalBytes, stable } = await readFilePrefix(target, input.maxBytes);
6157
6232
  const text = clipped.toString("utf8");
6158
6233
  const clippedByBytes = totalBytes > clipped.byteLength;
@@ -6248,11 +6323,7 @@ var readManyFilesTool = {
6248
6323
  const sections = [];
6249
6324
  let remainingBytes = input.maxTotalBytes;
6250
6325
  for (const path of paths) {
6251
- const target = await resolveReadablePath(context.workspace, path);
6252
- const fileStat = await stat4(target);
6253
- if (!fileStat.isFile()) {
6254
- throw new Error(`Not a file: ${path}`);
6255
- }
6326
+ const { target, fileStat } = await resolveReadableFile(context.workspace, path);
6256
6327
  const readBudget = Math.min(input.maxBytesPerFile, remainingBytes);
6257
6328
  if (readBudget <= 0) {
6258
6329
  files.push({ path, bytesRead: 0, totalBytes: fileStat.size, truncated: true });
@@ -6865,12 +6936,12 @@ var builtinSecretRules = [
6865
6936
  ];
6866
6937
  async function resolveArtifact(workspace, requestedPath) {
6867
6938
  const absolute = await resolveInsideWorkspaceReal(workspace, requestedPath);
6868
- const relative2 = toWorkspaceRelative(workspace, absolute);
6939
+ const relative3 = toWorkspaceRelative(workspace, absolute);
6869
6940
  const info = await stat5(absolute);
6870
6941
  if (!info.isFile()) {
6871
6942
  throw new Error(`Not a file: ${requestedPath}`);
6872
6943
  }
6873
- return { absolute, relative: relative2 };
6944
+ return { absolute, relative: relative3 };
6874
6945
  }
6875
6946
  function formatMode(mode) {
6876
6947
  return (mode & 511).toString(8).padStart(3, "0");
@@ -6970,8 +7041,8 @@ function shannonEntropy(value) {
6970
7041
  counts.set(char, (counts.get(char) ?? 0) + 1);
6971
7042
  }
6972
7043
  let entropy = 0;
6973
- for (const count4 of counts.values()) {
6974
- const probability = count4 / value.length;
7044
+ for (const count3 of counts.values()) {
7045
+ const probability = count3 / value.length;
6975
7046
  entropy -= probability * Math.log2(probability);
6976
7047
  }
6977
7048
  return entropy;
@@ -8796,7 +8867,7 @@ function summarizeDirectories(files) {
8796
8867
  const directory = dirname9(file.path).split(/[\\/]/)[0] || ".";
8797
8868
  counts.set(directory, (counts.get(directory) ?? 0) + 1);
8798
8869
  }
8799
- return [...counts.entries()].map(([name, count4]) => ({ name, count: count4 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
8870
+ return [...counts.entries()].map(([name, count3]) => ({ name, count: count3 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
8800
8871
  }
8801
8872
  function summarizeExtensions(files) {
8802
8873
  const summaries = /* @__PURE__ */ new Map();
@@ -9349,18 +9420,18 @@ function isConcurrencySafe(tool) {
9349
9420
  }
9350
9421
  function planToolBatches(calls, tools) {
9351
9422
  const batches = [];
9352
- let open9;
9423
+ let open11;
9353
9424
  for (const call of calls) {
9354
9425
  if (!isConcurrencySafe(tools.get(call.tool))) {
9355
9426
  batches.push([call]);
9356
- open9 = void 0;
9427
+ open11 = void 0;
9357
9428
  continue;
9358
9429
  }
9359
- if (!open9) {
9360
- open9 = [];
9361
- batches.push(open9);
9430
+ if (!open11) {
9431
+ open11 = [];
9432
+ batches.push(open11);
9362
9433
  }
9363
- open9.push(call);
9434
+ open11.push(call);
9364
9435
  }
9365
9436
  return batches;
9366
9437
  }
@@ -9855,7 +9926,7 @@ async function acquireLock(directory, waitMs) {
9855
9926
  };
9856
9927
  } catch (error) {
9857
9928
  if (error.code !== "EEXIST" || Date.now() >= deadline) throw error;
9858
- await new Promise((resolve15) => setTimeout(resolve15, 10));
9929
+ await new Promise((resolve17) => setTimeout(resolve17, 10));
9859
9930
  }
9860
9931
  }
9861
9932
  }
@@ -9903,6 +9974,8 @@ function readSessionContextSnapshot(session) {
9903
9974
  const snapshot = session.contextSnapshot;
9904
9975
  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) => {
9905
9976
  if (!message || typeof message !== "object" || !["user", "assistant", "system", "tool"].includes(message.role) || typeof message.content !== "string") return true;
9977
+ const image = message.image;
9978
+ 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;
9906
9979
  const native = message;
9907
9980
  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"));
9908
9981
  }) || !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.`);
@@ -11144,10 +11217,10 @@ async function withHardTimeout(promise, ms, onTimeout) {
11144
11217
  (error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) })
11145
11218
  );
11146
11219
  let timer;
11147
- const expiry = new Promise((resolve15) => {
11220
+ const expiry = new Promise((resolve17) => {
11148
11221
  timer = setTimeout(() => {
11149
11222
  onTimeout?.();
11150
- resolve15(void 0);
11223
+ resolve17(void 0);
11151
11224
  }, ms);
11152
11225
  });
11153
11226
  try {
@@ -12304,8 +12377,8 @@ function findFileInText(text, workspace) {
12304
12377
  for (const match of text.matchAll(stackFrame)) {
12305
12378
  const candidate = match[1];
12306
12379
  if (candidate.includes("node_modules") || candidate.includes("/internal/")) continue;
12307
- const relative2 = relativizeFile(candidate, workspace);
12308
- if (relative2 !== "unknown") return relative2;
12380
+ const relative3 = relativizeFile(candidate, workspace);
12381
+ if (relative3 !== "unknown") return relative3;
12309
12382
  }
12310
12383
  return null;
12311
12384
  }
@@ -12793,12 +12866,12 @@ async function sendRequest(url, input, approved, signal) {
12793
12866
  // The socket goes to a pinned address; TLS still verifies the name.
12794
12867
  servername: secure && !isIP(hostname) ? hostname : void 0
12795
12868
  };
12796
- return await new Promise((resolve15, reject) => {
12869
+ return await new Promise((resolve17, reject) => {
12797
12870
  const request = secure ? httpsRequest(options) : httpRequest(options);
12798
12871
  request.on("response", (response) => {
12799
12872
  response.on("error", () => {
12800
12873
  });
12801
- resolve15({
12874
+ resolve17({
12802
12875
  status: response.statusCode ?? 0,
12803
12876
  statusText: response.statusMessage ?? "",
12804
12877
  ok: (response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) < 300,
@@ -13277,8 +13350,8 @@ var LOOPBACK_TIMEOUT_MS = 10 * 60 * 1e3;
13277
13350
  async function startLoopbackServer(options) {
13278
13351
  const timeoutMs = options.timeoutMs ?? LOOPBACK_TIMEOUT_MS;
13279
13352
  let settle;
13280
- const wait = new Promise((resolve15) => {
13281
- settle = resolve15;
13353
+ const wait = new Promise((resolve17) => {
13354
+ settle = resolve17;
13282
13355
  });
13283
13356
  const server = http.createServer((request, response) => {
13284
13357
  const host = request.headers.host ?? "127.0.0.1";
@@ -13325,10 +13398,10 @@ async function startLoopbackServer(options) {
13325
13398
  settle?.({ ok: false, error: "server_error", description: error.message });
13326
13399
  settle = void 0;
13327
13400
  });
13328
- const port = await new Promise((resolve15, reject) => {
13401
+ const port = await new Promise((resolve17, reject) => {
13329
13402
  server.listen(0, "127.0.0.1", () => {
13330
13403
  const address = server.address();
13331
- if (address && typeof address === "object") resolve15(address.port);
13404
+ if (address && typeof address === "object") resolve17(address.port);
13332
13405
  else reject(new Error("loopback server did not bind a port"));
13333
13406
  });
13334
13407
  server.once("error", reject);
@@ -13347,9 +13420,9 @@ async function startLoopbackServer(options) {
13347
13420
  port,
13348
13421
  redirectUri: `http://127.0.0.1:${port}${LOOPBACK_PATH}`,
13349
13422
  wait,
13350
- close: () => new Promise((resolve15) => {
13423
+ close: () => new Promise((resolve17) => {
13351
13424
  clearTimeout(timer);
13352
- server.close(() => resolve15());
13425
+ server.close(() => resolve17());
13353
13426
  })
13354
13427
  };
13355
13428
  }
@@ -13463,6 +13536,22 @@ function errorMessage(error) {
13463
13536
  }
13464
13537
  return String(error);
13465
13538
  }
13539
+ async function readAccountPlan(credentials, fetchImpl = (input, init) => fetch(input, init)) {
13540
+ if (!credentials) return void 0;
13541
+ try {
13542
+ const response = await fetchImpl(modelsUrl(credentials.baseUrl), {
13543
+ headers: { authorization: `Bearer ${credentials.apiKey}`, accept: "application/json" },
13544
+ redirect: "error",
13545
+ signal: AbortSignal.timeout(2e3)
13546
+ });
13547
+ if (!response.ok) return void 0;
13548
+ const body = await response.json();
13549
+ const plans = { free: "Free", pro: "Pro", team: "Team", ultra: "Ultra" };
13550
+ return typeof body.account?.plan === "string" && Object.hasOwn(plans, body.account.plan) ? plans[body.account.plan] : void 0;
13551
+ } catch {
13552
+ return void 0;
13553
+ }
13554
+ }
13466
13555
 
13467
13556
  // src/auth/browser-login.ts
13468
13557
  var BROWSER_LOGIN_REQUEST_TIMEOUT_MS = 15e3;
@@ -13660,7 +13749,7 @@ function silenceTimeoutMs() {
13660
13749
  return Number.isFinite(override) && override > 0 ? override : DEFAULT_NON_TTY_SILENCE_TIMEOUT_MS;
13661
13750
  }
13662
13751
  function ask(rl, question, onSettle) {
13663
- return new Promise((resolve15) => {
13752
+ return new Promise((resolve17) => {
13664
13753
  let settled = false;
13665
13754
  let silenceTimer;
13666
13755
  const keepAwake = () => {
@@ -13674,7 +13763,7 @@ function ask(rl, question, onSettle) {
13674
13763
  stdin2.off("data", keepAwake);
13675
13764
  rl.close();
13676
13765
  onSettle?.();
13677
- resolve15(value.trim());
13766
+ resolve17(value.trim());
13678
13767
  };
13679
13768
  if (!stdin2.isTTY) {
13680
13769
  silenceTimer = setTimeout(() => finish(""), silenceTimeoutMs());
@@ -14026,9 +14115,9 @@ var JsonRpcEndpoint = class {
14026
14115
  if (signal?.aborted) {
14027
14116
  return Promise.reject(new JsonRpcRequestAbortedError(method, void 0, false));
14028
14117
  }
14029
- return new Promise((resolve15, reject) => {
14118
+ return new Promise((resolve17, reject) => {
14030
14119
  const limit = timeoutMs ?? this.defaultTimeoutMs;
14031
- const entry = { method, resolve: resolve15, reject, signal, dispatched: false };
14120
+ const entry = { method, resolve: resolve17, reject, signal, dispatched: false };
14032
14121
  if (signal) {
14033
14122
  entry.abortHandler = () => {
14034
14123
  this.settle(id, () => reject(new JsonRpcRequestAbortedError(method, id, entry.dispatched)));
@@ -14711,13 +14800,13 @@ function renderMcpStatuses(statuses) {
14711
14800
  }).join("\n");
14712
14801
  }
14713
14802
  function withTimeout(promise, ms, message) {
14714
- return new Promise((resolve15, reject) => {
14803
+ return new Promise((resolve17, reject) => {
14715
14804
  const timer = setTimeout(() => reject(new Error(`${message} within ${ms}ms.`)), ms);
14716
14805
  timer.unref?.();
14717
14806
  promise.then(
14718
14807
  (value) => {
14719
14808
  clearTimeout(timer);
14720
- resolve15(value);
14809
+ resolve17(value);
14721
14810
  },
14722
14811
  (error) => {
14723
14812
  clearTimeout(timer);
@@ -15116,14 +15205,14 @@ function redact(secret) {
15116
15205
  // src/notify/channel.ts
15117
15206
  function sleep(ms, signal) {
15118
15207
  if (signal?.aborted) return Promise.resolve(false);
15119
- return new Promise((resolve15) => {
15208
+ return new Promise((resolve17) => {
15120
15209
  const timer = setTimeout(() => {
15121
15210
  signal?.removeEventListener("abort", onAbort);
15122
- resolve15(true);
15211
+ resolve17(true);
15123
15212
  }, ms);
15124
15213
  const onAbort = () => {
15125
15214
  clearTimeout(timer);
15126
- resolve15(false);
15215
+ resolve17(false);
15127
15216
  };
15128
15217
  signal?.addEventListener("abort", onAbort, { once: true });
15129
15218
  });
@@ -15371,7 +15460,7 @@ ${question}${optionLines}`, signal);
15371
15460
  await Promise.allSettled(this.channels.map((channel) => channel.call?.(spoken, wait.signal)));
15372
15461
  })();
15373
15462
  const timeout = sleep(this.timing.timeoutMs, wait.signal).then(() => null);
15374
- const cancelled = new Promise((resolve15) => wait.signal.addEventListener("abort", () => resolve15(null), { once: true }));
15463
+ const cancelled = new Promise((resolve17) => wait.signal.addEventListener("abort", () => resolve17(null), { once: true }));
15375
15464
  const reply = await Promise.race([...replies, timeout, cancelled]);
15376
15465
  return reply === null ? null : pickOption(reply, options);
15377
15466
  } finally {
@@ -15682,6 +15771,80 @@ function registerTrustCommand(program2) {
15682
15771
  });
15683
15772
  }
15684
15773
 
15774
+ // src/core/image-attachments.ts
15775
+ import { constants as constants6 } from "fs";
15776
+ import { open as open9 } from "fs/promises";
15777
+ import { homedir as homedir2 } from "os";
15778
+ import { isAbsolute as isAbsolute4 } from "path";
15779
+ import { fileURLToPath as fileURLToPath2 } from "url";
15780
+ import sharp from "sharp";
15781
+ var MAX_SOURCE_BYTES = 20 * 1024 * 1024;
15782
+ function imageDrops(text) {
15783
+ const drops = [];
15784
+ const tokens = /"(?:\\.|[^"\\])*"|'[^']*'|(?:\\.|[^\s])+/g;
15785
+ for (const match of text.matchAll(tokens)) {
15786
+ const token = match[0];
15787
+ let path = token.startsWith("'") ? token.slice(1, -1) : token.startsWith('"') ? token.slice(1, -1).replace(/\\([\\"$`])/g, "$1") : token.replace(/\\(.)/g, "$1");
15788
+ if (path.startsWith("file://")) {
15789
+ try {
15790
+ path = fileURLToPath2(path);
15791
+ } catch {
15792
+ continue;
15793
+ }
15794
+ }
15795
+ if (path.startsWith("~/")) path = `${homedir2()}/${path.slice(2)}`;
15796
+ if (!isAbsolute4(path) || !/\.(png|jpe?g|webp)$/i.test(path)) continue;
15797
+ drops.push({ start: match.index, end: match.index + token.length, path, label: `[Image ${drops.length + 1}]` });
15798
+ }
15799
+ return drops;
15800
+ }
15801
+ function formatImageInput(text, cursor = text.length) {
15802
+ let display = "";
15803
+ let previous = 0;
15804
+ let shift = 0;
15805
+ let adjusted = cursor;
15806
+ for (const drop of imageDrops(text)) {
15807
+ display += text.slice(previous, drop.start) + drop.label;
15808
+ if (cursor >= drop.end) adjusted += drop.label.length - (drop.end - drop.start);
15809
+ else if (cursor > drop.start) adjusted = drop.start + shift + Math.min(drop.label.length, cursor - drop.start);
15810
+ shift += drop.label.length - (drop.end - drop.start);
15811
+ previous = drop.end;
15812
+ }
15813
+ return { text: display + text.slice(previous), cursor: Math.max(0, adjusted) };
15814
+ }
15815
+ function hasDroppedImage(text) {
15816
+ return imageDrops(text).length > 0;
15817
+ }
15818
+ async function prepareImageInput(text) {
15819
+ const drops = imageDrops(text);
15820
+ if (!drops.length) return { text };
15821
+ if (drops.length > 1) throw new Error("Attach one image per message. Cosmic currently accepts one image in its active context.");
15822
+ const drop = drops[0];
15823
+ const file = await open9(drop.path, constants6.O_RDONLY | constants6.O_NOFOLLOW | constants6.O_NONBLOCK);
15824
+ let bytes;
15825
+ try {
15826
+ const stat11 = await file.stat();
15827
+ if (!stat11.isFile() || stat11.size > MAX_SOURCE_BYTES) throw new Error("Image must be a regular file no larger than 20 MiB.");
15828
+ const buffer = Buffer.alloc(Math.min(stat11.size, MAX_SOURCE_BYTES) + 1);
15829
+ let used = 0;
15830
+ while (used < buffer.length) {
15831
+ const read = await file.read(buffer, used, buffer.length - used, null);
15832
+ if (!read.bytesRead) break;
15833
+ used += read.bytesRead;
15834
+ }
15835
+ if (used > MAX_SOURCE_BYTES) throw new Error("Image exceeds 20 MiB.");
15836
+ bytes = buffer.subarray(0, used);
15837
+ } finally {
15838
+ await file.close();
15839
+ }
15840
+ const image = sharp(bytes, { limitInputPixels: 4e7, animated: false });
15841
+ const metadata = await image.metadata();
15842
+ if (!["png", "jpeg", "webp"].includes(metadata.format ?? "") || (metadata.pages ?? 1) > 1) throw new Error("Attach a single PNG, JPEG or WebP image.");
15843
+ const normalized = await image.rotate().resize({ width: 1024, height: 1024, fit: "inside", withoutEnlargement: true }).png().toBuffer();
15844
+ if (normalized.length > 5 * 1024 * 1024) throw new Error("Normalized image exceeds the upload limit.");
15845
+ return { text: formatImageInput(text).text, image: { url: `data:image/png;base64,${normalized.toString("base64")}` } };
15846
+ }
15847
+
15685
15848
  // src/core/manual-compaction.ts
15686
15849
  async function compactManually(messages, pinned, provider, model, focus = "", signal) {
15687
15850
  const retained = new Set(pinned);
@@ -15959,7 +16122,8 @@ Acknowledge that you have the context and wait for the user's first request.`
15959
16122
  async ask(input, handlers = {}) {
15960
16123
  if (!this.started) await this.start();
15961
16124
  if (!this.firstTask) this.firstTask = input;
15962
- const asked = { role: "user", content: input };
16125
+ if (handlers.image) for (const message of this.messages) delete message.image;
16126
+ const asked = { role: "user", content: input, ...handlers.image ? { image: handlers.image } : {} };
15963
16127
  this.messages.push(asked);
15964
16128
  this.pinnedMessages.push(asked);
15965
16129
  this.userConstraints.push({ kind: "message", content: input });
@@ -16275,7 +16439,7 @@ Acknowledge that you have the context and wait for the user's first request.`
16275
16439
  constraintVersion: 1,
16276
16440
  userConstraints: [...this.userConstraints],
16277
16441
  mcpResultArchiveVersion: 1,
16278
- ...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "text", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
16442
+ ...this.messages.some((message) => isCompactionSummary(message) || message.image) ? { contextSnapshot: { version: 1, kind: "text", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
16279
16443
  // How the last turn ended, so a saved interactive transcript says whether
16280
16444
  // its final message is an answer or the place it ran out of room - the
16281
16445
  // same question `ultrac run` already answers for a one-shot run.
@@ -16490,12 +16654,18 @@ ${context}`
16490
16654
  async ask(input, handlers = {}) {
16491
16655
  if (!this.started) await this.start();
16492
16656
  if (!this.firstTask) this.firstTask = input;
16493
- const asked = { role: "user", content: input };
16657
+ if (handlers.image) {
16658
+ for (const message of this.messages) if (message.role === "user" || message.role === "system") delete message.image;
16659
+ }
16660
+ const asked = { role: "user", content: input, ...handlers.image ? { image: handlers.image } : {} };
16494
16661
  this.messages.push(asked);
16495
16662
  this.pinnedMessages.push(asked);
16496
16663
  this.userConstraints.push({ kind: "message", content: input });
16497
16664
  let finalMessage = "";
16498
16665
  let toolCallsUsed = 0;
16666
+ let consecutiveEmptyResponses = 0;
16667
+ const turnStarted = performance.now();
16668
+ const firstStep = this.steps.length;
16499
16669
  let stopReason = "max-steps";
16500
16670
  try {
16501
16671
  for (let index = 0; index < this.options.maxSteps; index += 1) {
@@ -16548,6 +16718,7 @@ ${context}`
16548
16718
  throw budgetError;
16549
16719
  }
16550
16720
  toolCallsUsed += response.toolCalls.length;
16721
+ if (response.content.trim() || response.toolCalls.length) consecutiveEmptyResponses = 0;
16551
16722
  if (response.content.trim()) {
16552
16723
  finalMessage = response.content;
16553
16724
  handlers.onAssistantMessage?.(response.content);
@@ -16567,6 +16738,21 @@ ${context}`
16567
16738
  handlers.onInterrupted?.();
16568
16739
  break;
16569
16740
  }
16741
+ if (!response.content.trim()) {
16742
+ consecutiveEmptyResponses += 1;
16743
+ const willRetry = consecutiveEmptyResponses <= 2 && index + 1 < this.options.maxSteps;
16744
+ diagnosticEvent("session.empty-response", {
16745
+ sessionId: this.id,
16746
+ attempt: consecutiveEmptyResponses,
16747
+ willRetry,
16748
+ maxRecoveries: 2,
16749
+ stepCount: this.steps.length
16750
+ });
16751
+ if (!willRetry) {
16752
+ throw Object.assign(new Error("The model returned an empty response without completing the task. Empty-response recovery was exhausted; the session checkpoint is preserved."), { status: 422 });
16753
+ }
16754
+ continue;
16755
+ }
16570
16756
  if (this.appendQueuedMessages(handlers) === 0) {
16571
16757
  stopReason = "completed";
16572
16758
  break;
@@ -16634,6 +16820,15 @@ ${context}`
16634
16820
  throw error;
16635
16821
  } finally {
16636
16822
  this.lastStopReason = stopReason;
16823
+ diagnosticEvent("session.turn.end", {
16824
+ sessionId: this.id,
16825
+ reason: stopReason,
16826
+ stepCount: this.steps.length,
16827
+ turnSteps: this.steps.length - firstStep,
16828
+ maxSteps: this.options.maxSteps,
16829
+ emptyResponses: consecutiveEmptyResponses,
16830
+ durationMs: Math.max(0, performance.now() - turnStarted)
16831
+ });
16637
16832
  this.subagents.push(...drainSubagentTranscripts());
16638
16833
  }
16639
16834
  handlers.onStopped?.(stopReason);
@@ -16840,7 +17035,7 @@ ${guidance.join("\n")}` : ""}`;
16840
17035
  constraintVersion: 1,
16841
17036
  userConstraints: [...this.userConstraints],
16842
17037
  mcpResultArchiveVersion: 1,
16843
- ...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "native", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
17038
+ ...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 } } : {},
16844
17039
  // How the last turn ended, so a saved interactive transcript says whether
16845
17040
  // its final message is an answer or the place it ran out of room - the
16846
17041
  // same question `ultrac run` already answers for a one-shot run.
@@ -16882,10 +17077,10 @@ function boundedInteger(value, name, min, max) {
16882
17077
  }
16883
17078
  function abortableSleep(milliseconds, signal) {
16884
17079
  signal.throwIfAborted();
16885
- return new Promise((resolve15, reject) => {
17080
+ return new Promise((resolve17, reject) => {
16886
17081
  const timer = setTimeout(() => {
16887
17082
  signal.removeEventListener("abort", abort);
16888
- resolve15();
17083
+ resolve17();
16889
17084
  }, milliseconds);
16890
17085
  const abort = () => {
16891
17086
  clearTimeout(timer);
@@ -16937,6 +17132,11 @@ async function runAutonomousLoop(options) {
16937
17132
  let lastStepCount = 0;
16938
17133
  let stalls = 0;
16939
17134
  let consecutiveErrors = 0;
17135
+ const finish = (session, rounds, haltReason) => {
17136
+ diagnosticEvent("autonomous.end", { id, sessionId: session.id, rounds, reason: haltReason, elapsedMs: elapsed() });
17137
+ return { session, rounds, haltReason };
17138
+ };
17139
+ diagnosticEvent("autonomous.start", { id, maxRounds: options.maxRounds, maxElapsedMs });
16940
17140
  try {
16941
17141
  signal.throwIfAborted();
16942
17142
  for (let round = 1; ; round += 1) {
@@ -16967,8 +17167,8 @@ async function runAutonomousLoop(options) {
16967
17167
  else if (!shouldResume(session.stopReason)) haltReason = "stopped";
16968
17168
  else if (stalls >= stallLimit) haltReason = "stalled";
16969
17169
  else if (round >= options.maxRounds) haltReason = "max_rounds";
16970
- const retryDelay = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
16971
- if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay >= maxElapsedMs) haltReason = "elapsed_budget";
17170
+ const retryDelay2 = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
17171
+ if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay2 >= maxElapsedMs) haltReason = "elapsed_budget";
16972
17172
  diagnosticEvent("round.end", {
16973
17173
  id,
16974
17174
  round,
@@ -16982,10 +17182,10 @@ async function runAutonomousLoop(options) {
16982
17182
  });
16983
17183
  if (haltReason === "stalled") diagnosticEvent("round.stall", { id, round, repeats: stalls, completedTools: current.completedTools, elapsedMs: elapsed() });
16984
17184
  await options.onRoundEnd?.(round, session, !haltReason);
16985
- if (haltReason) return { session, rounds: round, haltReason };
17185
+ if (haltReason) return finish(session, round, haltReason);
16986
17186
  if (session.stopReason === "error") {
16987
- const delayMs = retryDelay;
16988
- if (elapsed() + delayMs >= maxElapsedMs) return { session, rounds: round, haltReason: "elapsed_budget" };
17187
+ const delayMs = retryDelay2;
17188
+ if (elapsed() + delayMs >= maxElapsedMs) return finish(session, round, "elapsed_budget");
16989
17189
  diagnosticEvent("round.retry", {
16990
17190
  id,
16991
17191
  round,
@@ -16999,12 +17199,15 @@ async function runAutonomousLoop(options) {
16999
17199
  await sleep2(delayMs, signal);
17000
17200
  } catch (error) {
17001
17201
  if (!signal.aborted) throw error;
17002
- return { session, rounds: round, haltReason: budget.signal.aborted ? "elapsed_budget" : "aborted" };
17202
+ return finish(session, round, budget.signal.aborted ? "elapsed_budget" : "aborted");
17003
17203
  }
17004
17204
  }
17005
- if (signal.aborted || elapsed() >= maxElapsedMs) return { session, rounds: round, haltReason: budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted" };
17205
+ if (signal.aborted || elapsed() >= maxElapsedMs) return finish(session, round, budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted");
17006
17206
  previous = session;
17007
17207
  }
17208
+ } catch (error) {
17209
+ diagnosticEvent("autonomous.end", { id, reason: budget.signal.aborted ? "elapsed_budget" : signal.aborted ? "aborted" : "exception", elapsedMs: elapsed() });
17210
+ throw error;
17008
17211
  } finally {
17009
17212
  clearTimeout(deadline);
17010
17213
  }
@@ -17271,53 +17474,8 @@ function attachProgressUi(options = {}) {
17271
17474
  };
17272
17475
  }
17273
17476
 
17274
- // src/ui/spinner.ts
17275
- import { stdout as stdout4 } from "process";
17276
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
17277
- var Spinner = class {
17278
- constructor(label = "thinking") {
17279
- this.label = label;
17280
- }
17281
- label;
17282
- timer;
17283
- index = 0;
17284
- holdWhile;
17285
- held = false;
17286
- start(holdWhile) {
17287
- this.holdWhile = holdWhile;
17288
- if (!stdout4.isTTY || this.timer) return;
17289
- this.render();
17290
- this.timer = setInterval(() => this.render(), 90);
17291
- }
17292
- update(label) {
17293
- this.label = label;
17294
- }
17295
- stop() {
17296
- if (this.timer) {
17297
- clearInterval(this.timer);
17298
- this.timer = void 0;
17299
- }
17300
- if (stdout4.isTTY) {
17301
- stdout4.clearLine(0);
17302
- stdout4.cursorTo(0);
17303
- }
17304
- }
17305
- render() {
17306
- if (this.holdWhile?.()) {
17307
- if (!this.held) {
17308
- stdout4.clearLine(0);
17309
- stdout4.cursorTo(0);
17310
- this.held = true;
17311
- }
17312
- return;
17313
- }
17314
- this.held = false;
17315
- this.index = (this.index + 1) % FRAMES.length;
17316
- stdout4.clearLine(0);
17317
- stdout4.cursorTo(0);
17318
- stdout4.write(` ${FRAMES[this.index]} ${this.label}\u2026`);
17319
- }
17320
- };
17477
+ // src/ui/activity.ts
17478
+ import { stripVTControlCharacters } from "util";
17321
17479
 
17322
17480
  // src/ui/tool-activity.ts
17323
17481
  var VERBS = {
@@ -17337,7 +17495,9 @@ var VERBS = {
17337
17495
  run_tests: "Run tests",
17338
17496
  diagnostics: "Check diagnostics",
17339
17497
  task: "Delegate",
17340
- fetch_url: "Fetch",
17498
+ fetch_url: "Opened",
17499
+ web_search: "Searched the web for",
17500
+ search_web: "Searched the web for",
17341
17501
  git_status: "Check Git status",
17342
17502
  git_diff: "Read Git diff",
17343
17503
  git_log: "Read Git log",
@@ -17365,7 +17525,7 @@ function toolTarget(call) {
17365
17525
  if (paths.length > 1) return `${paths.length} files`;
17366
17526
  if (call.tool === "run_shell") return compact2(stringValue(input.command), 72);
17367
17527
  if (call.tool === "run_package_script") return compact2(stringValue(input.script), 50);
17368
- if (call.tool === "search_files") return quote(stringValue(input.query) || stringValue(input.pattern));
17528
+ if (["search_files", "web_search", "search_web"].includes(call.tool)) return quote(stringValue(input.query) || stringValue(input.pattern));
17369
17529
  if (call.tool === "fetch_url") return compact2(stringValue(input.url), 72);
17370
17530
  if (call.tool === "task") return compact2(stringValue(input.description) || stringValue(input.task), 64);
17371
17531
  if (call.tool === "git_show") return compact2(stringValue(input.ref), 40);
@@ -17390,14 +17550,180 @@ function humanize(value) {
17390
17550
  return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Tool";
17391
17551
  }
17392
17552
 
17553
+ // src/ui/spinner.ts
17554
+ import { stdout as stdout4 } from "process";
17555
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
17556
+ var Spinner = class {
17557
+ constructor(label = "working", renderLine) {
17558
+ this.label = label;
17559
+ this.renderLine = renderLine;
17560
+ }
17561
+ label;
17562
+ renderLine;
17563
+ timer;
17564
+ index = 0;
17565
+ holdWhile;
17566
+ held = false;
17567
+ startedAt;
17568
+ start(holdWhile, startedAt = performance.now()) {
17569
+ this.holdWhile = holdWhile;
17570
+ if (this.startedAt !== void 0) return;
17571
+ this.startedAt = startedAt;
17572
+ this.held = false;
17573
+ if (!stdout4.isTTY) return;
17574
+ this.render();
17575
+ this.timer = setInterval(() => this.render(), 90);
17576
+ }
17577
+ update(label) {
17578
+ this.label = label;
17579
+ }
17580
+ stop(completion) {
17581
+ const elapsed = this.startedAt === void 0 ? void 0 : performance.now() - this.startedAt;
17582
+ this.startedAt = void 0;
17583
+ if (this.timer) {
17584
+ clearInterval(this.timer);
17585
+ this.timer = void 0;
17586
+ }
17587
+ if (stdout4.isTTY && elapsed !== void 0) {
17588
+ if (this.renderLine) this.renderLine("");
17589
+ else {
17590
+ stdout4.clearLine(0);
17591
+ stdout4.cursorTo(0);
17592
+ }
17593
+ if (completion && elapsed >= 1e3) stdout4.write(` ${completion} ${formatSeconds(elapsed)}.
17594
+ `);
17595
+ }
17596
+ }
17597
+ render() {
17598
+ if (this.holdWhile?.()) {
17599
+ if (!this.held) {
17600
+ if (this.renderLine) this.renderLine("");
17601
+ else {
17602
+ stdout4.clearLine(0);
17603
+ stdout4.cursorTo(0);
17604
+ }
17605
+ this.held = true;
17606
+ }
17607
+ return;
17608
+ }
17609
+ this.held = false;
17610
+ this.index = (this.index + 1) % FRAMES.length;
17611
+ if (!this.renderLine) {
17612
+ stdout4.clearLine(0);
17613
+ stdout4.cursorTo(0);
17614
+ }
17615
+ const milliseconds = performance.now() - (this.startedAt ?? performance.now());
17616
+ const elapsed = milliseconds >= 1e3 ? formatElapsed(milliseconds) : "";
17617
+ const limit = Math.max(0, (stdout4.columns ?? 80) - elapsed.length - 10);
17618
+ let label = "";
17619
+ let cells2 = 0;
17620
+ for (const character of this.label.replace(/[\x00-\x1f\x7f]/g, " ")) {
17621
+ const size = character.codePointAt(0) > 127 ? 2 : 1;
17622
+ if (cells2 + size > limit) break;
17623
+ label += character;
17624
+ cells2 += size;
17625
+ }
17626
+ const line = ` ${FRAMES[this.index]} ${label}\u2026${elapsed ? ` \xB7 ${elapsed}` : ""}`;
17627
+ if (this.renderLine) this.renderLine(line);
17628
+ else stdout4.write(line);
17629
+ }
17630
+ };
17631
+ function formatSeconds(milliseconds) {
17632
+ const seconds = Math.max(0, milliseconds) / 1e3;
17633
+ const count3 = seconds.toFixed(1).replace(/\.0$/, "");
17634
+ return `${count3} ${count3 === "1" ? "second" : "seconds"}`;
17635
+ }
17636
+
17637
+ // src/ui/activity.ts
17638
+ var sink;
17639
+ var workingSink;
17640
+ function setWorkingSink(value) {
17641
+ workingSink = value;
17642
+ }
17643
+ function getWorkingSink() {
17644
+ return workingSink;
17645
+ }
17646
+ function setActivitySink(value) {
17647
+ sink = value;
17648
+ }
17649
+ function getActivitySink() {
17650
+ return sink;
17651
+ }
17652
+ var clean = (text) => stripVTControlCharacters(text).replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
17653
+ function activityEntry(call, result, durationMs) {
17654
+ const fields = ["command", "script", "path", "paths", "query", "pattern", "url", "cwd"];
17655
+ const request = fields.flatMap((key) => {
17656
+ const value = call.input[key];
17657
+ return typeof value === "string" ? [`${key}: ${value}`] : Array.isArray(value) && value.every((item) => typeof item === "string") ? [`${key}: ${value.join(", ")}`] : [];
17658
+ }).join(" \xB7 ");
17659
+ return {
17660
+ label: clean(toolActivityLabel(call)),
17661
+ request: clean(request).slice(0, 4e3),
17662
+ output: clean(result.content).slice(0, 1e3),
17663
+ ok: result.ok,
17664
+ durationMs
17665
+ };
17666
+ }
17667
+ function activityRows(snapshot, expanded, selected, end) {
17668
+ const failures = snapshot.entries.filter((entry) => !entry.ok).length;
17669
+ const elapsed = snapshot.thinkingMs >= 1e3 ? ` \xB7 Worked for ${formatSeconds(snapshot.thinkingMs)}` : "";
17670
+ const summary = `${expanded ? "\u25BE" : "\u25B8"} \u25CF ${snapshot.entries.length} ${snapshot.entries.length === 1 ? "tool call" : "tool calls"}${failures ? ` \xB7 ${failures} failed` : ""}${snapshot.finished ? elapsed : ""} \xB7 Ctrl+O details`;
17671
+ const rows = [{ text: summary, ok: failures || snapshot.failed ? false : snapshot.finished ? true : void 0 }];
17672
+ if (!expanded) {
17673
+ if (!snapshot.finished) for (let index = Math.max(0, snapshot.entries.length - 3); index < snapshot.entries.length; index += 1) {
17674
+ const entry = snapshot.entries[index];
17675
+ rows.push({ text: ` \u25CF ${entry.label}`, ok: entry.ok, entry: index });
17676
+ }
17677
+ return rows;
17678
+ }
17679
+ const last = Math.min(snapshot.entries.length, end);
17680
+ for (let index = Math.max(0, last - 5); index < last; index += 1) {
17681
+ const entry = snapshot.entries[index];
17682
+ const duration = entry.durationMs >= 1e3 ? ` \xB7 Worked for ${formatSeconds(entry.durationMs)}` : "";
17683
+ rows.push({ text: ` ${selected === index ? "\u25BE" : "\u25B8"} \u25CF ${entry.label}${duration}`, ok: entry.ok, entry: index });
17684
+ if (selected === index) {
17685
+ if (entry.request) rows.push({ text: ` ${entry.request}`, entry: index });
17686
+ if (entry.output) rows.push({ text: ` ${entry.output}`, ok: entry.ok, entry: index });
17687
+ }
17688
+ }
17689
+ if (snapshot.entries.length > 5) rows.push({ text: " Scroll to browse actions \xB7 /output for full saved results" });
17690
+ return rows;
17691
+ }
17692
+
17693
+ // src/ui/task-progress.ts
17694
+ function startTaskProgress(options) {
17695
+ let lastMessage = performance.now();
17696
+ let lastState = "";
17697
+ let stopped = false;
17698
+ const timer = setInterval(() => {
17699
+ if (stopped || options.holdWhile?.()) return;
17700
+ const state = options.snapshot();
17701
+ if (!state.activeTool && !state.waitingForModel) return;
17702
+ const fingerprint = `${state.completed}:${state.failed}:${state.activeTool ?? "model"}`;
17703
+ if (performance.now() - lastMessage < (fingerprint === lastState ? 6e4 : 3e4)) return;
17704
+ const completed = state.completed ? `I have completed ${state.completed} tool ${state.completed === 1 ? "call" : "calls"}.${state.failed ? ` Of those, ${state.failed} failed.` : ""} ` : "";
17705
+ const current = state.activeTool ? `I am running the ${state.activeTool} tool.` : "I am waiting for the model's next response.";
17706
+ options.write(`${completed}${current}`);
17707
+ lastMessage = performance.now();
17708
+ lastState = fingerprint;
17709
+ }, 5e3);
17710
+ timer.unref?.();
17711
+ return { markMessage: () => {
17712
+ lastMessage = performance.now();
17713
+ }, stop: () => {
17714
+ stopped = true;
17715
+ clearInterval(timer);
17716
+ } };
17717
+ }
17718
+
17393
17719
  // src/ui/interactive-choice.ts
17394
17720
  var MOUSE_ON = "\x1B[?1006h\x1B[?1000h";
17395
17721
  var MOUSE_OFF = "\x1B[?1000l\x1B[?1006l";
17396
17722
  function displayLabel(value, width2) {
17397
- const clean = value.replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|.)/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
17723
+ const clean2 = value.replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|.)/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
17398
17724
  let result = "";
17399
17725
  let used = 0;
17400
- for (const character of clean) {
17726
+ for (const character of clean2) {
17401
17727
  const size = /[\p{Mark}\u200d\ufe0e\ufe0f]/u.test(character) ? 0 : character.codePointAt(0) < 128 ? 1 : 2;
17402
17728
  if (used + size > width2 - 1) return `${result}\u2026`;
17403
17729
  result += character;
@@ -17410,8 +17736,8 @@ function attachInteractiveChoices(readline, options, settings = {}) {
17410
17736
  const output = settings.output ?? rl.output;
17411
17737
  const prompt = settings.prompt ?? " \u203A ";
17412
17738
  let resolveReady;
17413
- const ready = new Promise((resolve15) => {
17414
- resolveReady = resolve15;
17739
+ const ready = new Promise((resolve17) => {
17740
+ resolveReady = resolve17;
17415
17741
  });
17416
17742
  const original = rl._ttyWrite;
17417
17743
  const priorPrompt = rl.getPrompt();
@@ -17617,6 +17943,15 @@ function attachInteractiveChoices(readline, options, settings = {}) {
17617
17943
  }
17618
17944
 
17619
17945
  // src/ui/slash-completion.ts
17946
+ function builtinSlashEntries(commands, help) {
17947
+ const descriptions = /* @__PURE__ */ new Map();
17948
+ for (const line of help.split("\n")) {
17949
+ const match = /^(\/[^\n]+?)\s{2,}(.+)$/.exec(line.trim());
17950
+ if (!match) continue;
17951
+ for (const command of match[1].matchAll(/\/([a-z-]+)/g)) descriptions.set(command[1], match[2]);
17952
+ }
17953
+ return commands.map((command) => ({ text: `/${command}`, label: `/${command}`, description: descriptions.get(command) }));
17954
+ }
17620
17955
  function attachSlashCompletion(readline, loadEntries, render, initialEntries = []) {
17621
17956
  const rl = readline;
17622
17957
  const original = rl._ttyWrite;
@@ -17718,11 +18053,11 @@ function attachSlashCompletion(readline, loadEntries, render, initialEntries = [
17718
18053
 
17719
18054
  // src/ui/terminal-layout.ts
17720
18055
  import { Writable } from "stream";
17721
- import { stripVTControlCharacters } from "util";
18056
+ import { stripVTControlCharacters as stripVTControlCharacters2 } from "util";
17722
18057
  var CSI = "\x1B[";
17723
18058
  var RESET_REGION = `${CSI}r`;
17724
18059
  var MOUSE_OFF2 = `${CSI}?1000l${CSI}?1002l${CSI}?1003l${CSI}?1006l`;
17725
- var BASE_FOOTER_ROWS = 5;
18060
+ var BASE_FOOTER_ROWS = 4;
17726
18061
  var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
17727
18062
  var cells = (value) => [...segmenter.segment(value)].map(({ segment }) => {
17728
18063
  const code = segment.codePointAt(0) ?? 0;
@@ -17731,7 +18066,7 @@ var cells = (value) => [...segmenter.segment(value)].map(({ segment }) => {
17731
18066
  });
17732
18067
  var width = (text) => cells(text).reduce((sum, cell) => sum + cell.width, 0);
17733
18068
  function plain(value) {
17734
- return stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f]/g, " ");
18069
+ return stripVTControlCharacters2(value).replace(/[\x00-\x1f\x7f]/g, " ");
17735
18070
  }
17736
18071
  function fit(text, columns) {
17737
18072
  if (columns <= 0) return "";
@@ -17745,7 +18080,6 @@ function fit(text, columns) {
17745
18080
  return result;
17746
18081
  }
17747
18082
  var cup = (row, column = 1) => `${CSI}${row};${column}H`;
17748
- var count3 = (value) => value !== void 0 && Number.isFinite(value) && value >= 0 ? Math.floor(value).toLocaleString("en-US") : "unknown";
17749
18083
  var ComposerOutput = class extends Writable {
17750
18084
  constructor(layout) {
17751
18085
  super();
@@ -17781,6 +18115,7 @@ var TerminalLayout = class {
17781
18115
  details;
17782
18116
  rl;
17783
18117
  prompt = "\u203A ";
18118
+ inputFormatter;
17784
18119
  transcriptRow = 1;
17785
18120
  transcriptColumn = 1;
17786
18121
  wrapPending = false;
@@ -17788,6 +18123,10 @@ var TerminalLayout = class {
17788
18123
  lastFooterTop = 0;
17789
18124
  suggestions = [];
17790
18125
  selected = 0;
18126
+ activity;
18127
+ liveActivity;
18128
+ completedActivities = [];
18129
+ restoreActivityInput;
17791
18130
  installed = false;
17792
18131
  closed = false;
17793
18132
  suspended = 0;
@@ -17796,6 +18135,7 @@ var TerminalLayout = class {
17796
18135
  resize = () => {
17797
18136
  if (this.closed || !this.enabled) return;
17798
18137
  this.clearPreviousFooter();
18138
+ this.completedActivities = this.completedActivities.filter((item) => item.row >= 1 && item.row + item.height - 1 <= this.contentRows);
17799
18139
  this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
17800
18140
  this.transcriptColumn = Math.min(this.transcriptColumn, this.columns);
17801
18141
  this.rawWrite(`${CSI}1;${this.contentRows}r`);
@@ -17829,7 +18169,7 @@ var TerminalLayout = class {
17829
18169
  return this.rows - BASE_FOOTER_ROWS - this.suggestionRows;
17830
18170
  }
17831
18171
  get inputRow() {
17832
- return this.rows - 3;
18172
+ return this.rows - 2;
17833
18173
  }
17834
18174
  install() {
17835
18175
  if (this.installed || this.closed || !this.enabled) return this;
@@ -17851,6 +18191,155 @@ var TerminalLayout = class {
17851
18191
  attachReadline(rl) {
17852
18192
  this.rl = rl;
17853
18193
  rl.setPrompt(this.prompt);
18194
+ const reader = rl;
18195
+ const original = reader._ttyWrite;
18196
+ let sequence = "";
18197
+ let sequenceTimer;
18198
+ const wrapper = (text, key) => {
18199
+ if ((this.activity || this.completedActivities.length) && !this.suspended && key?.ctrl && key.name === "o") {
18200
+ this.toggleActivity();
18201
+ return;
18202
+ }
18203
+ const value = key?.sequence ?? text ?? "";
18204
+ if (sequence || value.startsWith("\x1B[<")) {
18205
+ sequence += value;
18206
+ clearTimeout(sequenceTimer);
18207
+ const mouse = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(sequence);
18208
+ if (mouse) {
18209
+ sequence = "";
18210
+ if (!this.suspended) this.activityMouse(Number(mouse[1]), Number(mouse[3]), mouse[4] === "M");
18211
+ return;
18212
+ }
18213
+ if (/^\x1b\[<[0-9;]*$/.test(sequence) && sequence.length < 64) {
18214
+ sequenceTimer = setTimeout(() => {
18215
+ sequence = "";
18216
+ }, 250);
18217
+ return;
18218
+ }
18219
+ const pending = sequence;
18220
+ sequence = "";
18221
+ original.call(reader, pending, key);
18222
+ return;
18223
+ }
18224
+ original.call(reader, text, key);
18225
+ };
18226
+ reader._ttyWrite = wrapper;
18227
+ this.restoreActivityInput = () => {
18228
+ clearTimeout(sequenceTimer);
18229
+ if (reader._ttyWrite === wrapper) reader._ttyWrite = original;
18230
+ };
18231
+ this.paintFooter();
18232
+ }
18233
+ setActivity(snapshot) {
18234
+ this.activity = snapshot;
18235
+ if (!this.enabled || !this.installed || this.closed) return;
18236
+ if (!this.liveActivity && !snapshot.finished) {
18237
+ this.writeTranscript(`
18238
+ ${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.length)[0])}
18239
+ `);
18240
+ this.liveActivity = { snapshot, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1, end: snapshot.entries.length };
18241
+ this.completedActivities.push(this.liveActivity);
18242
+ }
18243
+ const item = this.liveActivity;
18244
+ if (!item) return;
18245
+ const previous = item.snapshot.entries.length;
18246
+ item.snapshot = snapshot;
18247
+ if (item.end >= previous) item.end = snapshot.entries.length;
18248
+ if (!snapshot.finished) {
18249
+ this.redrawCompleted(item);
18250
+ return;
18251
+ }
18252
+ if (item.row >= 2) {
18253
+ const removed = item.height + 1;
18254
+ this.rawWrite(`${cup(item.row - 1)}${CSI}${removed}M`);
18255
+ if (this.transcriptRow > item.row) this.transcriptRow -= removed;
18256
+ for (const other of this.completedActivities) if (other !== item && other.row > item.row) other.row -= removed;
18257
+ }
18258
+ this.completedActivities = this.completedActivities.filter((entry) => entry !== item);
18259
+ this.liveActivity = void 0;
18260
+ this.writeTranscript(`${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.length)[0])}
18261
+ `);
18262
+ this.completedActivities.push({ ...item, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1 });
18263
+ this.paintFooter();
18264
+ }
18265
+ setWorkingStatus(text) {
18266
+ const item = this.liveActivity;
18267
+ if (!item) return;
18268
+ if (item.row < 2 || item.row + item.height > this.contentRows) {
18269
+ if (item.row >= 1 && item.row <= this.contentRows) {
18270
+ this.rawWrite(`${cup(item.row)}${CSI}${item.height}M`);
18271
+ if (this.transcriptRow > item.row) this.transcriptRow -= item.height;
18272
+ for (const other of this.completedActivities) if (other !== item && other.row > item.row) other.row -= item.height;
18273
+ }
18274
+ this.completedActivities = this.completedActivities.filter((entry) => entry !== item);
18275
+ this.writeTranscript(`
18276
+ ${this.colorActivityRow(activityRows(item.snapshot, false, -1, item.end)[0])}
18277
+ `);
18278
+ item.row = this.transcriptRow - 1;
18279
+ item.height = 1;
18280
+ item.expanded = false;
18281
+ this.completedActivities.push(item);
18282
+ }
18283
+ this.rawWrite(`${cup(item.row - 1)}${CSI}0m${CSI}2K${fit(text, this.columns)}`);
18284
+ this.placeInputCursor();
18285
+ }
18286
+ colorActivityRow(row) {
18287
+ const text = fit(row.text, this.columns);
18288
+ const color = process.env.NO_COLOR || row.ok === void 0 ? "" : `${CSI}${row.ok ? 32 : 31}m`;
18289
+ const dot = text.indexOf("\u25CF");
18290
+ return dot >= 0 && color ? `${text.slice(0, dot)}${color}\u25CF${CSI}39m${text.slice(dot + 1)}` : text;
18291
+ }
18292
+ shiftCompleted(rows) {
18293
+ for (const item of this.completedActivities) item.row -= rows;
18294
+ this.completedActivities = this.completedActivities.filter((item) => item.row >= 1);
18295
+ }
18296
+ redrawCompleted(item) {
18297
+ const rows = activityRows(item.snapshot, item.expanded, item.selected, item.end).slice(0, Math.max(1, this.contentRows - 2));
18298
+ const delta = rows.length - item.height;
18299
+ const scroll = Math.max(0, this.transcriptRow + delta - this.contentRows);
18300
+ if (scroll) {
18301
+ this.rawWrite(`${cup(this.contentRows)}${CSI}${scroll}S`);
18302
+ this.shiftCompleted(scroll);
18303
+ this.transcriptRow -= scroll;
18304
+ }
18305
+ if (item.row < 1) return;
18306
+ if (delta > 0) this.rawWrite(`${cup(item.row + item.height)}${CSI}${delta}L`);
18307
+ if (delta < 0) this.rawWrite(`${cup(item.row + rows.length)}${CSI}${-delta}M`);
18308
+ for (const other of this.completedActivities) if (other !== item && other.row > item.row) other.row += delta;
18309
+ if (this.transcriptRow > item.row) this.transcriptRow += delta;
18310
+ item.height = rows.length;
18311
+ rows.forEach((row, index) => this.rawWrite(`${cup(item.row + index)}${CSI}0m${CSI}2K${this.colorActivityRow(row)}`));
18312
+ this.paintFooter();
18313
+ }
18314
+ toggleActivity() {
18315
+ if (this.liveActivity && (this.liveActivity.row < 2 || this.liveActivity.row + this.liveActivity.height > this.contentRows)) this.setWorkingStatus("working\u2026");
18316
+ const item = this.liveActivity ?? this.completedActivities.at(-1);
18317
+ if (item) {
18318
+ item.expanded = !item.expanded;
18319
+ this.redrawCompleted(item);
18320
+ }
18321
+ }
18322
+ activityMouse(button, row, press) {
18323
+ if (!press) return;
18324
+ const completed = this.completedActivities.find((item) => row >= item.row && row < item.row + item.height);
18325
+ if (completed && (button === 64 || button === 65)) {
18326
+ if (!completed.expanded) return;
18327
+ completed.end = Math.max(Math.min(5, completed.snapshot.entries.length), Math.min(completed.snapshot.entries.length, completed.end + (button === 64 ? -1 : 1)));
18328
+ completed.selected = -1;
18329
+ this.redrawCompleted(completed);
18330
+ return;
18331
+ }
18332
+ if (completed && button === 0) {
18333
+ const rows = activityRows(completed.snapshot, completed.expanded, completed.selected, completed.end);
18334
+ const entry = rows[row - completed.row]?.entry;
18335
+ if (row === completed.row) completed.expanded = !completed.expanded;
18336
+ else if (entry !== void 0) completed.selected = completed.selected === entry ? -1 : entry;
18337
+ this.redrawCompleted(completed);
18338
+ return;
18339
+ }
18340
+ }
18341
+ setInputFormatter(formatter) {
18342
+ this.inputFormatter = formatter;
17854
18343
  this.paintFooter();
17855
18344
  }
17856
18345
  setPrompt(prompt) {
@@ -17910,9 +18399,10 @@ var TerminalLayout = class {
17910
18399
  for (let row = first; row <= last; row += 1) erased += `${cup(row)}${CSI}2K`;
17911
18400
  return erased + saved;
17912
18401
  });
17913
- if (this.wrapPending && !/^[\r\n]/.test(stripVTControlCharacters(formatted))) {
18402
+ if (this.wrapPending && !/^[\r\n]/.test(stripVTControlCharacters2(formatted))) {
17914
18403
  this.rawWrite(`${cup(this.transcriptRow, this.columns)}\r
17915
18404
  `);
18405
+ if (this.transcriptRow === this.contentRows) this.shiftCompleted(1);
17916
18406
  this.transcriptRow = Math.min(this.contentRows, this.transcriptRow + 1);
17917
18407
  this.transcriptColumn = 1;
17918
18408
  this.wrapPending = false;
@@ -17977,6 +18467,7 @@ var TerminalLayout = class {
17977
18467
  this.wrapPending = true;
17978
18468
  }
17979
18469
  }
18470
+ if (this.transcriptRow > this.contentRows) this.shiftCompleted(this.transcriptRow - this.contentRows);
17980
18471
  this.transcriptRow = Math.min(this.contentRows, this.transcriptRow);
17981
18472
  }
17982
18473
  }
@@ -17987,8 +18478,10 @@ var TerminalLayout = class {
17987
18478
  composer() {
17988
18479
  const available = this.columns - 4;
17989
18480
  const prompt = fit(this.rl?.getPrompt() ?? this.prompt, Math.min(24, Math.max(2, Math.floor(available / 3))));
17990
- const input = plain(this.rl?.line ?? "");
17991
- const cursor = width(plain((this.rl?.line ?? "").slice(0, this.rl?.cursor ?? 0)));
18481
+ const raw = this.rl?.line ?? "";
18482
+ const formatted = this.inputFormatter?.(raw, this.rl?.cursor ?? 0) ?? { text: raw, cursor: this.rl?.cursor ?? 0 };
18483
+ const input = plain(formatted.text);
18484
+ const cursor = width(plain(formatted.text.slice(0, formatted.cursor)));
17992
18485
  const capacity = Math.max(1, available - width(prompt));
17993
18486
  const offset = Math.max(0, cursor - capacity + 1);
17994
18487
  let visible = "";
@@ -18021,29 +18514,45 @@ var TerminalLayout = class {
18021
18514
  const inner = this.columns - 2;
18022
18515
  const border = "\u2500".repeat(inner);
18023
18516
  const composer = this.composer();
18024
- const status = this.details.status ? `${plain(this.details.status)} \xB7 ` : "";
18025
- const modelRoom = Math.max(6, this.columns - width(`${status}${this.details.provider} \xB7 \xB7 ${this.details.mode}`));
18026
- const metadata = `${status}${this.details.provider} \xB7 ${fit(this.details.model, modelRoom)} \xB7 ${this.details.mode}`;
18027
- const usage = `${count3(this.details.tokens)} reported tokens \xB7 ${count3(this.details.steps)} steps \xB7 est ctx ${count3(this.details.contextUsed)}/${count3(this.details.contextMax)}`;
18517
+ const model = /^cosmic[- ]1$/i.test(this.details.model) ? "Cosmic 1" : this.details.model;
18518
+ const metadata = fit(model, this.columns);
18028
18519
  let frame = "";
18520
+ const activity = [];
18521
+ if ((activity.length || this.completedActivities.length) && !this.suspended) frame += `${CSI}?1006h${CSI}?1000h`;
18522
+ activity.forEach((row, index) => {
18523
+ const rendered = this.colorActivityRow(row);
18524
+ frame += `${cup(this.contentRows + 1 + index)}${CSI}0m${CSI}2K${rendered}`;
18525
+ });
18029
18526
  const suggestions = this.suggestionRows;
18030
18527
  const first = Math.max(0, Math.min(this.selected - Math.floor(suggestions / 2), this.suggestions.length - suggestions));
18528
+ const commandWidth = Math.min(
18529
+ Math.max(18, ...this.suggestions.map((row) => width(plain(row).split(/ {2,}/)[0] ?? ""))),
18530
+ Math.max(8, Math.floor(this.columns / 3))
18531
+ );
18031
18532
  for (let i = 0; i < suggestions; i += 1) {
18032
18533
  const index = first + i;
18033
18534
  const selected = index === this.selected;
18034
- frame += `${cup(this.contentRows + 1 + i)}${CSI}2K${selected ? `${CSI}7m` : ""}${fit(`${selected ? "\u203A" : " "} ${this.suggestions[index] ?? ""}`, this.columns)}${CSI}0m`;
18535
+ const entry = plain(this.suggestions[index] ?? "");
18536
+ const match = /^(.+?) {2,}(.+)$/.exec(entry);
18537
+ const command = fit(match?.[1] ?? entry, commandWidth);
18538
+ const description = match?.[2] ?? "";
18539
+ const row = fit(`${selected ? "\u203A" : " "} ${command}${description ? `${" ".repeat(Math.max(0, commandWidth - width(command)) + 6)}${description}` : ""}`, this.columns);
18540
+ const style = process.env.NO_COLOR ? "" : selected ? `${CSI}1m${CSI}39m${CSI}49m` : `${CSI}22m${CSI}39m${CSI}49m`;
18541
+ frame += `${cup(this.contentRows + activity.length + 1 + i)}${CSI}0m${CSI}2K${style}${row}${" ".repeat(this.columns - width(row))}${CSI}0m`;
18035
18542
  }
18036
18543
  const inputText = fit(composer.text, this.columns - 4);
18037
18544
  const paddedInput = inputText + " ".repeat(Math.max(0, this.columns - 4 - width(inputText)));
18038
- const lines = [`\u250C${border}\u2510`, `\u2502 ${paddedInput} \u2502`, `\u2514${border}\u2518`, fit(metadata, this.columns), fit(usage, this.columns)];
18545
+ const dimBorder = (value) => process.env.NO_COLOR ? value : `${CSI}90m${value}${CSI}39m`;
18546
+ const lines = [dimBorder(`\u250C${border}\u2510`), `${dimBorder("\u2502")} ${paddedInput} ${dimBorder("\u2502")}`, dimBorder(`\u2514${border}\u2518`), metadata];
18039
18547
  lines.forEach((line, index) => {
18040
- frame += `${cup(this.rows - 4 + index)}${CSI}2K${line}`;
18548
+ frame += `${cup(this.rows - 3 + index)}${CSI}2K${line}`;
18041
18549
  });
18042
18550
  this.rawWrite(`${frame}${cup(this.inputRow, composer.column)}${CSI}?25h`);
18043
18551
  }
18044
18552
  close() {
18045
18553
  if (this.closed) return;
18046
18554
  this.closed = true;
18555
+ this.restoreActivityInput?.();
18047
18556
  if (!this.installed) return;
18048
18557
  this.output.write = this.originalWrite;
18049
18558
  if (this.errorOutput && this.errorOutput !== this.output && this.originalErrorWrite) this.errorOutput.write = this.originalErrorWrite;
@@ -18068,6 +18577,26 @@ function createTerminalLayout(options) {
18068
18577
 
18069
18578
  // src/ui/welcome.ts
18070
18579
  import chalk10, { Chalk } from "chalk";
18580
+ import { homedir as homedir3 } from "os";
18581
+ import { sep } from "path";
18582
+
18583
+ // src/ui/provider-brand.ts
18584
+ import { stripVTControlCharacters as stripVTControlCharacters3 } from "util";
18585
+ var GOLD = "\x1B[38;2;202;168;103m";
18586
+ var DEFAULT_FOREGROUND = "\x1B[39m";
18587
+ function safeDisplayText(value) {
18588
+ return stripVTControlCharacters3(value).replace(/[\x00-\x1f\x7f]/g, " ");
18589
+ }
18590
+ function providerDisplayName(provider) {
18591
+ const name = safeDisplayText(provider);
18592
+ return name.toLowerCase() === "ultrac" ? "spinalfluid" : name;
18593
+ }
18594
+ function paintProviderSpan(line, offset, provider, color) {
18595
+ if (!color || provider !== "spinalfluid" || offset < 0 || line.slice(offset, offset + provider.length) !== provider) return line;
18596
+ return `${line.slice(0, offset)}${GOLD}${provider}${DEFAULT_FOREGROUND}${line.slice(offset + provider.length)}`;
18597
+ }
18598
+
18599
+ // src/ui/welcome.ts
18071
18600
  var LOGO = [
18072
18601
  "\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",
18073
18602
  "\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",
@@ -18079,8 +18608,7 @@ var LOGO = [
18079
18608
  function renderWelcome(info, options = {}) {
18080
18609
  const columns = normalizeColumns(options.columns);
18081
18610
  const paint = new Chalk({ level: options.color === false ? 0 : chalk10.level });
18082
- const cardWidth = columns;
18083
- const innerWidth = cardWidth - 4;
18611
+ const innerWidth = columns - 2;
18084
18612
  const wide = columns >= 64;
18085
18613
  const lines = [];
18086
18614
  if (wide) {
@@ -18091,16 +18619,14 @@ function renderWelcome(info, options = {}) {
18091
18619
  } else {
18092
18620
  lines.push("", ` ${paint.bold.white("ULTRAC")} ${paint.gray(`v${info.version}`)}`);
18093
18621
  }
18094
- const topLabel = wide ? ` ultrac v${info.version} ` : " session ";
18095
- const topFill = Math.max(0, cardWidth - topLabel.length - 2);
18096
- lines.push(paint.gray(`\u256D${topLabel}${"\u2500".repeat(topFill)}\u256E`));
18097
- lines.push(cardLine(`${info.model} via ${info.providerName}`, innerWidth, paint.bold));
18098
- lines.push(cardLine(`${info.mode} \xB7 ${info.protocol}`, innerWidth, paint.white));
18099
- lines.push(cardLine(shortenMiddle(info.workspace, innerWidth), innerWidth, paint.gray));
18100
- lines.push(paint.gray(`\u251C${"\u2500".repeat(cardWidth - 2)}\u2524`));
18101
- lines.push(cardLine(info.budget, innerWidth, paint.gray));
18102
- lines.push(cardLine(`logs ${shortenMiddle(info.diagnosticPath, Math.max(8, innerWidth - 5))}`, innerWidth, paint.gray));
18103
- lines.push(paint.gray(`\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`));
18622
+ const provider = providerDisplayName(info.providerName);
18623
+ const prefix = `${safeDisplayText(info.model)} \xB7 `;
18624
+ const account = `${provider}${info.planName ? ` ${safeDisplayText(info.planName)}` : ""}`;
18625
+ const modelLine = ` ${truncate3(`${prefix}${account}`, innerWidth)}`;
18626
+ lines.push("", paintProviderSpan(modelLine, 2 + prefix.length, provider, paint.level > 0));
18627
+ const home = homedir3();
18628
+ const workspace = info.workspace === home ? "~" : info.workspace.startsWith(`${home}${sep}`) ? `~${info.workspace.slice(home.length)}` : info.workspace;
18629
+ lines.push(paint.gray(` ${shortenMiddle(safeDisplayText(workspace), innerWidth)}`), "");
18104
18630
  lines.push(
18105
18631
  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")
18106
18632
  );
@@ -18118,10 +18644,6 @@ ${paint.gray("\u2502")}`,
18118
18644
  prompt: paint.bold.white("\u2570\u2500\u276F ")
18119
18645
  };
18120
18646
  }
18121
- function cardLine(value, width2, style) {
18122
- const clipped = truncate3(value, width2);
18123
- return `\u2502 ${style(clipped)}${" ".repeat(Math.max(0, width2 - clipped.length))} \u2502`;
18124
- }
18125
18647
  function normalizeColumns(columns) {
18126
18648
  return Number.isFinite(columns) ? Math.max(16, Math.floor(columns)) : 80;
18127
18649
  }
@@ -18137,11 +18659,18 @@ function shortenMiddle(value, width2) {
18137
18659
  return `${value.slice(0, side)}\u2026${value.slice(value.length - (width2 - side - 1))}`;
18138
18660
  }
18139
18661
 
18662
+ // src/ui/update-prompt.ts
18663
+ import { constants as constants7 } from "fs";
18664
+ import { lstat as lstat4, mkdir as mkdir15, open as open10, rename as rename4, unlink as unlink5 } from "fs/promises";
18665
+ import { randomBytes as randomBytes5 } from "crypto";
18666
+ import { homedir as homedir4 } from "os";
18667
+ import { join as join15, parse, resolve as resolve13 } from "path";
18668
+
18140
18669
  // src/update/check.ts
18141
18670
  import { spawn as spawn4 } from "child_process";
18142
18671
  import { randomBytes as randomBytes4 } from "crypto";
18143
18672
  import { readFile as readFile13, rename as rename3, unlink as unlink4, writeFile as writeFile12 } from "fs/promises";
18144
- import { join as join14 } from "path";
18673
+ import { isAbsolute as isAbsolute5, join as join14, relative as relative2, resolve as resolve12 } from "path";
18145
18674
  var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
18146
18675
  var QUERY_TIMEOUT_MS = 5e3;
18147
18676
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -18174,17 +18703,67 @@ async function queryLatestVersion(packageName) {
18174
18703
  return void 0;
18175
18704
  }
18176
18705
  }
18177
- async function installLatestVersion(packageName) {
18706
+ async function installLatestVersion(packageName, version = "latest") {
18707
+ if (version !== "latest" && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)) {
18708
+ throw new Error("Invalid npm release version.");
18709
+ }
18178
18710
  const result = await runNpm(
18179
- ["install", "--global", `${packageName}@latest`, "--no-audit", "--no-fund"],
18711
+ ["install", "--global", `${packageName}@${version}`, "--no-audit", "--no-fund"],
18180
18712
  { timeoutMs: INSTALL_TIMEOUT_MS, capture: false }
18181
18713
  );
18182
18714
  if (result.code !== 0) {
18183
18715
  throw new Error(
18184
- `npm could not install ${packageName}@latest. Confirm npm can reach the registry and install global packages.`
18716
+ `npm could not install ${packageName}@${version}. Confirm npm can reach the registry and install global packages.`
18185
18717
  );
18186
18718
  }
18187
18719
  }
18720
+ async function installVerifiedVersion(packageName, version) {
18721
+ await installLatestVersion(packageName, version);
18722
+ const result = await runNpm(["list", "--global", packageName, "--depth=0", "--json"], {
18723
+ timeoutMs: QUERY_TIMEOUT_MS,
18724
+ capture: true,
18725
+ maxOutputBytes: 4096
18726
+ });
18727
+ let installedVersion;
18728
+ try {
18729
+ const parsed = JSON.parse(result.stdout);
18730
+ if (typeof parsed === "object" && parsed !== null && "dependencies" in parsed && typeof parsed.dependencies === "object" && parsed.dependencies !== null) {
18731
+ const dependency = parsed.dependencies[packageName];
18732
+ if (typeof dependency === "object" && dependency !== null && "version" in dependency) {
18733
+ installedVersion = dependency.version;
18734
+ }
18735
+ }
18736
+ } catch {
18737
+ }
18738
+ if (result.code !== 0 || installedVersion !== version) {
18739
+ throw new Error("The update could not be verified. Run ultrac update and restart Ultrac.");
18740
+ }
18741
+ try {
18742
+ const globalRoot = await runNpm(["root", "--global"], {
18743
+ timeoutMs: QUERY_TIMEOUT_MS,
18744
+ capture: true,
18745
+ maxOutputBytes: 4096
18746
+ });
18747
+ const root = globalRoot.stdout.trim();
18748
+ if (globalRoot.code !== 0 || !isAbsolute5(root) || root.includes("\n")) throw new Error("Invalid npm root");
18749
+ const packageRoot = join14(root, packageName);
18750
+ const manifest = JSON.parse(await readFile13(join14(packageRoot, "package.json"), "utf8"));
18751
+ if (typeof manifest !== "object" || manifest === null || !("version" in manifest) || manifest.version !== version || !("bin" in manifest)) throw new Error("Invalid package");
18752
+ const bin = manifest.bin;
18753
+ const entry = typeof bin === "string" ? bin : typeof bin === "object" && bin !== null && "ultrac" in bin ? bin.ultrac : void 0;
18754
+ if (typeof entry !== "string") throw new Error("Missing executable");
18755
+ const executable = resolve12(packageRoot, entry);
18756
+ const withinPackage = relative2(packageRoot, executable);
18757
+ if (withinPackage.startsWith("..") || isAbsolute5(withinPackage)) throw new Error("Invalid executable");
18758
+ const probe = await runExecutable(process.execPath, [executable, "--version"], {
18759
+ timeoutMs: QUERY_TIMEOUT_MS,
18760
+ capture: true
18761
+ });
18762
+ if (probe.code !== 0 || probe.stdout.trim() !== version) throw new Error("Invalid executable version");
18763
+ } catch {
18764
+ throw new Error("The installed Ultrac executable could not be verified. Restart and run ultrac --version.");
18765
+ }
18766
+ }
18188
18767
  function isNewerVersion(candidate, current) {
18189
18768
  const left = parseVersion(candidate);
18190
18769
  const right = parseVersion(current);
@@ -18237,12 +18816,15 @@ async function writeCache(path, cache) {
18237
18816
  }
18238
18817
  }
18239
18818
  async function runNpm(args, options) {
18819
+ return runExecutable(process.platform === "win32" ? "npm.cmd" : "npm", args, options);
18820
+ }
18821
+ async function runExecutable(executable, args, options) {
18240
18822
  const env2 = { ...process.env };
18241
18823
  for (const key of Object.keys(env2)) {
18242
18824
  if (key.startsWith("ULTRAC_")) delete env2[key];
18243
18825
  }
18244
- return new Promise((resolve15) => {
18245
- const child = spawn4(process.platform === "win32" ? "npm.cmd" : "npm", args, {
18826
+ return new Promise((resolve17) => {
18827
+ const child = spawn4(executable, args, {
18246
18828
  env: env2,
18247
18829
  stdio: options.capture ? ["ignore", "pipe", "ignore"] : "inherit",
18248
18830
  windowsHide: true
@@ -18251,21 +18833,90 @@ async function runNpm(args, options) {
18251
18833
  if (options.capture) {
18252
18834
  child.stdout?.setEncoding("utf8");
18253
18835
  child.stdout?.on("data", (chunk) => {
18254
- if (stdout6.length <= 256) stdout6 += chunk;
18836
+ const limit = options.maxOutputBytes ?? 256;
18837
+ if (stdout6.length <= limit) stdout6 += chunk.slice(0, limit + 1 - stdout6.length);
18255
18838
  });
18256
18839
  }
18257
18840
  const timer = setTimeout(() => child.kill(), options.timeoutMs);
18258
18841
  child.once("error", () => {
18259
18842
  clearTimeout(timer);
18260
- resolve15({ code: 1, stdout: "" });
18843
+ resolve17({ code: 1, stdout: "" });
18261
18844
  });
18262
18845
  child.once("close", (code) => {
18263
18846
  clearTimeout(timer);
18264
- resolve15({ code: code ?? 1, stdout: stdout6 });
18847
+ resolve17({ code: code ?? 1, stdout: stdout6 });
18265
18848
  });
18266
18849
  });
18267
18850
  }
18268
18851
 
18852
+ // src/ui/update-prompt.ts
18853
+ var UPDATE_CHOICES = ["Update now", "Skip", "Skip until next version"];
18854
+ async function offerUpdatePrompt(status, options) {
18855
+ const version = status.latestVersion;
18856
+ if (!options.interactive || options.signal?.aborted || !status.updateAvailable || !version || !validVersion(version) || !validVersion(status.currentVersion) || !isNewerVersion(version, status.currentVersion) || await skippedVersion() === version) return "not_offered";
18857
+ const answer = await options.ask(
18858
+ `Ultrac ${version} is available. You are running ${status.currentVersion}.`,
18859
+ UPDATE_CHOICES,
18860
+ options.signal
18861
+ );
18862
+ if (options.signal?.aborted) return "skip";
18863
+ if (answer === UPDATE_CHOICES[0]) return "update_now";
18864
+ if (answer === UPDATE_CHOICES[2]) {
18865
+ await rememberSkippedVersion(version);
18866
+ return "skip_version";
18867
+ }
18868
+ return "skip";
18869
+ }
18870
+ function validVersion(version) {
18871
+ return version.length <= 128 && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version);
18872
+ }
18873
+ async function statePath(create) {
18874
+ const home = resolve13(ultracHomeDir());
18875
+ if (home === parse(home).root || home === resolve13(homedir4())) {
18876
+ throw new Error("Ultrac update preferences require a dedicated ULTRAC_HOME directory.");
18877
+ }
18878
+ if (create) await mkdir15(home, { recursive: true, mode: 448 });
18879
+ const info = await lstat4(home);
18880
+ if (!info.isDirectory() || info.isSymbolicLink()) {
18881
+ throw new Error("Ultrac update preferences require a real home directory.");
18882
+ }
18883
+ return join15(home, "update-preferences.json");
18884
+ }
18885
+ async function skippedVersion() {
18886
+ try {
18887
+ const path = await statePath(false);
18888
+ const file = await open10(path, constants7.O_RDONLY | constants7.O_NOFOLLOW);
18889
+ try {
18890
+ const info = await file.stat();
18891
+ if (!info.isFile() || info.size > 1024) return void 0;
18892
+ const parsed = JSON.parse(await file.readFile("utf8"));
18893
+ if (typeof parsed !== "object" || parsed === null || !("skippedVersion" in parsed)) return void 0;
18894
+ return typeof parsed.skippedVersion === "string" && validVersion(parsed.skippedVersion) ? parsed.skippedVersion : void 0;
18895
+ } finally {
18896
+ await file.close();
18897
+ }
18898
+ } catch {
18899
+ return void 0;
18900
+ }
18901
+ }
18902
+ async function rememberSkippedVersion(version) {
18903
+ const path = await statePath(true);
18904
+ const temporary = `${path}.${randomBytes5(8).toString("hex")}.tmp`;
18905
+ try {
18906
+ const file = await open10(temporary, constants7.O_WRONLY | constants7.O_CREAT | constants7.O_EXCL, 384);
18907
+ try {
18908
+ await file.writeFile(`${JSON.stringify({ skippedVersion: version })}
18909
+ `, "utf8");
18910
+ await file.sync();
18911
+ } finally {
18912
+ await file.close();
18913
+ }
18914
+ await rename4(temporary, path);
18915
+ } finally {
18916
+ await unlink5(temporary).catch(() => void 0);
18917
+ }
18918
+ }
18919
+
18269
18920
  // src/commands/repl.ts
18270
18921
  var HELP = `Commands:
18271
18922
  /help Show this help
@@ -18296,7 +18947,7 @@ var HELP = `Commands:
18296
18947
  /whoami Show sign-in state
18297
18948
  /tools List available tools
18298
18949
  /logs Show the private diagnostic log path
18299
- /ghost [on|off|status] Quiet activity output; diagnostics stay enabled
18950
+ /ghost [on|off|status] Quiet activity output; diagnostics stay enabled
18300
18951
  /output [number|list] Inspect saved tool output (latest by default)
18301
18952
  /memory List and inspect stored memories
18302
18953
  /skill <name> [args] Run an installed skill
@@ -18312,11 +18963,12 @@ Custom commands come from skills that declare a 'command:' key in their
18312
18963
  frontmatter; built-ins always win a name clash. Type anything else to talk to Ultrac.`;
18313
18964
  var EXIT_REPL_NOT_STARTED = 1;
18314
18965
  function registerReplCommand(program2) {
18315
- program2.command("interactive", { isDefault: true }).alias("repl").description("Start an interactive ultrac session").argument("[task...]", "Optional initial prompt to send on start").option("-w, --workspace <path>", "Workspace path", ".").option("--provider <name>", "Provider: ultrac (default) or echo (offline test stub)").option("--model <model>", "Model name").option("--base-url <url>", "Ultrac API base URL (overrides ULTRAC_BASE_URL and the stored login)").option("--mode <name>", `Agent mode: ${agentModeNames.join(", ")}`).option("--max-steps <number>", "Maximum agent steps per turn").option("--allow-write", "Allow file writes without prompting", false).option("--allow-shell", "Grant broad, unsandboxed shell access without prompting", false).option("--allow-network", "Allow network access without prompting", false).option("--no-native-tools", "Use the JSON tool protocol instead of native tool calling").option("--tools <list>", "Tools to advertise: `all`, or a comma-separated list. Default: the mode's set").option("--context-tokens <number>", "Model context window in tokens (default 262144)").option("--max-tool-result-chars <number>", "Cap on one tool result in the transcript (default 6000)").action(async (taskParts, options, command) => {
18966
+ program2.command("interactive", { isDefault: true }).alias("repl").description("Start an interactive ultrac session").argument("[task...]", "Optional initial prompt to send on start").option("-w, --workspace <path>", "Workspace path", ".").option("--provider <name>", "Provider: ultrac (default) or echo (offline test stub)").option("--model <model>", "Model name").option("--base-url <url>", "Ultrac API base URL (overrides ULTRAC_BASE_URL and the stored login)").option("--mode <name>", `Agent mode: ${agentModeNames.join(", ")}`).option("--max-steps <number>", "Maximum agent steps per turn").option("--permissions <mode>", "Tool permission preset: readonly, workspace, or full (no tool approval prompts)").option("--allow-write", "Allow file writes without prompting", false).option("--allow-shell", "Grant broad, unsandboxed shell access without prompting", false).option("--allow-network", "Allow network access without prompting", false).option("--no-native-tools", "Use the JSON tool protocol instead of native tool calling").option("--tools <list>", "Tools to advertise: `all`, or a comma-separated list. Default: the mode's set").option("--context-tokens <number>", "Model context window in tokens (default 262144)").option("--max-tool-result-chars <number>", "Cap on one tool result in the transcript (default 6000)").action(async (taskParts, options, command) => {
18316
18967
  const task = (taskParts ?? []).join(" ").trim();
18317
18968
  const invokedByName = namesReplCommand(command, command.parent?.args[0]);
18318
18969
  const mistyped = invokedByName ? void 0 : mistypedCommand(task, knownCommandNames(program2));
18319
18970
  if (mistyped) throw new Error(mistyped);
18971
+ if (options.permissions && !["readonly", "workspace", "full"].includes(options.permissions)) throw new Error("--permissions must be readonly, workspace, or full.");
18320
18972
  await runRepl(options, task);
18321
18973
  });
18322
18974
  }
@@ -18424,7 +19076,7 @@ async function runRepl(options, initialTask = "") {
18424
19076
  diagnosticEvent("provider.retry", event);
18425
19077
  console.log(
18426
19078
  chalk11.yellow(
18427
- ` retrying ${event.target.provider} (${event.target.model}) in ${event.delayMs}ms after ${event.classification.reason}`
19079
+ ` ${event.classification.reason} Retrying ${event.target.model} in ${(event.delayMs / 1e3).toFixed(1)} seconds (retry ${event.attempt}).`
18428
19080
  )
18429
19081
  );
18430
19082
  }
@@ -18435,9 +19087,9 @@ async function runRepl(options, initialTask = "") {
18435
19087
  return;
18436
19088
  }
18437
19089
  const basePermissions = {
18438
- write: options.allowWrite || config.permissions.write,
18439
- shell: options.allowShell || config.permissions.shell,
18440
- network: options.allowNetwork || config.permissions.network
19090
+ write: options.permissions ? options.permissions !== "readonly" : options.allowWrite || config.permissions.write,
19091
+ shell: options.permissions ? options.permissions === "full" : options.allowShell || config.permissions.shell,
19092
+ network: options.permissions ? options.permissions === "full" : options.allowNetwork || config.permissions.network
18441
19093
  };
18442
19094
  const registry = createDefaultToolRegistry();
18443
19095
  const mcpRuntime = await connectWorkspaceMcpServers(config.mcpServers, { workspace, registry });
@@ -18454,7 +19106,11 @@ async function runRepl(options, initialTask = "") {
18454
19106
  ...toolSelection ? { tools: toolSelection } : {}
18455
19107
  };
18456
19108
  setSubagentRuntime({ provider, model: init.model, mode: init.mode, registry });
18457
- return useNativeTools ? new NativeSession(opts) : new InteractiveSession(opts);
19109
+ const created = useNativeTools ? new NativeSession(opts) : new InteractiveSession(opts);
19110
+ if (options.permissions) for (const permission of ["write", "shell", "network"]) {
19111
+ created.setPermission(permission, init.permissions[permission]);
19112
+ }
19113
+ return created;
18458
19114
  };
18459
19115
  let session = createSession({
18460
19116
  model,
@@ -18467,6 +19123,8 @@ async function runRepl(options, initialTask = "") {
18467
19123
  bindSignals: false,
18468
19124
  details: { provider: provider.name, model: session.model, mode: session.mode, status: "Idle" }
18469
19125
  }) : void 0;
19126
+ setActivitySink(layout?.enabled ? (snapshot) => layout.setActivity(snapshot) : void 0);
19127
+ setWorkingSink(layout?.enabled ? (text) => layout.setWorkingStatus(text) : void 0);
18470
19128
  const refreshDetails = () => {
18471
19129
  if (!layout?.enabled) return;
18472
19130
  const steps = session.toSession().steps;
@@ -18504,7 +19162,8 @@ async function runRepl(options, initialTask = "") {
18504
19162
  workspace,
18505
19163
  useNativeTools ? "native tools" : "json protocol",
18506
19164
  describeBudget(mode, toolSelection, registry),
18507
- diagnosticPath2
19165
+ diagnosticPath2,
19166
+ provider.name.toLowerCase() === "ultrac" ? await readAccountPlan(await resolveUltracCredentials()) : void 0
18508
19167
  );
18509
19168
  if (mcpRuntime && mcpRuntime.statuses.length > 0) {
18510
19169
  console.log(chalk11.bold("\nMCP servers"));
@@ -18517,15 +19176,14 @@ async function runRepl(options, initialTask = "") {
18517
19176
  throw new Error(`Could not start the session: ${errorMessage2(error)}`, { cause: error });
18518
19177
  }
18519
19178
  const update = await updateCheck;
18520
- if (update?.updateAvailable && update.latestVersion) {
18521
- console.log(chalk11.yellow(`
18522
- \u2191 Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
18523
- console.log(chalk11.gray(" Run `ultrac update` after this session."));
19179
+ if (!stdin5.isTTY && update?.updateAvailable && update.latestVersion) {
19180
+ console.log(chalk11.yellow(`Update available: ${update.currentVersion} \u2192 ${update.latestVersion}. Run ultrac update.`));
18524
19181
  }
18525
19182
  const rl = createInterface3({ input: stdin5, output: layout?.enabled ? layout.outputProxy : stdout5 });
18526
19183
  layout?.attachReadline(rl);
19184
+ layout?.setInputFormatter(formatImageInput);
18527
19185
  refreshDetails();
18528
- const builtinEntries = BUILTIN_SLASH_COMMANDS.map((command) => ({ text: `/${command}`, label: `/${command}` }));
19186
+ const builtinEntries = builtinSlashEntries(BUILTIN_SLASH_COMMANDS, HELP);
18529
19187
  const slashCompletion = layout?.enabled ? attachSlashCompletion(rl, async () => {
18530
19188
  const [commands, skills] = await Promise.all([listSkillCommands(workspace), listSkills(workspace)]);
18531
19189
  const declared = new Set(commands.commands.map((command) => command.skillName));
@@ -18550,7 +19208,7 @@ async function runRepl(options, initialTask = "") {
18550
19208
  }
18551
19209
  };
18552
19210
  const drainQueuedMessages = () => reader.drainQueuedMessages();
18553
- let autonomousRounds = 1;
19211
+ let autonomousRounds = 5;
18554
19212
  setAskUserRuntime(async (text, options2, signal) => {
18555
19213
  console.log(chalk11.white(`
18556
19214
  ? ${text}`));
@@ -18605,29 +19263,35 @@ async function runRepl(options, initialTask = "") {
18605
19263
  rl.close();
18606
19264
  };
18607
19265
  process.on("SIGTERM", terminate);
18608
- let turnStartedAt;
18609
- const footerTimer = setInterval(() => {
18610
- const seconds = turnStartedAt === void 0 ? void 0 : Math.floor((Date.now() - turnStartedAt) / 1e3);
18611
- layout?.updateDetails({ status: seconds === void 0 ? "Idle" : `Running ${Math.floor(seconds / 60).toString().padStart(2, "0")}:${(seconds % 60).toString().padStart(2, "0")}` });
18612
- }, 1e3);
18613
- footerTimer.unref();
18614
19266
  const withInterrupt = async (run) => {
18615
19267
  turnAbort = new AbortController();
18616
- turnStartedAt = Date.now();
18617
- layout?.updateDetails({ status: "Running 00:00" });
19268
+ layout?.updateDetails({ status: "Running" });
18618
19269
  try {
18619
19270
  return await run(turnAbort.signal);
18620
19271
  } finally {
18621
19272
  turnAbort = void 0;
18622
- turnStartedAt = void 0;
18623
19273
  refreshDetails();
18624
19274
  layout?.updateDetails({ status: "Idle" });
18625
19275
  }
18626
19276
  };
18627
19277
  try {
19278
+ if (update) {
19279
+ const decision = await withInterrupt((signal) => offerUpdatePrompt(update, {
19280
+ interactive: Boolean(stdin5.isTTY && stdout5.isTTY),
19281
+ signal,
19282
+ ask: (text, choices, askSignal) => getAskUserRuntime()(text, [...choices], askSignal)
19283
+ }));
19284
+ if (decision === "update_now") {
19285
+ layout?.close();
19286
+ rl.close();
19287
+ await installVerifiedVersion(packageInfo.name, update.latestVersion);
19288
+ console.log(`Updated to ${update.latestVersion}. Run ultrac again to use the new version.`);
19289
+ return;
19290
+ }
19291
+ }
18628
19292
  if (initialTask) {
18629
19293
  console.log(chalk11.white(`
18630
- \u203A ${initialTask}`));
19294
+ \u203A ${formatImageInput(initialTask).text}`));
18631
19295
  await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
18632
19296
  }
18633
19297
  while (running) {
@@ -18642,10 +19306,10 @@ async function runRepl(options, initialTask = "") {
18642
19306
  }
18643
19307
  const line = await reader.next();
18644
19308
  if (line === null) break;
18645
- if (layout?.enabled && line.trim()) console.log(`\u203A ${line.replace(/[\x00-\x1f\x7f]/g, " ")}`);
19309
+ if (layout?.enabled && line.trim()) console.log(`\u203A ${formatImageInput(line).text.replace(/[\x00-\x1f\x7f]/g, " ")}`);
18646
19310
  const raw = line.trim();
18647
19311
  if (!raw) continue;
18648
- if (raw.startsWith("/")) {
19312
+ if (raw.startsWith("/") && !hasDroppedImage(raw)) {
18649
19313
  const previousSession = session;
18650
19314
  const slashCommand = raw.slice(1).trim().split(/\s+/, 1)[0]?.toLowerCase();
18651
19315
  const handled = await withInterrupt(async (signal) => {
@@ -18658,7 +19322,7 @@ async function runRepl(options, initialTask = "") {
18658
19322
  resetContextBudgetNotices();
18659
19323
  console.log(chalk11.gray("Started a fresh conversation."));
18660
19324
  }, async (prompt) => {
18661
- await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds);
19325
+ await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds, false);
18662
19326
  }, signal, async (id) => {
18663
19327
  const lineage = await loadSessionLineage(workspace, id);
18664
19328
  const restored = createSession({ model: session.model, mode: session.mode, permissions: session.currentPermissions });
@@ -18681,7 +19345,7 @@ async function runRepl(options, initialTask = "") {
18681
19345
  if (handled === "unhandled") {
18682
19346
  try {
18683
19347
  await withInterrupt((signal) => runSkillCommand(session, raw, workspace, question, async (prompt) => {
18684
- await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds);
19348
+ await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds, false);
18685
19349
  }));
18686
19350
  } finally {
18687
19351
  await persistSession();
@@ -18696,11 +19360,12 @@ async function runRepl(options, initialTask = "") {
18696
19360
  await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
18697
19361
  }
18698
19362
  } finally {
18699
- clearInterval(footerTimer);
18700
19363
  slashCompletion?.cleanup();
18701
19364
  process.off("SIGTERM", terminate);
18702
19365
  if (stdin5.isTTY) stdin5.off("keypress", onKeypress);
18703
19366
  setAskUserRuntime(void 0);
19367
+ setActivitySink(void 0);
19368
+ setWorkingSink(void 0);
18704
19369
  rl.close();
18705
19370
  try {
18706
19371
  await saveTranscript(session, writeCheckpoint);
@@ -18711,14 +19376,50 @@ async function runRepl(options, initialTask = "") {
18711
19376
  }
18712
19377
  console.log(chalk11.gray("Goodbye."));
18713
19378
  }
18714
- async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing) {
18715
- const spinner = new Spinner();
19379
+ async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop = true, activityState) {
19380
+ const activity = isGhostEnabled(session) ? void 0 : getActivitySink();
19381
+ const entries = activityState?.entries ?? [];
19382
+ let thinkingMs = activityState?.thinkingMs ?? 0;
19383
+ let thinkingStarted;
19384
+ let turnFailed = false;
19385
+ const refreshActivity = (finished = false) => {
19386
+ if (activityState) {
19387
+ activityState.thinkingMs = thinkingMs;
19388
+ activityState.failed ||= turnFailed;
19389
+ }
19390
+ activity?.({ entries: [...entries], thinkingMs, finished: finished && !activityState, failed: turnFailed });
19391
+ };
19392
+ refreshActivity();
19393
+ const spinner = new Spinner("working", activity ? getWorkingSink() : void 0);
19394
+ const workSpinner = new Spinner("working", activity ? getWorkingSink() : void 0);
19395
+ const activeTools = /* @__PURE__ */ new Map();
19396
+ let permissionPrompts = 0;
19397
+ const renderWorking = () => {
19398
+ workSpinner.stop();
19399
+ const active = [...activeTools.entries()].at(-1);
19400
+ if (!active || quiet || permissionPrompts || [...activeTools.keys()].some((call) => call.tool === "ask_user")) return;
19401
+ workSpinner.update(`working \xB7 ${toolActivityLabel(active[0])}${activeTools.size > 1 ? ` (+${activeTools.size - 1} active)` : ""}`);
19402
+ workSpinner.start(isComposing, active[1]);
19403
+ };
18716
19404
  const quiet = isGhostEnabled(session);
18717
19405
  const detachProgress = quiet ? () => {
18718
19406
  } : attachProgressUi();
18719
19407
  let deltaChars = 0;
19408
+ const progress2 = quiet ? void 0 : startTaskProgress({
19409
+ snapshot: () => ({
19410
+ completed: entries.length,
19411
+ failed: entries.filter((entry) => !entry.ok).length,
19412
+ activeTool: [...activeTools.keys()].at(-1)?.tool,
19413
+ waitingForModel: thinkingStarted !== void 0
19414
+ }),
19415
+ holdWhile: () => permissionPrompts > 0 || [...activeTools.keys()].some((call) => call.tool === "ask_user"),
19416
+ write: (text) => console.log(`
19417
+ ${text}`)
19418
+ });
18720
19419
  try {
18721
- await session.ask(input, {
19420
+ const prepared = allowImageDrop ? await prepareImageInput(input) : { text: input };
19421
+ await session.ask(prepared.text, {
19422
+ ...prepared.image ? { image: prepared.image } : {},
18722
19423
  ...signal ? { signal } : {},
18723
19424
  ...persist ? {
18724
19425
  onCheckpoint: async () => {
@@ -18731,23 +19432,48 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
18731
19432
  },
18732
19433
  onModelStart: () => {
18733
19434
  deltaChars = 0;
18734
- if (!quiet) spinner.start(isComposing);
19435
+ thinkingStarted = performance.now();
19436
+ if (!quiet) {
19437
+ spinner.update("working");
19438
+ spinner.start(isComposing, performance.now() - thinkingMs);
19439
+ }
18735
19440
  },
18736
19441
  onModelDelta: (text) => {
18737
19442
  deltaChars += text.length;
18738
- if (!quiet) spinner.update(`thinking (${deltaChars} chars)`);
19443
+ if (!quiet) spinner.update(`working (${deltaChars} chars)`);
19444
+ },
19445
+ onModelSettled: () => {
19446
+ if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
19447
+ thinkingStarted = void 0;
19448
+ spinner.stop(activity ? void 0 : signal?.aborted ? "Work stopped after" : "Worked for");
19449
+ refreshActivity();
18739
19450
  },
18740
- onModelSettled: () => spinner.stop(),
18741
19451
  onAssistantMessage: (message) => {
18742
19452
  if (message.trim()) {
19453
+ progress2?.markMessage();
18743
19454
  console.log(`
18744
19455
  ${message.trim()}`);
18745
19456
  }
18746
19457
  },
18747
19458
  onToolStart: (call) => {
18748
- if (!quiet) console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
19459
+ activeTools.set(call, performance.now());
19460
+ renderWorking();
19461
+ if (!stdout5.isTTY && !quiet) console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
19462
+ },
19463
+ onToolResult: (call, result) => {
19464
+ workSpinner.stop();
19465
+ const started = activeTools.get(call);
19466
+ activeTools.delete(call);
19467
+ const duration = started === void 0 ? 0 : performance.now() - started;
19468
+ entries.push(activityEntry(call, result, duration));
19469
+ refreshActivity();
19470
+ if (!activity && !quiet) {
19471
+ const dot = result.ok ? chalk11.green("\u25CF") : chalk11.red("\u25CF");
19472
+ console.log(` ${dot} ${toolActivityLabel(call)}${duration >= 1e3 ? ` \xB7 Worked for ${formatSeconds(duration)}` : ""}`);
19473
+ }
19474
+ printToolResult2(result, Boolean(activity));
19475
+ renderWorking();
18749
19476
  },
18750
- onToolResult: (_call, result) => printToolResult2(result),
18751
19477
  ...drainQueuedMessages ? {
18752
19478
  drainQueuedMessages,
18753
19479
  onQueuedMessage: (message) => {
@@ -18762,7 +19488,7 @@ ${message.trim()}`);
18762
19488
  console.log(chalk11.yellow(` ! ${notice}`));
18763
19489
  },
18764
19490
  onStopped: (reason) => {
18765
- if (reason === "aborted") return;
19491
+ if (reason === "aborted" || reason === "max-steps" && activityState?.continuing) return;
18766
19492
  const line = stopReasonLine(reason);
18767
19493
  const hint = turnStopHint(reason, session.maxSteps);
18768
19494
  if (!line || !hint) return;
@@ -18771,42 +19497,69 @@ ${message.trim()}`);
18771
19497
  console.log(chalk11.gray(` ${hint}`));
18772
19498
  },
18773
19499
  requestPermission: async (call, permission) => {
18774
- 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 `;
18775
- const rawAnswer = await question(chalk11.yellow(prompt), signal);
18776
- if (signal?.aborted || rawAnswer === null) return "deny";
18777
- const answer = rawAnswer.trim().toLowerCase();
18778
- if (answer === "a" || answer === "always") return "always";
18779
- if (answer === "o" || answer === "once" || answer === "y" || answer === "yes") return "once";
18780
- if (answer === "e" || answer === "never") {
18781
- console.log(
18782
- chalk11.gray(
18783
- ` ${permission} refused for the rest of the session \u2014 nothing will ask again. /allow ${permission} undoes it.`
18784
- )
18785
- );
18786
- return "never";
19500
+ permissionPrompts += 1;
19501
+ workSpinner.stop();
19502
+ try {
19503
+ const prompt = permission === "shell" && call.tool === "run_shell" ? shellApprovalPrompt(call, session.toSession().workspace) : permission === "shell" ? ` ${call.tool} wants shell access.
19504
+ 1. [o]nce (this call only)
19505
+ 2. [a]lways (this session)
19506
+ 3. [n]o (this call)
19507
+ 4. n[e]ver (this session)
19508
+ Answer: ` : ` ${call.tool} wants ${permission} access.
19509
+ 1. [o]nce
19510
+ 2. [a]lways (this session)
19511
+ 3. [n]o
19512
+ 4. n[e]ver (this session)
19513
+ Answer: `;
19514
+ const choices = ["Allow once", "Allow for this session", "Deny this call", "Never allow this permission this session"];
19515
+ const asker = getAskUserRuntime();
19516
+ const rawAnswer = asker ? await asker(prompt.split("\n").slice(0, -5).join("\n"), choices, signal) : await question(chalk11.yellow(prompt), signal);
19517
+ if (signal?.aborted || rawAnswer === null) return "deny";
19518
+ const answer = rawAnswer.trim().toLowerCase();
19519
+ if (answer === "a" || answer === "always" || answer === "2" || answer === choices[1].toLowerCase()) return "always";
19520
+ if (answer === "o" || answer === "once" || answer === "y" || answer === "yes" || answer === "1" || answer === choices[0].toLowerCase()) return "once";
19521
+ if (answer === "e" || answer === "never" || answer === "4" || answer === choices[3].toLowerCase()) {
19522
+ console.log(
19523
+ chalk11.gray(
19524
+ ` ${permission} refused for the rest of the session \u2014 nothing will ask again. /allow ${permission} undoes it.`
19525
+ )
19526
+ );
19527
+ return "never";
19528
+ }
19529
+ return "deny";
19530
+ } finally {
19531
+ permissionPrompts -= 1;
19532
+ renderWorking();
18787
19533
  }
18788
- return "deny";
18789
19534
  }
18790
19535
  });
18791
19536
  return true;
18792
19537
  } catch (error) {
19538
+ turnFailed = true;
18793
19539
  spinner.stop();
18794
19540
  printTurnError(error);
18795
19541
  return false;
18796
19542
  } finally {
19543
+ spinner.stop();
19544
+ workSpinner.stop();
19545
+ activeTools.clear();
19546
+ if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
19547
+ refreshActivity(true);
19548
+ progress2?.stop();
18797
19549
  detachProgress();
18798
19550
  }
18799
19551
  }
18800
- async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing, maxRounds = 1) {
19552
+ async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing, maxRounds = 1, allowImageDrop = true) {
19553
+ const activityState = { entries: [], thinkingMs: 0, continuing: maxRounds > 1 };
18801
19554
  try {
18802
- if (maxRounds === 1) return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing);
19555
+ if (maxRounds === 1) return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop, activityState);
18803
19556
  let successful = true;
18804
19557
  const result = await runAutonomousLoop({
18805
19558
  maxRounds,
18806
19559
  signal,
18807
19560
  classifyFailure: () => ({ kind: "unknown", retryable: false, failoverable: false, reason: "Interactive provider retries already exhausted." }),
18808
19561
  runRound: async (round, _previous, roundSignal) => {
18809
- successful = await runTurn(session, round === 1 ? input : AUTONOMOUS_CONTINUE_TASK, question, roundSignal, drainQueuedMessages, persist, isComposing);
19562
+ successful = await runTurn(session, round === 1 ? input : AUTONOMOUS_CONTINUE_TASK, question, roundSignal, drainQueuedMessages, persist, isComposing, allowImageDrop, activityState);
18810
19563
  await persist(session);
18811
19564
  const snapshot = session.toSession();
18812
19565
  return successful ? snapshot : { ...snapshot, stopReason: roundSignal.aborted ? "aborted" : "error" };
@@ -18818,6 +19571,7 @@ async function runAndCheckpointTurn(session, input, question, signal, persist, d
18818
19571
  if (result.haltReason !== "completed" && result.haltReason !== "aborted") console.log(chalk11.gray(`Automatic continuation stopped: ${result.haltReason}. Session preserved.`));
18819
19572
  return successful;
18820
19573
  } finally {
19574
+ if (!isGhostEnabled(session)) getActivitySink()?.({ ...activityState, entries: [...activityState.entries], finished: true });
18821
19575
  await persist(session);
18822
19576
  }
18823
19577
  }
@@ -19000,7 +19754,7 @@ ${extra}`);
19000
19754
  else {
19001
19755
  for (const field of ["inputTokens", "outputTokens", "totalTokens"]) {
19002
19756
  const counts = reported.flatMap((step) => typeof step.usage?.[field] === "number" ? [step.usage[field]] : []);
19003
- console.log(`${field}: ${counts.length ? counts.reduce((sum, count4) => sum + count4, 0) : "not reported"}`);
19757
+ console.log(`${field}: ${counts.length ? counts.reduce((sum, count3) => sum + count3, 0) : "not reported"}`);
19004
19758
  }
19005
19759
  console.log(`Counts cover ${reported.length}/${steps.length} agent steps in this session. Compaction calls and ancestor sessions are excluded. Card charges are not inferred from tokens.`);
19006
19760
  }
@@ -19077,7 +19831,7 @@ ${extra}`);
19077
19831
  for (const permission of ["write", "shell", "network"]) {
19078
19832
  session.setPermission(permission, argument === "full" || argument === "workspace" && permission === "write");
19079
19833
  }
19080
- console.log("Permissions updated. These are tool grants; shell access is not an operating-system sandbox.");
19834
+ console.log(argument === "full" ? "Full access: file, shell, and network tools run without approval prompts for this session." : "Permissions updated. Ungranted tools are denied without prompting; /allow restores access.");
19081
19835
  }
19082
19836
  const p = session.currentPermissions;
19083
19837
  console.log(`write: ${p.write} shell: ${p.shell} network: ${p.network}`);
@@ -19127,6 +19881,8 @@ ${extra}`);
19127
19881
  }
19128
19882
  const entry = results[selected - 1];
19129
19883
  console.log(`${selected}. ${toolActivityLabel(entry.call)} (${entry.result.ok ? "ok" : "failed"})`);
19884
+ const detail = activityEntry(entry.call, entry.result, 0);
19885
+ if (detail.request) console.log(detail.request);
19130
19886
  console.log(entry.result.content);
19131
19887
  for (const warning of getMcpArchiveWarnings(entry.result)) console.log(chalk11.yellow(`Warning: ${warning}`));
19132
19888
  return "ok";
@@ -19205,7 +19961,7 @@ async function runSkillCommand(session, raw, workspace, question, runPrompt) {
19205
19961
  chalk11.gray(` /${resolved.command.command} \u2192 skill ${resolved.command.skillName} (${resolved.command.scope})`)
19206
19962
  );
19207
19963
  if (runPrompt) await runPrompt(resolved.prompt);
19208
- else await runTurn(session, resolved.prompt, question);
19964
+ else await runTurn(session, resolved.prompt, question, void 0, void 0, void 0, void 0, false);
19209
19965
  return;
19210
19966
  }
19211
19967
  if (resolved.status === "conflict") {
@@ -19270,7 +20026,7 @@ var LineReader = class {
19270
20026
  const messages = [];
19271
20027
  while (this.queue.length) {
19272
20028
  const next = this.queue[0].trim();
19273
- if (next.startsWith("/")) break;
20029
+ if (next.startsWith("/") || hasDroppedImage(next)) break;
19274
20030
  this.queue.shift();
19275
20031
  if (next) messages.push(next);
19276
20032
  }
@@ -19283,8 +20039,8 @@ var LineReader = class {
19283
20039
  if (buffered !== void 0) return Promise.resolve(buffered);
19284
20040
  }
19285
20041
  if (this.closed) return Promise.resolve(null);
19286
- return new Promise((resolve15) => {
19287
- const waiter = { resolve: resolve15, ...signal ? { signal } : {} };
20042
+ return new Promise((resolve17) => {
20043
+ const waiter = { resolve: resolve17, ...signal ? { signal } : {} };
19288
20044
  if (signal) {
19289
20045
  waiter.onAbort = () => {
19290
20046
  const index = this.waiters.indexOf(waiter);
@@ -19317,11 +20073,12 @@ function handleReplExit(activeTurn) {
19317
20073
  activeTurn?.abort();
19318
20074
  return "exit-session";
19319
20075
  }
19320
- function printBanner(version, providerName, model, mode, workspace, protocol, budget, diagnosticPath2) {
20076
+ function printBanner(version, providerName, model, mode, workspace, protocol, budget, diagnosticPath2, planName) {
19321
20077
  console.log(renderWelcome(
19322
20078
  {
19323
20079
  version,
19324
20080
  providerName,
20081
+ planName,
19325
20082
  model,
19326
20083
  mode,
19327
20084
  workspace,
@@ -19346,9 +20103,9 @@ function describeAdvertisedTools(mode, selection, registry = createDefaultToolRe
19346
20103
  names: advertised.map((tool) => tool.name)
19347
20104
  };
19348
20105
  }
19349
- function printToolResult2(result) {
20106
+ function printToolResult2(result, collapsed = false) {
19350
20107
  const failure = toolFailurePreview(result);
19351
- if (failure) console.log(chalk11.red(` Failed: ${failure}`));
20108
+ if (failure && !collapsed) console.log(chalk11.red(` Failed: ${failure}`));
19352
20109
  for (const warning of getMcpArchiveWarnings(result)) {
19353
20110
  console.log(chalk11.yellow(indent3(`Warning: ${warning}`)));
19354
20111
  }
@@ -19365,11 +20122,16 @@ function shellApprovalPrompt(call, workspace) {
19365
20122
  const timeoutMs = typeof call.input.timeoutMs === "number" ? call.input.timeoutMs : 3e4;
19366
20123
  return [
19367
20124
  " run_shell wants shell access.",
19368
- " WARNING: this command runs unsandboxed as your user. It can read or write files available to your account and use your network connection.",
20125
+ " Runs unsandboxed as your user.",
20126
+ " Can read or write your files and use your network connection.",
19369
20127
  ` Command: ${JSON.stringify(command)}`,
19370
20128
  ` Resolved cwd: ${JSON.stringify(resolvedCwd)}`,
19371
20129
  ` Timeout: ${timeoutMs.toLocaleString("en-US")} ms`,
19372
- " [o]nce runs only this command / [a]lways grants shell access for this whole session, including other shell-using tools / [n]o (this call) / n[e]ver (this session) \u203A "
20130
+ " 1. [o]nce runs only this command",
20131
+ " 2. [a]lways grants shell access for this whole session, including other shell-using tools",
20132
+ " 3. [n]o (this call)",
20133
+ " 4. n[e]ver (this session)",
20134
+ " Answer: "
19373
20135
  ].join("\n");
19374
20136
  }
19375
20137
  function indent3(value) {
@@ -19389,7 +20151,7 @@ function parsePositiveInt(value, flag) {
19389
20151
  var EXIT_INCOMPLETE_RUN = 2;
19390
20152
  var NOTIFY_SUMMARY_CHARS = 1200;
19391
20153
  function registerRunCommand(program2) {
19392
- program2.command("run").alias("chat").description("Run a one-shot coding agent task").argument("<task...>", "Task for the agent").option("-w, --workspace <path>", "Workspace path", ".").option("--provider <name>", "Provider: ultrac (default) or echo (offline test stub)").option("--model <model>", "Model name").option("--base-url <url>", "Ultrac API base URL (overrides ULTRAC_BASE_URL and the stored login)").option("--mode <name>", `Agent mode: ${agentModeNames.join(", ")}`).option("--max-steps <number>", "Maximum agent steps").option("--no-native-tools", "Use the legacy JSON tool protocol").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Grant the model broad, unsandboxed shell access", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--continue <id>", "Continue from a saved session id").option("--tools <list>", "Tools to advertise: `all`, or a comma-separated list. Default: the mode's set").option("--context-tokens <number>", "Model context window in tokens (default 262144)").option("--max-tool-result-chars <number>", "Cap on one tool result in the transcript (default 6000)").option(
20154
+ program2.command("run").alias("chat").description("Run a one-shot coding agent task").argument("<task...>", "Task for the agent").option("-w, --workspace <path>", "Workspace path", ".").option("--provider <name>", "Provider: ultrac (default) or echo (offline test stub)").option("--model <model>", "Model name").option("--base-url <url>", "Ultrac API base URL (overrides ULTRAC_BASE_URL and the stored login)").option("--mode <name>", `Agent mode: ${agentModeNames.join(", ")}`).option("--max-steps <number>", "Maximum agent steps").option("--no-native-tools", "Use the legacy JSON tool protocol").option("--permissions <mode>", "Tool permission preset: readonly, workspace, or full").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Grant the model broad, unsandboxed shell access", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--continue <id>", "Continue from a saved session id").option("--tools <list>", "Tools to advertise: `all`, or a comma-separated list. Default: the mode's set").option("--context-tokens <number>", "Model context window in tokens (default 262144)").option("--max-tool-result-chars <number>", "Cap on one tool result in the transcript (default 6000)").option(
19393
20155
  "--allow-incomplete",
19394
20156
  `Exit 0 even when the run stops without finishing. By default an unfinished run (step cap, abort, error) exits ${EXIT_INCOMPLETE_RUN} so a script or a \`&&\` chain can tell a partial answer from a real one; pass this when hitting --max-steps on a long task is an acceptable outcome. The stop reason is still printed either way.`,
19395
20157
  false
@@ -19506,7 +20268,8 @@ function registerRunCommand(program2) {
19506
20268
  const abortRun = () => controller.abort();
19507
20269
  process.on("SIGINT", abortRun);
19508
20270
  process.on("SIGTERM", abortRun);
19509
- const permissions = autonomous ? { write: true, shell: true, network: true } : {
20271
+ if (options.permissions && !["readonly", "workspace", "full"].includes(options.permissions)) throw new Error("--permissions must be readonly, workspace, or full.");
20272
+ const permissions = options.permissions ? { write: options.permissions !== "readonly", shell: options.permissions === "full", network: options.permissions === "full" } : autonomous ? { write: true, shell: true, network: true } : {
19510
20273
  write: options.allowWrite || config.permissions.write,
19511
20274
  shell: options.allowShell || config.permissions.shell,
19512
20275
  network: options.allowNetwork || config.permissions.network
@@ -19624,7 +20387,7 @@ ${summary}` : ""}`).catch((error) => console.error(chalk12.yellow(`Could not sen
19624
20387
  }
19625
20388
 
19626
20389
  // src/commands/sessions.ts
19627
- import { resolve as resolve12 } from "path";
20390
+ import { resolve as resolve14 } from "path";
19628
20391
  function registerSessionsCommand(program2) {
19629
20392
  const sessions = program2.command("sessions").description("Inspect saved Ultrac session transcripts");
19630
20393
  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) => {
@@ -19641,7 +20404,7 @@ function registerSessionsCommand(program2) {
19641
20404
  );
19642
20405
  });
19643
20406
  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) => {
19644
- const workspace = resolve12(options.workspace);
20407
+ const workspace = resolve14(options.workspace);
19645
20408
  const format3 = parseExportFormat(options.format);
19646
20409
  const maxChars = parsePositiveInt2(options.maxChars);
19647
20410
  const outcome = parseOutcomeStatus(options.outcome);
@@ -19658,7 +20421,7 @@ function registerSessionsCommand(program2) {
19658
20421
  });
19659
20422
  }
19660
20423
  async function runSessionTool(tool, input, options) {
19661
- const workspace = resolve12(options.workspace);
20424
+ const workspace = resolve14(options.workspace);
19662
20425
  const result = await createDefaultToolRegistry().execute(
19663
20426
  { tool, input },
19664
20427
  { workspace, permissions: { write: false, shell: false, network: false } }
@@ -19834,11 +20597,11 @@ function collectToolArtifacts(session, toolName) {
19834
20597
  }
19835
20598
 
19836
20599
  // src/commands/skills.ts
19837
- import { resolve as resolve13 } from "path";
20600
+ import { resolve as resolve15 } from "path";
19838
20601
  function registerSkillsCommand(program2) {
19839
20602
  const skills = program2.command("skills").description("Manage Ultrac skills (SKILL.md playbooks)");
19840
20603
  skills.command("list").description("List installed skills").option("-w, --workspace <path>", "Workspace path", ".").action(async (options) => {
19841
- const installed = await listSkills(resolve13(options.workspace));
20604
+ const installed = await listSkills(resolve15(options.workspace));
19842
20605
  if (!installed.length) {
19843
20606
  console.log("No skills installed. Create one with: ultrac skills create <name> --description <text>");
19844
20607
  return;
@@ -19849,7 +20612,7 @@ function registerSkillsCommand(program2) {
19849
20612
  }
19850
20613
  });
19851
20614
  skills.command("show").description("Show a skill's full instructions").argument("<name>", "Skill name").option("-w, --workspace <path>", "Workspace path", ".").action(async (name, options) => {
19852
- const skill = await readSkill(resolve13(options.workspace), name);
20615
+ const skill = await readSkill(resolve15(options.workspace), name);
19853
20616
  if (!skill) throw new Error(`No skill named "${name}".`);
19854
20617
  console.log(`# ${skill.name} (${skill.scope})`);
19855
20618
  console.log(`Path: ${skill.path}`);
@@ -19864,7 +20627,7 @@ function registerSkillsCommand(program2) {
19864
20627
  if (options.argumentHint && !command) throw new Error("--argument-hint requires --command.");
19865
20628
  const argumentHint = options.argumentHint?.trim();
19866
20629
  if (argumentHint && (argumentHint.length > 256 || /[\r\n]/.test(argumentHint))) throw new Error("Argument hints must be one line, at most 256 characters.");
19867
- const skill = await createSkill(resolve13(options.workspace), {
20630
+ const skill = await createSkill(resolve15(options.workspace), {
19868
20631
  name,
19869
20632
  description: options.description,
19870
20633
  whenToUse: options.when,
@@ -19879,7 +20642,7 @@ function registerSkillsCommand(program2) {
19879
20642
  }
19880
20643
  );
19881
20644
  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) => {
19882
- console.log(skillsDir(parseScope2(options.scope), resolve13(options.workspace)));
20645
+ console.log(skillsDir(parseScope2(options.scope), resolve15(options.workspace)));
19883
20646
  });
19884
20647
  }
19885
20648
  function parseScope2(scope) {
@@ -20181,13 +20944,13 @@ var FollowAbortedError = class extends Error {
20181
20944
  }
20182
20945
  };
20183
20946
  function defaultSleep2(ms, signal) {
20184
- return new Promise((resolve15) => {
20185
- if (signal?.aborted) return resolve15();
20947
+ return new Promise((resolve17) => {
20948
+ if (signal?.aborted) return resolve17();
20186
20949
  const timer = setTimeout(done, ms);
20187
20950
  function done() {
20188
20951
  signal?.removeEventListener("abort", done);
20189
20952
  clearTimeout(timer);
20190
- resolve15();
20953
+ resolve17();
20191
20954
  }
20192
20955
  signal?.addEventListener("abort", done, { once: true });
20193
20956
  });
@@ -20620,8 +21383,8 @@ Leaving run ${runId} as it is.
20620
21383
  case "succeeded":
20621
21384
  finishHuman(result.text);
20622
21385
  if (result.toolCalls.length && !result.text) {
20623
- const count4 = result.toolCalls.length;
20624
- r.err(chalk13.gray(`Done with ${count4} tool call${count4 === 1 ? "" : "s"} and no text.
21386
+ const count3 = result.toolCalls.length;
21387
+ r.err(chalk13.gray(`Done with ${count3} tool call${count3 === 1 ? "" : "s"} and no text.
20625
21388
  `));
20626
21389
  }
20627
21390
  return result.outputSource === "incomplete" ? EXIT_FAILED : EXIT_DONE;
@@ -20683,11 +21446,11 @@ async function interrupted(r, runId, options, printed) {
20683
21446
  }
20684
21447
  async function resolveDeps(options, deps) {
20685
21448
  if (options.baseUrl) setBaseUrlFlag(options.baseUrl);
20686
- const resolve15 = deps.credentials ?? (async () => {
21449
+ const resolve17 = deps.credentials ?? (async () => {
20687
21450
  const credentials2 = await resolveUltracCredentials();
20688
21451
  return credentials2 ? { apiKey: credentials2.apiKey, baseUrl: credentials2.baseUrl } : null;
20689
21452
  });
20690
- const credentials = await resolve15();
21453
+ const credentials = await resolve17();
20691
21454
  if (!credentials) {
20692
21455
  throw new Error("Not signed in to Ultrac. Run `ultrac login` (or set ULTRAC_API_KEY).");
20693
21456
  }
@@ -20786,7 +21549,7 @@ ${run.output}${run.output.endsWith("\n") ? "" : "\n"}`;
20786
21549
  }
20787
21550
 
20788
21551
  // src/commands/tools.ts
20789
- import { resolve as resolve14 } from "path";
21552
+ import { resolve as resolve16 } from "path";
20790
21553
  import chalk14 from "chalk";
20791
21554
  function registerToolsCommand(program2) {
20792
21555
  const toolsCommand = program2.command("tools");
@@ -20821,7 +21584,7 @@ ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${ser
20821
21584
  const result = await createDefaultToolRegistry().execute(
20822
21585
  { tool, input },
20823
21586
  {
20824
- workspace: resolve14(options.workspace),
21587
+ workspace: resolve16(options.workspace),
20825
21588
  permissions: {
20826
21589
  write: options.allowWrite,
20827
21590
  shell: options.allowShell,