@spinalfluid/ultrac 0.1.11 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +15 -9
  2. package/dist/cli.js +810 -256
  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;
@@ -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 {
@@ -2723,17 +2736,17 @@ function computeBackoffDelay(attempt, options) {
2723
2736
  return Math.round(capped * (0.5 + random() * 0.5));
2724
2737
  }
2725
2738
  function defaultSleep(ms, cancel) {
2726
- return new Promise((resolve15) => {
2739
+ return new Promise((resolve17) => {
2727
2740
  if (cancel?.aborted) {
2728
- resolve15();
2741
+ resolve17();
2729
2742
  return;
2730
2743
  }
2731
- const timer = setTimeout(resolve15, ms);
2744
+ const timer = setTimeout(resolve17, ms);
2732
2745
  cancel?.addEventListener(
2733
2746
  "abort",
2734
2747
  () => {
2735
2748
  clearTimeout(timer);
2736
- resolve15();
2749
+ resolve17();
2737
2750
  },
2738
2751
  { once: true }
2739
2752
  );
@@ -2808,10 +2821,19 @@ var FallbackProvider = class {
2808
2821
  );
2809
2822
  }
2810
2823
  async chatWithTools(request) {
2811
- return this.run("chatWithTools", request.signal, request.model, (entry, model, signal) => {
2824
+ return this.run("chatWithTools", request.signal, request.model, (entry, model, signal, touch) => {
2812
2825
  if (entry.provider.supportsToolCalling && entry.provider.chatWithTools) {
2813
- return entry.provider.chatWithTools({ ...request, model, signal });
2826
+ return entry.provider.chatWithTools({
2827
+ ...request,
2828
+ model,
2829
+ signal,
2830
+ onProgress: () => {
2831
+ touch();
2832
+ request.onProgress?.();
2833
+ }
2834
+ });
2814
2835
  }
2836
+ if (request.messages.some((message) => "image" in message && message.image)) throw Object.assign(new Error("This fallback provider cannot inspect image attachments."), { status: 400 });
2815
2837
  return this.chatThroughToolProtocol(entry, model, request, signal);
2816
2838
  });
2817
2839
  }
@@ -2896,7 +2918,8 @@ var FallbackProvider = class {
2896
2918
  const result = await this.withDeadline(
2897
2919
  entry.name,
2898
2920
  signal,
2899
- (attemptSignal) => invoke(entry, model, attemptSignal)
2921
+ (attemptSignal, touch) => invoke(entry, model, attemptSignal, touch),
2922
+ operation === "chatWithTools"
2900
2923
  );
2901
2924
  this.active = target;
2902
2925
  trace.end({ outcome: "ok" });
@@ -3116,10 +3139,10 @@ var FallbackProvider = class {
3116
3139
  error.name = "AbortError";
3117
3140
  throw error;
3118
3141
  }
3119
- async withDeadline(provider, signal, run) {
3120
- const deadline = this.createDeadline(provider, signal);
3142
+ async withDeadline(provider, signal, run, inactivity = false) {
3143
+ const deadline = this.createDeadline(provider, signal, inactivity);
3121
3144
  try {
3122
- return await deadline.race(run(deadline.signal));
3145
+ return await deadline.race(run(deadline.signal, deadline.touch));
3123
3146
  } finally {
3124
3147
  deadline.clear();
3125
3148
  }
@@ -3129,31 +3152,55 @@ var FallbackProvider = class {
3129
3152
  * only ever rejects while the deadline is live, so clearing it can never leave
3130
3153
  * an unhandled rejection behind.
3131
3154
  */
3132
- createDeadline(provider, signal) {
3155
+ createDeadline(provider, signal, inactivity = false) {
3133
3156
  if (this.requestTimeoutMs <= 0) {
3134
- return { signal, race: (promise) => promise, clear: () => void 0 };
3157
+ return { signal, race: (promise) => promise, clear: () => void 0, touch: () => void 0 };
3135
3158
  }
3136
3159
  const controller = new AbortController();
3137
- const forwardAbort = () => controller.abort(signal?.reason);
3160
+ let cleared = false;
3161
+ let idleTimer;
3162
+ const hardTimer = new AbortController();
3163
+ let rejectExpired;
3164
+ const expired = new Promise((_, reject) => {
3165
+ rejectExpired = reject;
3166
+ });
3167
+ const forwardAbort = () => {
3168
+ const reason = signal?.reason ?? Object.assign(new Error("Request was aborted."), { name: "AbortError" });
3169
+ rejectExpired(reason);
3170
+ controller.abort(reason);
3171
+ };
3172
+ const expire = (timeoutMs) => {
3173
+ const error = new ProviderTimeoutError(timeoutMs, provider);
3174
+ rejectExpired(error);
3175
+ controller.abort(error);
3176
+ };
3177
+ const touch = () => {
3178
+ if (cleared || controller.signal.aborted) return;
3179
+ idleTimer?.abort();
3180
+ const timer = new AbortController();
3181
+ idleTimer = timer;
3182
+ void this.sleep(this.requestTimeoutMs, timer.signal).then(() => {
3183
+ if (!cleared && !timer.signal.aborted) expire(this.requestTimeoutMs);
3184
+ });
3185
+ };
3186
+ touch();
3187
+ if (inactivity) {
3188
+ void this.sleep(3e5, hardTimer.signal).then(() => {
3189
+ if (!cleared && !hardTimer.signal.aborted) expire(3e5);
3190
+ });
3191
+ }
3138
3192
  if (signal) {
3139
3193
  if (signal.aborted) forwardAbort();
3140
3194
  else signal.addEventListener("abort", forwardAbort, { once: true });
3141
3195
  }
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
3196
  return {
3152
3197
  signal: controller.signal,
3153
3198
  race: (promise) => Promise.race([promise, expired]),
3199
+ touch: inactivity ? touch : () => void 0,
3154
3200
  clear: () => {
3155
3201
  cleared = true;
3156
- cancelDeadline.abort();
3202
+ idleTimer?.abort();
3203
+ hardTimer.abort();
3157
3204
  signal?.removeEventListener("abort", forwardAbort);
3158
3205
  }
3159
3206
  };
@@ -4066,7 +4113,7 @@ function runProcess(command, args, options) {
4066
4113
  });
4067
4114
  }
4068
4115
  const { env: env2, redactedEnvKeys } = createSanitizedProcessEnv();
4069
- return new Promise((resolve15, reject) => {
4116
+ return new Promise((resolve17, reject) => {
4070
4117
  const child = spawn(command, args, {
4071
4118
  cwd: options.cwd,
4072
4119
  stdio: ["pipe", "pipe", "pipe"],
@@ -4151,7 +4198,7 @@ function runProcess(command, args, options) {
4151
4198
  child.on("error", (error) => {
4152
4199
  finish(() => {
4153
4200
  if (termination === "aborted") {
4154
- resolve15({
4201
+ resolve17({
4155
4202
  stdout: stdout6.text(),
4156
4203
  stderr: stderr2.text(),
4157
4204
  started,
@@ -4173,7 +4220,7 @@ function runProcess(command, args, options) {
4173
4220
  const stdoutTruncation = stdout6.truncation();
4174
4221
  const stderrTruncation = stderr2.truncation();
4175
4222
  finish(
4176
- () => resolve15({
4223
+ () => resolve17({
4177
4224
  stdout: stdout6.text(),
4178
4225
  stderr: stderr2.text(),
4179
4226
  started,
@@ -5185,7 +5232,7 @@ function topCounts(values) {
5185
5232
  for (const value of values) {
5186
5233
  counts.set(value, (counts.get(value) ?? 0) + 1);
5187
5234
  }
5188
- return [...counts.entries()].map(([name, count4]) => ({ name, count: count4 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
5235
+ return [...counts.entries()].map(([name, count3]) => ({ name, count: count3 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
5189
5236
  }
5190
5237
  async function collectGitContext(workspace, signal) {
5191
5238
  const statusResult = await runGit(["status", "--short", "--branch"], { cwd: workspace, signal });
@@ -5377,7 +5424,7 @@ var diagnosticsTool = {
5377
5424
  const byFile = summarizeByFile(matched);
5378
5425
  const records = matched.slice(0, input.maxRecords);
5379
5426
  const checkerExit = runs[0].exitCode;
5380
- const clean = matched.length === 0;
5427
+ const clean2 = matched.length === 0;
5381
5428
  const unparsed = matched.length === 0 && runs.some((run) => run.exitCode !== 0);
5382
5429
  const rawTail = unparsed ? tail(runs.find((run) => run.exitCode !== 0)?.output ?? "", 2e3) : void 0;
5383
5430
  const lines = [
@@ -5394,9 +5441,9 @@ var diagnosticsTool = {
5394
5441
  ...byFile.length ? byFile.slice(0, input.maxFiles).map((entry) => `- ${entry.file}: ${entry.errors} error(s), ${entry.warnings} warning(s)`) : ["- none"],
5395
5442
  byFile.length > input.maxFiles ? `- ...${byFile.length - input.maxFiles} more file(s)` : void 0,
5396
5443
  "",
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
5444
+ clean2 ? unparsed ? "No diagnostics parsed, but the checker exited non-zero. Raw output tail:" : "No diagnostics." : `Diagnostics (showing ${records.length} of ${matched.length}):`,
5445
+ ...clean2 ? rawTail ? [rawTail] : [] : records.map(renderDiagnostic),
5446
+ !clean2 && matched.length > records.length ? `...${matched.length - records.length} more suppressed` : void 0
5400
5447
  ].filter((line) => line !== void 0);
5401
5448
  return {
5402
5449
  ok: !runs.some((run) => run.aborted) && (matched.length > 0 || runs.every((run) => run.exitCode === 0)),
@@ -5676,8 +5723,8 @@ function normalizeFilters(workspace, paths) {
5676
5723
  }
5677
5724
  function normalizeDiagnosticPath(workspace, diagnostic) {
5678
5725
  try {
5679
- const relative2 = toWorkspaceRelative(workspace, resolveInsideWorkspace(workspace, diagnostic.file));
5680
- return { ...diagnostic, file: relative2.replaceAll("\\", "/") };
5726
+ const relative3 = toWorkspaceRelative(workspace, resolveInsideWorkspace(workspace, diagnostic.file));
5727
+ return { ...diagnostic, file: relative3.replaceAll("\\", "/") };
5681
5728
  } catch {
5682
5729
  return { ...diagnostic, file: diagnostic.file.replaceAll("\\", "/") };
5683
5730
  }
@@ -6090,6 +6137,21 @@ var listFilesTool = {
6090
6137
  }
6091
6138
  };
6092
6139
  var RANGE_MARKER_RESERVE_CHARS = 300;
6140
+ async function resolveReadableFile(workspace, path) {
6141
+ try {
6142
+ const target = await resolveReadablePath(workspace, path);
6143
+ const fileStat = await stat4(target);
6144
+ if (!fileStat.isFile()) throw new Error(`Not a file: ${path}`);
6145
+ return { target, fileStat };
6146
+ } catch (error) {
6147
+ const code = error.code;
6148
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
6149
+ throw new Error(
6150
+ `File not found: ${JSON.stringify(path)}. Use list_files to discover the exact workspace path or search_files to locate relevant code, then retry with a confirmed path. No alternate file was read.`,
6151
+ { cause: error }
6152
+ );
6153
+ }
6154
+ }
6093
6155
  async function readFilePrefix(target, maxBytes) {
6094
6156
  const handle = await open3(target, "r");
6095
6157
  try {
@@ -6148,11 +6210,7 @@ var readFileTool = {
6148
6210
  description: "Read a UTF-8 text file from inside the workspace. Reads a line range (startLine, maxLines) when the whole file would not fit the tool-result cap, and says where to continue.",
6149
6211
  inputSchema: ReadFileInput,
6150
6212
  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
- }
6213
+ const { target } = await resolveReadableFile(context.workspace, input.path);
6156
6214
  const { buffer: clipped, totalBytes, stable } = await readFilePrefix(target, input.maxBytes);
6157
6215
  const text = clipped.toString("utf8");
6158
6216
  const clippedByBytes = totalBytes > clipped.byteLength;
@@ -6248,11 +6306,7 @@ var readManyFilesTool = {
6248
6306
  const sections = [];
6249
6307
  let remainingBytes = input.maxTotalBytes;
6250
6308
  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
- }
6309
+ const { target, fileStat } = await resolveReadableFile(context.workspace, path);
6256
6310
  const readBudget = Math.min(input.maxBytesPerFile, remainingBytes);
6257
6311
  if (readBudget <= 0) {
6258
6312
  files.push({ path, bytesRead: 0, totalBytes: fileStat.size, truncated: true });
@@ -6865,12 +6919,12 @@ var builtinSecretRules = [
6865
6919
  ];
6866
6920
  async function resolveArtifact(workspace, requestedPath) {
6867
6921
  const absolute = await resolveInsideWorkspaceReal(workspace, requestedPath);
6868
- const relative2 = toWorkspaceRelative(workspace, absolute);
6922
+ const relative3 = toWorkspaceRelative(workspace, absolute);
6869
6923
  const info = await stat5(absolute);
6870
6924
  if (!info.isFile()) {
6871
6925
  throw new Error(`Not a file: ${requestedPath}`);
6872
6926
  }
6873
- return { absolute, relative: relative2 };
6927
+ return { absolute, relative: relative3 };
6874
6928
  }
6875
6929
  function formatMode(mode) {
6876
6930
  return (mode & 511).toString(8).padStart(3, "0");
@@ -6970,8 +7024,8 @@ function shannonEntropy(value) {
6970
7024
  counts.set(char, (counts.get(char) ?? 0) + 1);
6971
7025
  }
6972
7026
  let entropy = 0;
6973
- for (const count4 of counts.values()) {
6974
- const probability = count4 / value.length;
7027
+ for (const count3 of counts.values()) {
7028
+ const probability = count3 / value.length;
6975
7029
  entropy -= probability * Math.log2(probability);
6976
7030
  }
6977
7031
  return entropy;
@@ -8796,7 +8850,7 @@ function summarizeDirectories(files) {
8796
8850
  const directory = dirname9(file.path).split(/[\\/]/)[0] || ".";
8797
8851
  counts.set(directory, (counts.get(directory) ?? 0) + 1);
8798
8852
  }
8799
- return [...counts.entries()].map(([name, count4]) => ({ name, count: count4 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
8853
+ return [...counts.entries()].map(([name, count3]) => ({ name, count: count3 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
8800
8854
  }
8801
8855
  function summarizeExtensions(files) {
8802
8856
  const summaries = /* @__PURE__ */ new Map();
@@ -9349,18 +9403,18 @@ function isConcurrencySafe(tool) {
9349
9403
  }
9350
9404
  function planToolBatches(calls, tools) {
9351
9405
  const batches = [];
9352
- let open9;
9406
+ let open11;
9353
9407
  for (const call of calls) {
9354
9408
  if (!isConcurrencySafe(tools.get(call.tool))) {
9355
9409
  batches.push([call]);
9356
- open9 = void 0;
9410
+ open11 = void 0;
9357
9411
  continue;
9358
9412
  }
9359
- if (!open9) {
9360
- open9 = [];
9361
- batches.push(open9);
9413
+ if (!open11) {
9414
+ open11 = [];
9415
+ batches.push(open11);
9362
9416
  }
9363
- open9.push(call);
9417
+ open11.push(call);
9364
9418
  }
9365
9419
  return batches;
9366
9420
  }
@@ -9855,7 +9909,7 @@ async function acquireLock(directory, waitMs) {
9855
9909
  };
9856
9910
  } catch (error) {
9857
9911
  if (error.code !== "EEXIST" || Date.now() >= deadline) throw error;
9858
- await new Promise((resolve15) => setTimeout(resolve15, 10));
9912
+ await new Promise((resolve17) => setTimeout(resolve17, 10));
9859
9913
  }
9860
9914
  }
9861
9915
  }
@@ -9903,6 +9957,8 @@ function readSessionContextSnapshot(session) {
9903
9957
  const snapshot = session.contextSnapshot;
9904
9958
  if (!snapshot || typeof snapshot !== "object" || snapshot.version !== 1 || !["native", "text", "run"].includes(snapshot.kind) || !Number.isSafeInteger(snapshot.completedSteps) || snapshot.completedSteps < 0 || !Array.isArray(snapshot.messages) || snapshot.messages.some((message) => {
9905
9959
  if (!message || typeof message !== "object" || !["user", "assistant", "system", "tool"].includes(message.role) || typeof message.content !== "string") return true;
9960
+ const image = message.image;
9961
+ if (image !== void 0 && (message.role !== "user" || !image || typeof image.url !== "string" || image.url.length > 7 * 1024 * 1024 || !/^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]+={0,2}$/.test(image.url))) return true;
9906
9962
  const native = message;
9907
9963
  return snapshot.kind !== "native" && (message.role === "system" || native.toolCalls !== void 0 || native.toolCallId !== void 0) || native.toolCalls !== void 0 && message.role !== "assistant" || native.toolCallId !== void 0 && message.role !== "tool" || snapshot.kind === "native" && message.role === "tool" && typeof native.toolCallId !== "string" || native.toolCallId !== void 0 && typeof native.toolCallId !== "string" || native.toolCalls !== void 0 && (!Array.isArray(native.toolCalls) || native.toolCalls.some((call) => !call || typeof call.id !== "string" || typeof call.name !== "string" || typeof call.arguments !== "string"));
9908
9964
  }) || !completeContextUnits(snapshot.messages) || (readSessionUserConstraints(session) ?? [{ content: session.task }]).some((constraint) => !snapshot.messages.some((message) => message.role === "user" && message.content === constraint.content))) throw new Error(`Session ${session.id} has an invalid saved context snapshot and cannot be continued safely.`);
@@ -11144,10 +11200,10 @@ async function withHardTimeout(promise, ms, onTimeout) {
11144
11200
  (error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) })
11145
11201
  );
11146
11202
  let timer;
11147
- const expiry = new Promise((resolve15) => {
11203
+ const expiry = new Promise((resolve17) => {
11148
11204
  timer = setTimeout(() => {
11149
11205
  onTimeout?.();
11150
- resolve15(void 0);
11206
+ resolve17(void 0);
11151
11207
  }, ms);
11152
11208
  });
11153
11209
  try {
@@ -12304,8 +12360,8 @@ function findFileInText(text, workspace) {
12304
12360
  for (const match of text.matchAll(stackFrame)) {
12305
12361
  const candidate = match[1];
12306
12362
  if (candidate.includes("node_modules") || candidate.includes("/internal/")) continue;
12307
- const relative2 = relativizeFile(candidate, workspace);
12308
- if (relative2 !== "unknown") return relative2;
12363
+ const relative3 = relativizeFile(candidate, workspace);
12364
+ if (relative3 !== "unknown") return relative3;
12309
12365
  }
12310
12366
  return null;
12311
12367
  }
@@ -12793,12 +12849,12 @@ async function sendRequest(url, input, approved, signal) {
12793
12849
  // The socket goes to a pinned address; TLS still verifies the name.
12794
12850
  servername: secure && !isIP(hostname) ? hostname : void 0
12795
12851
  };
12796
- return await new Promise((resolve15, reject) => {
12852
+ return await new Promise((resolve17, reject) => {
12797
12853
  const request = secure ? httpsRequest(options) : httpRequest(options);
12798
12854
  request.on("response", (response) => {
12799
12855
  response.on("error", () => {
12800
12856
  });
12801
- resolve15({
12857
+ resolve17({
12802
12858
  status: response.statusCode ?? 0,
12803
12859
  statusText: response.statusMessage ?? "",
12804
12860
  ok: (response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) < 300,
@@ -13277,8 +13333,8 @@ var LOOPBACK_TIMEOUT_MS = 10 * 60 * 1e3;
13277
13333
  async function startLoopbackServer(options) {
13278
13334
  const timeoutMs = options.timeoutMs ?? LOOPBACK_TIMEOUT_MS;
13279
13335
  let settle;
13280
- const wait = new Promise((resolve15) => {
13281
- settle = resolve15;
13336
+ const wait = new Promise((resolve17) => {
13337
+ settle = resolve17;
13282
13338
  });
13283
13339
  const server = http.createServer((request, response) => {
13284
13340
  const host = request.headers.host ?? "127.0.0.1";
@@ -13325,10 +13381,10 @@ async function startLoopbackServer(options) {
13325
13381
  settle?.({ ok: false, error: "server_error", description: error.message });
13326
13382
  settle = void 0;
13327
13383
  });
13328
- const port = await new Promise((resolve15, reject) => {
13384
+ const port = await new Promise((resolve17, reject) => {
13329
13385
  server.listen(0, "127.0.0.1", () => {
13330
13386
  const address = server.address();
13331
- if (address && typeof address === "object") resolve15(address.port);
13387
+ if (address && typeof address === "object") resolve17(address.port);
13332
13388
  else reject(new Error("loopback server did not bind a port"));
13333
13389
  });
13334
13390
  server.once("error", reject);
@@ -13347,9 +13403,9 @@ async function startLoopbackServer(options) {
13347
13403
  port,
13348
13404
  redirectUri: `http://127.0.0.1:${port}${LOOPBACK_PATH}`,
13349
13405
  wait,
13350
- close: () => new Promise((resolve15) => {
13406
+ close: () => new Promise((resolve17) => {
13351
13407
  clearTimeout(timer);
13352
- server.close(() => resolve15());
13408
+ server.close(() => resolve17());
13353
13409
  })
13354
13410
  };
13355
13411
  }
@@ -13463,6 +13519,22 @@ function errorMessage(error) {
13463
13519
  }
13464
13520
  return String(error);
13465
13521
  }
13522
+ async function readAccountPlan(credentials, fetchImpl = (input, init) => fetch(input, init)) {
13523
+ if (!credentials) return void 0;
13524
+ try {
13525
+ const response = await fetchImpl(modelsUrl(credentials.baseUrl), {
13526
+ headers: { authorization: `Bearer ${credentials.apiKey}`, accept: "application/json" },
13527
+ redirect: "error",
13528
+ signal: AbortSignal.timeout(2e3)
13529
+ });
13530
+ if (!response.ok) return void 0;
13531
+ const body = await response.json();
13532
+ const plans = { free: "Free", pro: "Pro", team: "Team", ultra: "Ultra" };
13533
+ return typeof body.account?.plan === "string" && Object.hasOwn(plans, body.account.plan) ? plans[body.account.plan] : void 0;
13534
+ } catch {
13535
+ return void 0;
13536
+ }
13537
+ }
13466
13538
 
13467
13539
  // src/auth/browser-login.ts
13468
13540
  var BROWSER_LOGIN_REQUEST_TIMEOUT_MS = 15e3;
@@ -13660,7 +13732,7 @@ function silenceTimeoutMs() {
13660
13732
  return Number.isFinite(override) && override > 0 ? override : DEFAULT_NON_TTY_SILENCE_TIMEOUT_MS;
13661
13733
  }
13662
13734
  function ask(rl, question, onSettle) {
13663
- return new Promise((resolve15) => {
13735
+ return new Promise((resolve17) => {
13664
13736
  let settled = false;
13665
13737
  let silenceTimer;
13666
13738
  const keepAwake = () => {
@@ -13674,7 +13746,7 @@ function ask(rl, question, onSettle) {
13674
13746
  stdin2.off("data", keepAwake);
13675
13747
  rl.close();
13676
13748
  onSettle?.();
13677
- resolve15(value.trim());
13749
+ resolve17(value.trim());
13678
13750
  };
13679
13751
  if (!stdin2.isTTY) {
13680
13752
  silenceTimer = setTimeout(() => finish(""), silenceTimeoutMs());
@@ -14026,9 +14098,9 @@ var JsonRpcEndpoint = class {
14026
14098
  if (signal?.aborted) {
14027
14099
  return Promise.reject(new JsonRpcRequestAbortedError(method, void 0, false));
14028
14100
  }
14029
- return new Promise((resolve15, reject) => {
14101
+ return new Promise((resolve17, reject) => {
14030
14102
  const limit = timeoutMs ?? this.defaultTimeoutMs;
14031
- const entry = { method, resolve: resolve15, reject, signal, dispatched: false };
14103
+ const entry = { method, resolve: resolve17, reject, signal, dispatched: false };
14032
14104
  if (signal) {
14033
14105
  entry.abortHandler = () => {
14034
14106
  this.settle(id, () => reject(new JsonRpcRequestAbortedError(method, id, entry.dispatched)));
@@ -14711,13 +14783,13 @@ function renderMcpStatuses(statuses) {
14711
14783
  }).join("\n");
14712
14784
  }
14713
14785
  function withTimeout(promise, ms, message) {
14714
- return new Promise((resolve15, reject) => {
14786
+ return new Promise((resolve17, reject) => {
14715
14787
  const timer = setTimeout(() => reject(new Error(`${message} within ${ms}ms.`)), ms);
14716
14788
  timer.unref?.();
14717
14789
  promise.then(
14718
14790
  (value) => {
14719
14791
  clearTimeout(timer);
14720
- resolve15(value);
14792
+ resolve17(value);
14721
14793
  },
14722
14794
  (error) => {
14723
14795
  clearTimeout(timer);
@@ -15116,14 +15188,14 @@ function redact(secret) {
15116
15188
  // src/notify/channel.ts
15117
15189
  function sleep(ms, signal) {
15118
15190
  if (signal?.aborted) return Promise.resolve(false);
15119
- return new Promise((resolve15) => {
15191
+ return new Promise((resolve17) => {
15120
15192
  const timer = setTimeout(() => {
15121
15193
  signal?.removeEventListener("abort", onAbort);
15122
- resolve15(true);
15194
+ resolve17(true);
15123
15195
  }, ms);
15124
15196
  const onAbort = () => {
15125
15197
  clearTimeout(timer);
15126
- resolve15(false);
15198
+ resolve17(false);
15127
15199
  };
15128
15200
  signal?.addEventListener("abort", onAbort, { once: true });
15129
15201
  });
@@ -15371,7 +15443,7 @@ ${question}${optionLines}`, signal);
15371
15443
  await Promise.allSettled(this.channels.map((channel) => channel.call?.(spoken, wait.signal)));
15372
15444
  })();
15373
15445
  const timeout = sleep(this.timing.timeoutMs, wait.signal).then(() => null);
15374
- const cancelled = new Promise((resolve15) => wait.signal.addEventListener("abort", () => resolve15(null), { once: true }));
15446
+ const cancelled = new Promise((resolve17) => wait.signal.addEventListener("abort", () => resolve17(null), { once: true }));
15375
15447
  const reply = await Promise.race([...replies, timeout, cancelled]);
15376
15448
  return reply === null ? null : pickOption(reply, options);
15377
15449
  } finally {
@@ -15682,6 +15754,80 @@ function registerTrustCommand(program2) {
15682
15754
  });
15683
15755
  }
15684
15756
 
15757
+ // src/core/image-attachments.ts
15758
+ import { constants as constants6 } from "fs";
15759
+ import { open as open9 } from "fs/promises";
15760
+ import { homedir as homedir2 } from "os";
15761
+ import { isAbsolute as isAbsolute4 } from "path";
15762
+ import { fileURLToPath as fileURLToPath2 } from "url";
15763
+ import sharp from "sharp";
15764
+ var MAX_SOURCE_BYTES = 20 * 1024 * 1024;
15765
+ function imageDrops(text) {
15766
+ const drops = [];
15767
+ const tokens = /"(?:\\.|[^"\\])*"|'[^']*'|(?:\\.|[^\s])+/g;
15768
+ for (const match of text.matchAll(tokens)) {
15769
+ const token = match[0];
15770
+ let path = token.startsWith("'") ? token.slice(1, -1) : token.startsWith('"') ? token.slice(1, -1).replace(/\\([\\"$`])/g, "$1") : token.replace(/\\(.)/g, "$1");
15771
+ if (path.startsWith("file://")) {
15772
+ try {
15773
+ path = fileURLToPath2(path);
15774
+ } catch {
15775
+ continue;
15776
+ }
15777
+ }
15778
+ if (path.startsWith("~/")) path = `${homedir2()}/${path.slice(2)}`;
15779
+ if (!isAbsolute4(path) || !/\.(png|jpe?g|webp)$/i.test(path)) continue;
15780
+ drops.push({ start: match.index, end: match.index + token.length, path, label: `[Image ${drops.length + 1}]` });
15781
+ }
15782
+ return drops;
15783
+ }
15784
+ function formatImageInput(text, cursor = text.length) {
15785
+ let display = "";
15786
+ let previous = 0;
15787
+ let shift = 0;
15788
+ let adjusted = cursor;
15789
+ for (const drop of imageDrops(text)) {
15790
+ display += text.slice(previous, drop.start) + drop.label;
15791
+ if (cursor >= drop.end) adjusted += drop.label.length - (drop.end - drop.start);
15792
+ else if (cursor > drop.start) adjusted = drop.start + shift + Math.min(drop.label.length, cursor - drop.start);
15793
+ shift += drop.label.length - (drop.end - drop.start);
15794
+ previous = drop.end;
15795
+ }
15796
+ return { text: display + text.slice(previous), cursor: Math.max(0, adjusted) };
15797
+ }
15798
+ function hasDroppedImage(text) {
15799
+ return imageDrops(text).length > 0;
15800
+ }
15801
+ async function prepareImageInput(text) {
15802
+ const drops = imageDrops(text);
15803
+ if (!drops.length) return { text };
15804
+ if (drops.length > 1) throw new Error("Attach one image per message. Cosmic currently accepts one image in its active context.");
15805
+ const drop = drops[0];
15806
+ const file = await open9(drop.path, constants6.O_RDONLY | constants6.O_NOFOLLOW | constants6.O_NONBLOCK);
15807
+ let bytes;
15808
+ try {
15809
+ const stat11 = await file.stat();
15810
+ if (!stat11.isFile() || stat11.size > MAX_SOURCE_BYTES) throw new Error("Image must be a regular file no larger than 20 MiB.");
15811
+ const buffer = Buffer.alloc(Math.min(stat11.size, MAX_SOURCE_BYTES) + 1);
15812
+ let used = 0;
15813
+ while (used < buffer.length) {
15814
+ const read = await file.read(buffer, used, buffer.length - used, null);
15815
+ if (!read.bytesRead) break;
15816
+ used += read.bytesRead;
15817
+ }
15818
+ if (used > MAX_SOURCE_BYTES) throw new Error("Image exceeds 20 MiB.");
15819
+ bytes = buffer.subarray(0, used);
15820
+ } finally {
15821
+ await file.close();
15822
+ }
15823
+ const image = sharp(bytes, { limitInputPixels: 4e7, animated: false });
15824
+ const metadata = await image.metadata();
15825
+ if (!["png", "jpeg", "webp"].includes(metadata.format ?? "") || (metadata.pages ?? 1) > 1) throw new Error("Attach a single PNG, JPEG or WebP image.");
15826
+ const normalized = await image.rotate().resize({ width: 1024, height: 1024, fit: "inside", withoutEnlargement: true }).png().toBuffer();
15827
+ if (normalized.length > 5 * 1024 * 1024) throw new Error("Normalized image exceeds the upload limit.");
15828
+ return { text: formatImageInput(text).text, image: { url: `data:image/png;base64,${normalized.toString("base64")}` } };
15829
+ }
15830
+
15685
15831
  // src/core/manual-compaction.ts
15686
15832
  async function compactManually(messages, pinned, provider, model, focus = "", signal) {
15687
15833
  const retained = new Set(pinned);
@@ -15959,7 +16105,8 @@ Acknowledge that you have the context and wait for the user's first request.`
15959
16105
  async ask(input, handlers = {}) {
15960
16106
  if (!this.started) await this.start();
15961
16107
  if (!this.firstTask) this.firstTask = input;
15962
- const asked = { role: "user", content: input };
16108
+ if (handlers.image) for (const message of this.messages) delete message.image;
16109
+ const asked = { role: "user", content: input, ...handlers.image ? { image: handlers.image } : {} };
15963
16110
  this.messages.push(asked);
15964
16111
  this.pinnedMessages.push(asked);
15965
16112
  this.userConstraints.push({ kind: "message", content: input });
@@ -16275,7 +16422,7 @@ Acknowledge that you have the context and wait for the user's first request.`
16275
16422
  constraintVersion: 1,
16276
16423
  userConstraints: [...this.userConstraints],
16277
16424
  mcpResultArchiveVersion: 1,
16278
- ...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "text", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
16425
+ ...this.messages.some((message) => isCompactionSummary(message) || message.image) ? { contextSnapshot: { version: 1, kind: "text", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
16279
16426
  // How the last turn ended, so a saved interactive transcript says whether
16280
16427
  // its final message is an answer or the place it ran out of room - the
16281
16428
  // same question `ultrac run` already answers for a one-shot run.
@@ -16490,7 +16637,10 @@ ${context}`
16490
16637
  async ask(input, handlers = {}) {
16491
16638
  if (!this.started) await this.start();
16492
16639
  if (!this.firstTask) this.firstTask = input;
16493
- const asked = { role: "user", content: input };
16640
+ if (handlers.image) {
16641
+ for (const message of this.messages) if (message.role === "user" || message.role === "system") delete message.image;
16642
+ }
16643
+ const asked = { role: "user", content: input, ...handlers.image ? { image: handlers.image } : {} };
16494
16644
  this.messages.push(asked);
16495
16645
  this.pinnedMessages.push(asked);
16496
16646
  this.userConstraints.push({ kind: "message", content: input });
@@ -16840,7 +16990,7 @@ ${guidance.join("\n")}` : ""}`;
16840
16990
  constraintVersion: 1,
16841
16991
  userConstraints: [...this.userConstraints],
16842
16992
  mcpResultArchiveVersion: 1,
16843
- ...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "native", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
16993
+ ...this.messages.some((message) => isCompactionSummary(message) || (message.role === "user" || message.role === "system") && message.image) ? { contextSnapshot: { version: 1, kind: "native", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
16844
16994
  // How the last turn ended, so a saved interactive transcript says whether
16845
16995
  // its final message is an answer or the place it ran out of room - the
16846
16996
  // same question `ultrac run` already answers for a one-shot run.
@@ -16882,10 +17032,10 @@ function boundedInteger(value, name, min, max) {
16882
17032
  }
16883
17033
  function abortableSleep(milliseconds, signal) {
16884
17034
  signal.throwIfAborted();
16885
- return new Promise((resolve15, reject) => {
17035
+ return new Promise((resolve17, reject) => {
16886
17036
  const timer = setTimeout(() => {
16887
17037
  signal.removeEventListener("abort", abort);
16888
- resolve15();
17038
+ resolve17();
16889
17039
  }, milliseconds);
16890
17040
  const abort = () => {
16891
17041
  clearTimeout(timer);
@@ -17271,53 +17421,8 @@ function attachProgressUi(options = {}) {
17271
17421
  };
17272
17422
  }
17273
17423
 
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
- };
17424
+ // src/ui/activity.ts
17425
+ import { stripVTControlCharacters } from "util";
17321
17426
 
17322
17427
  // src/ui/tool-activity.ts
17323
17428
  var VERBS = {
@@ -17390,14 +17495,129 @@ function humanize(value) {
17390
17495
  return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Tool";
17391
17496
  }
17392
17497
 
17498
+ // src/ui/spinner.ts
17499
+ import { stdout as stdout4 } from "process";
17500
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
17501
+ var Spinner = class {
17502
+ constructor(label = "thinking") {
17503
+ this.label = label;
17504
+ }
17505
+ label;
17506
+ timer;
17507
+ index = 0;
17508
+ holdWhile;
17509
+ held = false;
17510
+ startedAt;
17511
+ start(holdWhile, startedAt = performance.now()) {
17512
+ this.holdWhile = holdWhile;
17513
+ if (this.startedAt !== void 0) return;
17514
+ this.startedAt = startedAt;
17515
+ this.held = false;
17516
+ if (!stdout4.isTTY) return;
17517
+ this.render();
17518
+ this.timer = setInterval(() => this.render(), 90);
17519
+ }
17520
+ update(label) {
17521
+ this.label = label;
17522
+ }
17523
+ stop(completion) {
17524
+ const elapsed = this.startedAt === void 0 ? void 0 : performance.now() - this.startedAt;
17525
+ this.startedAt = void 0;
17526
+ if (this.timer) {
17527
+ clearInterval(this.timer);
17528
+ this.timer = void 0;
17529
+ }
17530
+ if (stdout4.isTTY && elapsed !== void 0) {
17531
+ stdout4.clearLine(0);
17532
+ stdout4.cursorTo(0);
17533
+ if (completion && elapsed >= 1e3) stdout4.write(` ${completion} ${formatSeconds(elapsed)}.
17534
+ `);
17535
+ }
17536
+ }
17537
+ render() {
17538
+ if (this.holdWhile?.()) {
17539
+ if (!this.held) {
17540
+ stdout4.clearLine(0);
17541
+ stdout4.cursorTo(0);
17542
+ this.held = true;
17543
+ }
17544
+ return;
17545
+ }
17546
+ this.held = false;
17547
+ this.index = (this.index + 1) % FRAMES.length;
17548
+ stdout4.clearLine(0);
17549
+ stdout4.cursorTo(0);
17550
+ const milliseconds = performance.now() - (this.startedAt ?? performance.now());
17551
+ const elapsed = milliseconds >= 1e3 ? formatElapsed(milliseconds) : "";
17552
+ const limit = Math.max(0, (stdout4.columns ?? 80) - elapsed.length - 10);
17553
+ let label = "";
17554
+ let cells2 = 0;
17555
+ for (const character of this.label.replace(/[\x00-\x1f\x7f]/g, " ")) {
17556
+ const size = character.codePointAt(0) > 127 ? 2 : 1;
17557
+ if (cells2 + size > limit) break;
17558
+ label += character;
17559
+ cells2 += size;
17560
+ }
17561
+ stdout4.write(` ${FRAMES[this.index]} ${label}\u2026${elapsed ? ` \xB7 ${elapsed}` : ""}`);
17562
+ }
17563
+ };
17564
+ function formatSeconds(milliseconds) {
17565
+ const seconds = Math.max(0, milliseconds) / 1e3;
17566
+ const count3 = seconds.toFixed(1).replace(/\.0$/, "");
17567
+ return `${count3} ${count3 === "1" ? "second" : "seconds"}`;
17568
+ }
17569
+
17570
+ // src/ui/activity.ts
17571
+ var sink;
17572
+ function setActivitySink(value) {
17573
+ sink = value;
17574
+ }
17575
+ function getActivitySink() {
17576
+ return sink;
17577
+ }
17578
+ var clean = (text) => stripVTControlCharacters(text).replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
17579
+ function activityEntry(call, result, durationMs) {
17580
+ const fields = ["command", "script", "path", "paths", "query", "pattern", "url", "cwd"];
17581
+ const request = fields.flatMap((key) => {
17582
+ const value = call.input[key];
17583
+ return typeof value === "string" ? [`${key}: ${value}`] : Array.isArray(value) && value.every((item) => typeof item === "string") ? [`${key}: ${value.join(", ")}`] : [];
17584
+ }).join(" \xB7 ");
17585
+ return {
17586
+ label: clean(toolActivityLabel(call)),
17587
+ request: clean(request).slice(0, 4e3),
17588
+ output: clean(result.content).slice(0, 1e3),
17589
+ ok: result.ok,
17590
+ durationMs
17591
+ };
17592
+ }
17593
+ function activityRows(snapshot, expanded, selected, end) {
17594
+ const failures = snapshot.entries.filter((entry) => !entry.ok).length;
17595
+ const elapsed = snapshot.thinkingMs >= 1e3 ? ` \xB7 Thought for ${formatSeconds(snapshot.thinkingMs)}` : "";
17596
+ const summary = `${expanded ? "\u25BE" : "\u25B8"} \u25CF ${snapshot.entries.length} actions${failures ? ` \xB7 ${failures} failed` : ""}${snapshot.finished ? elapsed : ""} \xB7 Ctrl+O details`;
17597
+ const rows = [{ text: summary, ok: failures || snapshot.failed ? false : snapshot.finished ? true : void 0 }];
17598
+ if (!expanded) return rows;
17599
+ const last = Math.min(snapshot.entries.length, end);
17600
+ for (let index = Math.max(0, last - 5); index < last; index += 1) {
17601
+ const entry = snapshot.entries[index];
17602
+ const duration = entry.durationMs >= 1e3 ? ` \xB7 Worked for ${formatSeconds(entry.durationMs)}` : "";
17603
+ rows.push({ text: ` ${selected === index ? "\u25BE" : "\u25B8"} \u25CF ${entry.label}${duration}`, ok: entry.ok, entry: index });
17604
+ if (selected === index) {
17605
+ if (entry.request) rows.push({ text: ` ${entry.request}`, entry: index });
17606
+ if (entry.output) rows.push({ text: ` ${entry.output}`, ok: entry.ok, entry: index });
17607
+ }
17608
+ }
17609
+ if (snapshot.entries.length > 5) rows.push({ text: " Scroll to browse actions \xB7 /output for full saved results" });
17610
+ return rows;
17611
+ }
17612
+
17393
17613
  // src/ui/interactive-choice.ts
17394
17614
  var MOUSE_ON = "\x1B[?1006h\x1B[?1000h";
17395
17615
  var MOUSE_OFF = "\x1B[?1000l\x1B[?1006l";
17396
17616
  function displayLabel(value, width2) {
17397
- const clean = value.replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|.)/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
17617
+ const clean2 = value.replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|.)/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
17398
17618
  let result = "";
17399
17619
  let used = 0;
17400
- for (const character of clean) {
17620
+ for (const character of clean2) {
17401
17621
  const size = /[\p{Mark}\u200d\ufe0e\ufe0f]/u.test(character) ? 0 : character.codePointAt(0) < 128 ? 1 : 2;
17402
17622
  if (used + size > width2 - 1) return `${result}\u2026`;
17403
17623
  result += character;
@@ -17410,8 +17630,8 @@ function attachInteractiveChoices(readline, options, settings = {}) {
17410
17630
  const output = settings.output ?? rl.output;
17411
17631
  const prompt = settings.prompt ?? " \u203A ";
17412
17632
  let resolveReady;
17413
- const ready = new Promise((resolve15) => {
17414
- resolveReady = resolve15;
17633
+ const ready = new Promise((resolve17) => {
17634
+ resolveReady = resolve17;
17415
17635
  });
17416
17636
  const original = rl._ttyWrite;
17417
17637
  const priorPrompt = rl.getPrompt();
@@ -17617,6 +17837,15 @@ function attachInteractiveChoices(readline, options, settings = {}) {
17617
17837
  }
17618
17838
 
17619
17839
  // src/ui/slash-completion.ts
17840
+ function builtinSlashEntries(commands, help) {
17841
+ const descriptions = /* @__PURE__ */ new Map();
17842
+ for (const line of help.split("\n")) {
17843
+ const match = /^(\/[^\n]+?)\s{2,}(.+)$/.exec(line.trim());
17844
+ if (!match) continue;
17845
+ for (const command of match[1].matchAll(/\/([a-z-]+)/g)) descriptions.set(command[1], match[2]);
17846
+ }
17847
+ return commands.map((command) => ({ text: `/${command}`, label: `/${command}`, description: descriptions.get(command) }));
17848
+ }
17620
17849
  function attachSlashCompletion(readline, loadEntries, render, initialEntries = []) {
17621
17850
  const rl = readline;
17622
17851
  const original = rl._ttyWrite;
@@ -17718,11 +17947,11 @@ function attachSlashCompletion(readline, loadEntries, render, initialEntries = [
17718
17947
 
17719
17948
  // src/ui/terminal-layout.ts
17720
17949
  import { Writable } from "stream";
17721
- import { stripVTControlCharacters } from "util";
17950
+ import { stripVTControlCharacters as stripVTControlCharacters2 } from "util";
17722
17951
  var CSI = "\x1B[";
17723
17952
  var RESET_REGION = `${CSI}r`;
17724
17953
  var MOUSE_OFF2 = `${CSI}?1000l${CSI}?1002l${CSI}?1003l${CSI}?1006l`;
17725
- var BASE_FOOTER_ROWS = 5;
17954
+ var BASE_FOOTER_ROWS = 4;
17726
17955
  var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
17727
17956
  var cells = (value) => [...segmenter.segment(value)].map(({ segment }) => {
17728
17957
  const code = segment.codePointAt(0) ?? 0;
@@ -17731,7 +17960,7 @@ var cells = (value) => [...segmenter.segment(value)].map(({ segment }) => {
17731
17960
  });
17732
17961
  var width = (text) => cells(text).reduce((sum, cell) => sum + cell.width, 0);
17733
17962
  function plain(value) {
17734
- return stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f]/g, " ");
17963
+ return stripVTControlCharacters2(value).replace(/[\x00-\x1f\x7f]/g, " ");
17735
17964
  }
17736
17965
  function fit(text, columns) {
17737
17966
  if (columns <= 0) return "";
@@ -17745,7 +17974,6 @@ function fit(text, columns) {
17745
17974
  return result;
17746
17975
  }
17747
17976
  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
17977
  var ComposerOutput = class extends Writable {
17750
17978
  constructor(layout) {
17751
17979
  super();
@@ -17781,6 +18009,7 @@ var TerminalLayout = class {
17781
18009
  details;
17782
18010
  rl;
17783
18011
  prompt = "\u203A ";
18012
+ inputFormatter;
17784
18013
  transcriptRow = 1;
17785
18014
  transcriptColumn = 1;
17786
18015
  wrapPending = false;
@@ -17788,6 +18017,11 @@ var TerminalLayout = class {
17788
18017
  lastFooterTop = 0;
17789
18018
  suggestions = [];
17790
18019
  selected = 0;
18020
+ activity;
18021
+ activityExpanded = false;
18022
+ activitySelected = -1;
18023
+ activityEnd = 0;
18024
+ restoreActivityInput;
17791
18025
  installed = false;
17792
18026
  closed = false;
17793
18027
  suspended = 0;
@@ -17825,11 +18059,15 @@ var TerminalLayout = class {
17825
18059
  get suggestionRows() {
17826
18060
  return Math.min(8, this.suggestions.length, Math.max(0, this.rows - BASE_FOOTER_ROWS - 2));
17827
18061
  }
18062
+ get activityLines() {
18063
+ if (!this.activity) return [];
18064
+ return activityRows(this.activity, this.activityExpanded, this.activitySelected, this.activityEnd).slice(0, Math.max(1, Math.min(10, this.rows - BASE_FOOTER_ROWS - this.suggestionRows - 2)));
18065
+ }
17828
18066
  get contentRows() {
17829
- return this.rows - BASE_FOOTER_ROWS - this.suggestionRows;
18067
+ return this.rows - BASE_FOOTER_ROWS - this.suggestionRows - this.activityLines.length;
17830
18068
  }
17831
18069
  get inputRow() {
17832
- return this.rows - 3;
18070
+ return this.rows - 2;
17833
18071
  }
17834
18072
  install() {
17835
18073
  if (this.installed || this.closed || !this.enabled) return this;
@@ -17851,6 +18089,93 @@ var TerminalLayout = class {
17851
18089
  attachReadline(rl) {
17852
18090
  this.rl = rl;
17853
18091
  rl.setPrompt(this.prompt);
18092
+ const reader = rl;
18093
+ const original = reader._ttyWrite;
18094
+ let sequence = "";
18095
+ let sequenceTimer;
18096
+ const wrapper = (text, key) => {
18097
+ if (this.activity && !this.suspended && key?.ctrl && key.name === "o") {
18098
+ this.toggleActivity();
18099
+ return;
18100
+ }
18101
+ const value = key?.sequence ?? text ?? "";
18102
+ if (sequence || value.startsWith("\x1B[<")) {
18103
+ sequence += value;
18104
+ clearTimeout(sequenceTimer);
18105
+ const mouse = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(sequence);
18106
+ if (mouse) {
18107
+ sequence = "";
18108
+ if (!this.suspended) this.activityMouse(Number(mouse[1]), Number(mouse[3]), mouse[4] === "M");
18109
+ return;
18110
+ }
18111
+ if (/^\x1b\[<[0-9;]*$/.test(sequence) && sequence.length < 64) {
18112
+ sequenceTimer = setTimeout(() => {
18113
+ sequence = "";
18114
+ }, 250);
18115
+ return;
18116
+ }
18117
+ const pending = sequence;
18118
+ sequence = "";
18119
+ original.call(reader, pending, key);
18120
+ return;
18121
+ }
18122
+ original.call(reader, text, key);
18123
+ };
18124
+ reader._ttyWrite = wrapper;
18125
+ this.restoreActivityInput = () => {
18126
+ clearTimeout(sequenceTimer);
18127
+ if (reader._ttyWrite === wrapper) reader._ttyWrite = original;
18128
+ };
18129
+ this.paintFooter();
18130
+ }
18131
+ setActivity(snapshot) {
18132
+ this.clearPreviousFooter();
18133
+ const previous = this.activity?.entries.length ?? 0;
18134
+ if (!snapshot.entries.length && !snapshot.finished) {
18135
+ this.activityExpanded = false;
18136
+ this.activitySelected = -1;
18137
+ }
18138
+ this.activity = snapshot;
18139
+ if (this.activityEnd >= previous || !previous) this.activityEnd = snapshot.entries.length;
18140
+ this.reflowActivity();
18141
+ }
18142
+ toggleActivity() {
18143
+ if (!this.activity) return;
18144
+ this.clearPreviousFooter();
18145
+ this.activityExpanded = !this.activityExpanded;
18146
+ this.reflowActivity();
18147
+ }
18148
+ reflowActivity() {
18149
+ if (!this.enabled || !this.installed || this.closed) return;
18150
+ this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
18151
+ this.rawWrite(`${CSI}1;${this.contentRows}r`);
18152
+ this.paintFooter();
18153
+ }
18154
+ activityMouse(button, row, press) {
18155
+ if (!this.activity || !press) return;
18156
+ if (button === 64 || button === 65) {
18157
+ if (!this.activityExpanded) return;
18158
+ this.clearPreviousFooter();
18159
+ this.activityEnd = Math.max(Math.min(5, this.activity.entries.length), Math.min(this.activity.entries.length, this.activityEnd + (button === 64 ? -1 : 1)));
18160
+ this.activitySelected = -1;
18161
+ this.reflowActivity();
18162
+ return;
18163
+ }
18164
+ if (button !== 0) return;
18165
+ const index = row - this.contentRows - 1;
18166
+ if (index === 0) {
18167
+ this.toggleActivity();
18168
+ return;
18169
+ }
18170
+ const entry = this.activityLines[index]?.entry;
18171
+ if (entry !== void 0) {
18172
+ this.clearPreviousFooter();
18173
+ this.activitySelected = this.activitySelected === entry ? -1 : entry;
18174
+ this.reflowActivity();
18175
+ }
18176
+ }
18177
+ setInputFormatter(formatter) {
18178
+ this.inputFormatter = formatter;
17854
18179
  this.paintFooter();
17855
18180
  }
17856
18181
  setPrompt(prompt) {
@@ -17910,7 +18235,7 @@ var TerminalLayout = class {
17910
18235
  for (let row = first; row <= last; row += 1) erased += `${cup(row)}${CSI}2K`;
17911
18236
  return erased + saved;
17912
18237
  });
17913
- if (this.wrapPending && !/^[\r\n]/.test(stripVTControlCharacters(formatted))) {
18238
+ if (this.wrapPending && !/^[\r\n]/.test(stripVTControlCharacters2(formatted))) {
17914
18239
  this.rawWrite(`${cup(this.transcriptRow, this.columns)}\r
17915
18240
  `);
17916
18241
  this.transcriptRow = Math.min(this.contentRows, this.transcriptRow + 1);
@@ -17987,8 +18312,10 @@ var TerminalLayout = class {
17987
18312
  composer() {
17988
18313
  const available = this.columns - 4;
17989
18314
  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)));
18315
+ const raw = this.rl?.line ?? "";
18316
+ const formatted = this.inputFormatter?.(raw, this.rl?.cursor ?? 0) ?? { text: raw, cursor: this.rl?.cursor ?? 0 };
18317
+ const input = plain(formatted.text);
18318
+ const cursor = width(plain(formatted.text.slice(0, formatted.cursor)));
17992
18319
  const capacity = Math.max(1, available - width(prompt));
17993
18320
  const offset = Math.max(0, cursor - capacity + 1);
17994
18321
  let visible = "";
@@ -18021,29 +18348,48 @@ var TerminalLayout = class {
18021
18348
  const inner = this.columns - 2;
18022
18349
  const border = "\u2500".repeat(inner);
18023
18350
  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)}`;
18351
+ const model = /^cosmic[- ]1$/i.test(this.details.model) ? "Cosmic 1" : this.details.model;
18352
+ const metadata = fit(model, this.columns);
18028
18353
  let frame = "";
18354
+ const activity = this.activityLines;
18355
+ if (activity.length && !this.suspended) frame += `${CSI}?1006h${CSI}?1000h`;
18356
+ activity.forEach((row, index) => {
18357
+ const text = fit(row.text, this.columns);
18358
+ const color = process.env.NO_COLOR || row.ok === void 0 ? "" : `${CSI}${row.ok ? 32 : 31}m`;
18359
+ const dot = text.indexOf("\u25CF");
18360
+ const rendered = dot >= 0 && color ? `${text.slice(0, dot)}${color}\u25CF${CSI}39m${text.slice(dot + 1)}` : text;
18361
+ frame += `${cup(this.contentRows + 1 + index)}${CSI}0m${CSI}2K${rendered}`;
18362
+ });
18029
18363
  const suggestions = this.suggestionRows;
18030
18364
  const first = Math.max(0, Math.min(this.selected - Math.floor(suggestions / 2), this.suggestions.length - suggestions));
18365
+ const commandWidth = Math.min(
18366
+ Math.max(18, ...this.suggestions.map((row) => width(plain(row).split(/ {2,}/)[0] ?? ""))),
18367
+ Math.max(8, Math.floor(this.columns / 3))
18368
+ );
18031
18369
  for (let i = 0; i < suggestions; i += 1) {
18032
18370
  const index = first + i;
18033
18371
  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`;
18372
+ const entry = plain(this.suggestions[index] ?? "");
18373
+ const match = /^(.+?) {2,}(.+)$/.exec(entry);
18374
+ const command = fit(match?.[1] ?? entry, commandWidth);
18375
+ const description = match?.[2] ?? "";
18376
+ const row = fit(`${selected ? "\u203A" : " "} ${command}${description ? `${" ".repeat(Math.max(0, commandWidth - width(command)) + 6)}${description}` : ""}`, this.columns);
18377
+ const style = process.env.NO_COLOR ? "" : selected ? `${CSI}1m${CSI}39m${CSI}49m` : `${CSI}22m${CSI}39m${CSI}49m`;
18378
+ frame += `${cup(this.contentRows + activity.length + 1 + i)}${CSI}0m${CSI}2K${style}${row}${" ".repeat(this.columns - width(row))}${CSI}0m`;
18035
18379
  }
18036
18380
  const inputText = fit(composer.text, this.columns - 4);
18037
18381
  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)];
18382
+ const dimBorder = (value) => process.env.NO_COLOR ? value : `${CSI}90m${value}${CSI}39m`;
18383
+ const lines = [dimBorder(`\u250C${border}\u2510`), `${dimBorder("\u2502")} ${paddedInput} ${dimBorder("\u2502")}`, dimBorder(`\u2514${border}\u2518`), metadata];
18039
18384
  lines.forEach((line, index) => {
18040
- frame += `${cup(this.rows - 4 + index)}${CSI}2K${line}`;
18385
+ frame += `${cup(this.rows - 3 + index)}${CSI}2K${line}`;
18041
18386
  });
18042
18387
  this.rawWrite(`${frame}${cup(this.inputRow, composer.column)}${CSI}?25h`);
18043
18388
  }
18044
18389
  close() {
18045
18390
  if (this.closed) return;
18046
18391
  this.closed = true;
18392
+ this.restoreActivityInput?.();
18047
18393
  if (!this.installed) return;
18048
18394
  this.output.write = this.originalWrite;
18049
18395
  if (this.errorOutput && this.errorOutput !== this.output && this.originalErrorWrite) this.errorOutput.write = this.originalErrorWrite;
@@ -18068,6 +18414,26 @@ function createTerminalLayout(options) {
18068
18414
 
18069
18415
  // src/ui/welcome.ts
18070
18416
  import chalk10, { Chalk } from "chalk";
18417
+ import { homedir as homedir3 } from "os";
18418
+ import { sep } from "path";
18419
+
18420
+ // src/ui/provider-brand.ts
18421
+ import { stripVTControlCharacters as stripVTControlCharacters3 } from "util";
18422
+ var GOLD = "\x1B[38;2;202;168;103m";
18423
+ var DEFAULT_FOREGROUND = "\x1B[39m";
18424
+ function safeDisplayText(value) {
18425
+ return stripVTControlCharacters3(value).replace(/[\x00-\x1f\x7f]/g, " ");
18426
+ }
18427
+ function providerDisplayName(provider) {
18428
+ const name = safeDisplayText(provider);
18429
+ return name.toLowerCase() === "ultrac" ? "spinalfluid" : name;
18430
+ }
18431
+ function paintProviderSpan(line, offset, provider, color) {
18432
+ if (!color || provider !== "spinalfluid" || offset < 0 || line.slice(offset, offset + provider.length) !== provider) return line;
18433
+ return `${line.slice(0, offset)}${GOLD}${provider}${DEFAULT_FOREGROUND}${line.slice(offset + provider.length)}`;
18434
+ }
18435
+
18436
+ // src/ui/welcome.ts
18071
18437
  var LOGO = [
18072
18438
  "\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557",
18073
18439
  "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
@@ -18079,8 +18445,7 @@ var LOGO = [
18079
18445
  function renderWelcome(info, options = {}) {
18080
18446
  const columns = normalizeColumns(options.columns);
18081
18447
  const paint = new Chalk({ level: options.color === false ? 0 : chalk10.level });
18082
- const cardWidth = columns;
18083
- const innerWidth = cardWidth - 4;
18448
+ const innerWidth = columns - 2;
18084
18449
  const wide = columns >= 64;
18085
18450
  const lines = [];
18086
18451
  if (wide) {
@@ -18091,16 +18456,14 @@ function renderWelcome(info, options = {}) {
18091
18456
  } else {
18092
18457
  lines.push("", ` ${paint.bold.white("ULTRAC")} ${paint.gray(`v${info.version}`)}`);
18093
18458
  }
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`));
18459
+ const provider = providerDisplayName(info.providerName);
18460
+ const prefix = `${safeDisplayText(info.model)} \xB7 `;
18461
+ const account = `${provider}${info.planName ? ` ${safeDisplayText(info.planName)}` : ""}`;
18462
+ const modelLine = ` ${truncate3(`${prefix}${account}`, innerWidth)}`;
18463
+ lines.push("", paintProviderSpan(modelLine, 2 + prefix.length, provider, paint.level > 0));
18464
+ const home = homedir3();
18465
+ const workspace = info.workspace === home ? "~" : info.workspace.startsWith(`${home}${sep}`) ? `~${info.workspace.slice(home.length)}` : info.workspace;
18466
+ lines.push(paint.gray(` ${shortenMiddle(safeDisplayText(workspace), innerWidth)}`), "");
18104
18467
  lines.push(
18105
18468
  wide ? `${paint.gray(" /help commands")} ${paint.dim("\xB7")} ${paint.gray("Esc stop")} ${paint.dim("\xB7")} ${paint.gray("Ctrl-C exit")}` : paint.gray(" /help \xB7 Esc \xB7 Ctrl-C")
18106
18469
  );
@@ -18118,10 +18481,6 @@ ${paint.gray("\u2502")}`,
18118
18481
  prompt: paint.bold.white("\u2570\u2500\u276F ")
18119
18482
  };
18120
18483
  }
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
18484
  function normalizeColumns(columns) {
18126
18485
  return Number.isFinite(columns) ? Math.max(16, Math.floor(columns)) : 80;
18127
18486
  }
@@ -18137,11 +18496,18 @@ function shortenMiddle(value, width2) {
18137
18496
  return `${value.slice(0, side)}\u2026${value.slice(value.length - (width2 - side - 1))}`;
18138
18497
  }
18139
18498
 
18499
+ // src/ui/update-prompt.ts
18500
+ import { constants as constants7 } from "fs";
18501
+ import { lstat as lstat4, mkdir as mkdir15, open as open10, rename as rename4, unlink as unlink5 } from "fs/promises";
18502
+ import { randomBytes as randomBytes5 } from "crypto";
18503
+ import { homedir as homedir4 } from "os";
18504
+ import { join as join15, parse, resolve as resolve13 } from "path";
18505
+
18140
18506
  // src/update/check.ts
18141
18507
  import { spawn as spawn4 } from "child_process";
18142
18508
  import { randomBytes as randomBytes4 } from "crypto";
18143
18509
  import { readFile as readFile13, rename as rename3, unlink as unlink4, writeFile as writeFile12 } from "fs/promises";
18144
- import { join as join14 } from "path";
18510
+ import { isAbsolute as isAbsolute5, join as join14, relative as relative2, resolve as resolve12 } from "path";
18145
18511
  var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
18146
18512
  var QUERY_TIMEOUT_MS = 5e3;
18147
18513
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -18174,17 +18540,67 @@ async function queryLatestVersion(packageName) {
18174
18540
  return void 0;
18175
18541
  }
18176
18542
  }
18177
- async function installLatestVersion(packageName) {
18543
+ async function installLatestVersion(packageName, version = "latest") {
18544
+ if (version !== "latest" && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)) {
18545
+ throw new Error("Invalid npm release version.");
18546
+ }
18178
18547
  const result = await runNpm(
18179
- ["install", "--global", `${packageName}@latest`, "--no-audit", "--no-fund"],
18548
+ ["install", "--global", `${packageName}@${version}`, "--no-audit", "--no-fund"],
18180
18549
  { timeoutMs: INSTALL_TIMEOUT_MS, capture: false }
18181
18550
  );
18182
18551
  if (result.code !== 0) {
18183
18552
  throw new Error(
18184
- `npm could not install ${packageName}@latest. Confirm npm can reach the registry and install global packages.`
18553
+ `npm could not install ${packageName}@${version}. Confirm npm can reach the registry and install global packages.`
18185
18554
  );
18186
18555
  }
18187
18556
  }
18557
+ async function installVerifiedVersion(packageName, version) {
18558
+ await installLatestVersion(packageName, version);
18559
+ const result = await runNpm(["list", "--global", packageName, "--depth=0", "--json"], {
18560
+ timeoutMs: QUERY_TIMEOUT_MS,
18561
+ capture: true,
18562
+ maxOutputBytes: 4096
18563
+ });
18564
+ let installedVersion;
18565
+ try {
18566
+ const parsed = JSON.parse(result.stdout);
18567
+ if (typeof parsed === "object" && parsed !== null && "dependencies" in parsed && typeof parsed.dependencies === "object" && parsed.dependencies !== null) {
18568
+ const dependency = parsed.dependencies[packageName];
18569
+ if (typeof dependency === "object" && dependency !== null && "version" in dependency) {
18570
+ installedVersion = dependency.version;
18571
+ }
18572
+ }
18573
+ } catch {
18574
+ }
18575
+ if (result.code !== 0 || installedVersion !== version) {
18576
+ throw new Error("The update could not be verified. Run ultrac update and restart Ultrac.");
18577
+ }
18578
+ try {
18579
+ const globalRoot = await runNpm(["root", "--global"], {
18580
+ timeoutMs: QUERY_TIMEOUT_MS,
18581
+ capture: true,
18582
+ maxOutputBytes: 4096
18583
+ });
18584
+ const root = globalRoot.stdout.trim();
18585
+ if (globalRoot.code !== 0 || !isAbsolute5(root) || root.includes("\n")) throw new Error("Invalid npm root");
18586
+ const packageRoot = join14(root, packageName);
18587
+ const manifest = JSON.parse(await readFile13(join14(packageRoot, "package.json"), "utf8"));
18588
+ if (typeof manifest !== "object" || manifest === null || !("version" in manifest) || manifest.version !== version || !("bin" in manifest)) throw new Error("Invalid package");
18589
+ const bin = manifest.bin;
18590
+ const entry = typeof bin === "string" ? bin : typeof bin === "object" && bin !== null && "ultrac" in bin ? bin.ultrac : void 0;
18591
+ if (typeof entry !== "string") throw new Error("Missing executable");
18592
+ const executable = resolve12(packageRoot, entry);
18593
+ const withinPackage = relative2(packageRoot, executable);
18594
+ if (withinPackage.startsWith("..") || isAbsolute5(withinPackage)) throw new Error("Invalid executable");
18595
+ const probe = await runExecutable(process.execPath, [executable, "--version"], {
18596
+ timeoutMs: QUERY_TIMEOUT_MS,
18597
+ capture: true
18598
+ });
18599
+ if (probe.code !== 0 || probe.stdout.trim() !== version) throw new Error("Invalid executable version");
18600
+ } catch {
18601
+ throw new Error("The installed Ultrac executable could not be verified. Restart and run ultrac --version.");
18602
+ }
18603
+ }
18188
18604
  function isNewerVersion(candidate, current) {
18189
18605
  const left = parseVersion(candidate);
18190
18606
  const right = parseVersion(current);
@@ -18237,12 +18653,15 @@ async function writeCache(path, cache) {
18237
18653
  }
18238
18654
  }
18239
18655
  async function runNpm(args, options) {
18656
+ return runExecutable(process.platform === "win32" ? "npm.cmd" : "npm", args, options);
18657
+ }
18658
+ async function runExecutable(executable, args, options) {
18240
18659
  const env2 = { ...process.env };
18241
18660
  for (const key of Object.keys(env2)) {
18242
18661
  if (key.startsWith("ULTRAC_")) delete env2[key];
18243
18662
  }
18244
- return new Promise((resolve15) => {
18245
- const child = spawn4(process.platform === "win32" ? "npm.cmd" : "npm", args, {
18663
+ return new Promise((resolve17) => {
18664
+ const child = spawn4(executable, args, {
18246
18665
  env: env2,
18247
18666
  stdio: options.capture ? ["ignore", "pipe", "ignore"] : "inherit",
18248
18667
  windowsHide: true
@@ -18251,21 +18670,90 @@ async function runNpm(args, options) {
18251
18670
  if (options.capture) {
18252
18671
  child.stdout?.setEncoding("utf8");
18253
18672
  child.stdout?.on("data", (chunk) => {
18254
- if (stdout6.length <= 256) stdout6 += chunk;
18673
+ const limit = options.maxOutputBytes ?? 256;
18674
+ if (stdout6.length <= limit) stdout6 += chunk.slice(0, limit + 1 - stdout6.length);
18255
18675
  });
18256
18676
  }
18257
18677
  const timer = setTimeout(() => child.kill(), options.timeoutMs);
18258
18678
  child.once("error", () => {
18259
18679
  clearTimeout(timer);
18260
- resolve15({ code: 1, stdout: "" });
18680
+ resolve17({ code: 1, stdout: "" });
18261
18681
  });
18262
18682
  child.once("close", (code) => {
18263
18683
  clearTimeout(timer);
18264
- resolve15({ code: code ?? 1, stdout: stdout6 });
18684
+ resolve17({ code: code ?? 1, stdout: stdout6 });
18265
18685
  });
18266
18686
  });
18267
18687
  }
18268
18688
 
18689
+ // src/ui/update-prompt.ts
18690
+ var UPDATE_CHOICES = ["Update now", "Skip", "Skip until next version"];
18691
+ async function offerUpdatePrompt(status, options) {
18692
+ const version = status.latestVersion;
18693
+ if (!options.interactive || options.signal?.aborted || !status.updateAvailable || !version || !validVersion(version) || !validVersion(status.currentVersion) || !isNewerVersion(version, status.currentVersion) || await skippedVersion() === version) return "not_offered";
18694
+ const answer = await options.ask(
18695
+ `Ultrac ${version} is available. You are running ${status.currentVersion}.`,
18696
+ UPDATE_CHOICES,
18697
+ options.signal
18698
+ );
18699
+ if (options.signal?.aborted) return "skip";
18700
+ if (answer === UPDATE_CHOICES[0]) return "update_now";
18701
+ if (answer === UPDATE_CHOICES[2]) {
18702
+ await rememberSkippedVersion(version);
18703
+ return "skip_version";
18704
+ }
18705
+ return "skip";
18706
+ }
18707
+ function validVersion(version) {
18708
+ return version.length <= 128 && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version);
18709
+ }
18710
+ async function statePath(create) {
18711
+ const home = resolve13(ultracHomeDir());
18712
+ if (home === parse(home).root || home === resolve13(homedir4())) {
18713
+ throw new Error("Ultrac update preferences require a dedicated ULTRAC_HOME directory.");
18714
+ }
18715
+ if (create) await mkdir15(home, { recursive: true, mode: 448 });
18716
+ const info = await lstat4(home);
18717
+ if (!info.isDirectory() || info.isSymbolicLink()) {
18718
+ throw new Error("Ultrac update preferences require a real home directory.");
18719
+ }
18720
+ return join15(home, "update-preferences.json");
18721
+ }
18722
+ async function skippedVersion() {
18723
+ try {
18724
+ const path = await statePath(false);
18725
+ const file = await open10(path, constants7.O_RDONLY | constants7.O_NOFOLLOW);
18726
+ try {
18727
+ const info = await file.stat();
18728
+ if (!info.isFile() || info.size > 1024) return void 0;
18729
+ const parsed = JSON.parse(await file.readFile("utf8"));
18730
+ if (typeof parsed !== "object" || parsed === null || !("skippedVersion" in parsed)) return void 0;
18731
+ return typeof parsed.skippedVersion === "string" && validVersion(parsed.skippedVersion) ? parsed.skippedVersion : void 0;
18732
+ } finally {
18733
+ await file.close();
18734
+ }
18735
+ } catch {
18736
+ return void 0;
18737
+ }
18738
+ }
18739
+ async function rememberSkippedVersion(version) {
18740
+ const path = await statePath(true);
18741
+ const temporary = `${path}.${randomBytes5(8).toString("hex")}.tmp`;
18742
+ try {
18743
+ const file = await open10(temporary, constants7.O_WRONLY | constants7.O_CREAT | constants7.O_EXCL, 384);
18744
+ try {
18745
+ await file.writeFile(`${JSON.stringify({ skippedVersion: version })}
18746
+ `, "utf8");
18747
+ await file.sync();
18748
+ } finally {
18749
+ await file.close();
18750
+ }
18751
+ await rename4(temporary, path);
18752
+ } finally {
18753
+ await unlink5(temporary).catch(() => void 0);
18754
+ }
18755
+ }
18756
+
18269
18757
  // src/commands/repl.ts
18270
18758
  var HELP = `Commands:
18271
18759
  /help Show this help
@@ -18296,7 +18784,7 @@ var HELP = `Commands:
18296
18784
  /whoami Show sign-in state
18297
18785
  /tools List available tools
18298
18786
  /logs Show the private diagnostic log path
18299
- /ghost [on|off|status] Quiet activity output; diagnostics stay enabled
18787
+ /ghost [on|off|status] Quiet activity output; diagnostics stay enabled
18300
18788
  /output [number|list] Inspect saved tool output (latest by default)
18301
18789
  /memory List and inspect stored memories
18302
18790
  /skill <name> [args] Run an installed skill
@@ -18467,6 +18955,7 @@ async function runRepl(options, initialTask = "") {
18467
18955
  bindSignals: false,
18468
18956
  details: { provider: provider.name, model: session.model, mode: session.mode, status: "Idle" }
18469
18957
  }) : void 0;
18958
+ setActivitySink(layout?.enabled ? (snapshot) => layout.setActivity(snapshot) : void 0);
18470
18959
  const refreshDetails = () => {
18471
18960
  if (!layout?.enabled) return;
18472
18961
  const steps = session.toSession().steps;
@@ -18504,7 +18993,8 @@ async function runRepl(options, initialTask = "") {
18504
18993
  workspace,
18505
18994
  useNativeTools ? "native tools" : "json protocol",
18506
18995
  describeBudget(mode, toolSelection, registry),
18507
- diagnosticPath2
18996
+ diagnosticPath2,
18997
+ provider.name.toLowerCase() === "ultrac" ? await readAccountPlan(await resolveUltracCredentials()) : void 0
18508
18998
  );
18509
18999
  if (mcpRuntime && mcpRuntime.statuses.length > 0) {
18510
19000
  console.log(chalk11.bold("\nMCP servers"));
@@ -18517,15 +19007,14 @@ async function runRepl(options, initialTask = "") {
18517
19007
  throw new Error(`Could not start the session: ${errorMessage2(error)}`, { cause: error });
18518
19008
  }
18519
19009
  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."));
19010
+ if (!stdin5.isTTY && update?.updateAvailable && update.latestVersion) {
19011
+ console.log(chalk11.yellow(`Update available: ${update.currentVersion} \u2192 ${update.latestVersion}. Run ultrac update.`));
18524
19012
  }
18525
19013
  const rl = createInterface3({ input: stdin5, output: layout?.enabled ? layout.outputProxy : stdout5 });
18526
19014
  layout?.attachReadline(rl);
19015
+ layout?.setInputFormatter(formatImageInput);
18527
19016
  refreshDetails();
18528
- const builtinEntries = BUILTIN_SLASH_COMMANDS.map((command) => ({ text: `/${command}`, label: `/${command}` }));
19017
+ const builtinEntries = builtinSlashEntries(BUILTIN_SLASH_COMMANDS, HELP);
18529
19018
  const slashCompletion = layout?.enabled ? attachSlashCompletion(rl, async () => {
18530
19019
  const [commands, skills] = await Promise.all([listSkillCommands(workspace), listSkills(workspace)]);
18531
19020
  const declared = new Set(commands.commands.map((command) => command.skillName));
@@ -18605,29 +19094,35 @@ async function runRepl(options, initialTask = "") {
18605
19094
  rl.close();
18606
19095
  };
18607
19096
  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
19097
  const withInterrupt = async (run) => {
18615
19098
  turnAbort = new AbortController();
18616
- turnStartedAt = Date.now();
18617
- layout?.updateDetails({ status: "Running 00:00" });
19099
+ layout?.updateDetails({ status: "Running" });
18618
19100
  try {
18619
19101
  return await run(turnAbort.signal);
18620
19102
  } finally {
18621
19103
  turnAbort = void 0;
18622
- turnStartedAt = void 0;
18623
19104
  refreshDetails();
18624
19105
  layout?.updateDetails({ status: "Idle" });
18625
19106
  }
18626
19107
  };
18627
19108
  try {
19109
+ if (update) {
19110
+ const decision = await withInterrupt((signal) => offerUpdatePrompt(update, {
19111
+ interactive: Boolean(stdin5.isTTY && stdout5.isTTY),
19112
+ signal,
19113
+ ask: (text, choices, askSignal) => getAskUserRuntime()(text, [...choices], askSignal)
19114
+ }));
19115
+ if (decision === "update_now") {
19116
+ layout?.close();
19117
+ rl.close();
19118
+ await installVerifiedVersion(packageInfo.name, update.latestVersion);
19119
+ console.log(`Updated to ${update.latestVersion}. Run ultrac again to use the new version.`);
19120
+ return;
19121
+ }
19122
+ }
18628
19123
  if (initialTask) {
18629
19124
  console.log(chalk11.white(`
18630
- \u203A ${initialTask}`));
19125
+ \u203A ${formatImageInput(initialTask).text}`));
18631
19126
  await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
18632
19127
  }
18633
19128
  while (running) {
@@ -18642,10 +19137,10 @@ async function runRepl(options, initialTask = "") {
18642
19137
  }
18643
19138
  const line = await reader.next();
18644
19139
  if (line === null) break;
18645
- if (layout?.enabled && line.trim()) console.log(`\u203A ${line.replace(/[\x00-\x1f\x7f]/g, " ")}`);
19140
+ if (layout?.enabled && line.trim()) console.log(`\u203A ${formatImageInput(line).text.replace(/[\x00-\x1f\x7f]/g, " ")}`);
18646
19141
  const raw = line.trim();
18647
19142
  if (!raw) continue;
18648
- if (raw.startsWith("/")) {
19143
+ if (raw.startsWith("/") && !hasDroppedImage(raw)) {
18649
19144
  const previousSession = session;
18650
19145
  const slashCommand = raw.slice(1).trim().split(/\s+/, 1)[0]?.toLowerCase();
18651
19146
  const handled = await withInterrupt(async (signal) => {
@@ -18658,7 +19153,7 @@ async function runRepl(options, initialTask = "") {
18658
19153
  resetContextBudgetNotices();
18659
19154
  console.log(chalk11.gray("Started a fresh conversation."));
18660
19155
  }, async (prompt) => {
18661
- await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds);
19156
+ await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds, false);
18662
19157
  }, signal, async (id) => {
18663
19158
  const lineage = await loadSessionLineage(workspace, id);
18664
19159
  const restored = createSession({ model: session.model, mode: session.mode, permissions: session.currentPermissions });
@@ -18681,7 +19176,7 @@ async function runRepl(options, initialTask = "") {
18681
19176
  if (handled === "unhandled") {
18682
19177
  try {
18683
19178
  await withInterrupt((signal) => runSkillCommand(session, raw, workspace, question, async (prompt) => {
18684
- await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds);
19179
+ await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds, false);
18685
19180
  }));
18686
19181
  } finally {
18687
19182
  await persistSession();
@@ -18696,11 +19191,11 @@ async function runRepl(options, initialTask = "") {
18696
19191
  await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
18697
19192
  }
18698
19193
  } finally {
18699
- clearInterval(footerTimer);
18700
19194
  slashCompletion?.cleanup();
18701
19195
  process.off("SIGTERM", terminate);
18702
19196
  if (stdin5.isTTY) stdin5.off("keypress", onKeypress);
18703
19197
  setAskUserRuntime(void 0);
19198
+ setActivitySink(void 0);
18704
19199
  rl.close();
18705
19200
  try {
18706
19201
  await saveTranscript(session, writeCheckpoint);
@@ -18711,14 +19206,33 @@ async function runRepl(options, initialTask = "") {
18711
19206
  }
18712
19207
  console.log(chalk11.gray("Goodbye."));
18713
19208
  }
18714
- async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing) {
19209
+ async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop = true) {
19210
+ const activity = isGhostEnabled(session) ? void 0 : getActivitySink();
19211
+ const entries = [];
19212
+ let thinkingMs = 0;
19213
+ let thinkingStarted;
19214
+ let turnFailed = false;
19215
+ const refreshActivity = (finished = false) => activity?.({ entries: [...entries], thinkingMs, finished, failed: turnFailed });
19216
+ refreshActivity();
18715
19217
  const spinner = new Spinner();
19218
+ const workSpinner = new Spinner("working");
19219
+ const activeTools = /* @__PURE__ */ new Map();
19220
+ let permissionPrompts = 0;
19221
+ const renderWorking = () => {
19222
+ workSpinner.stop();
19223
+ const active = [...activeTools.entries()].at(-1);
19224
+ if (!active || quiet || permissionPrompts || [...activeTools.keys()].some((call) => call.tool === "ask_user")) return;
19225
+ workSpinner.update(`working \xB7 ${toolActivityLabel(active[0])}${activeTools.size > 1 ? ` (+${activeTools.size - 1} active)` : ""}`);
19226
+ workSpinner.start(isComposing, active[1]);
19227
+ };
18716
19228
  const quiet = isGhostEnabled(session);
18717
19229
  const detachProgress = quiet ? () => {
18718
19230
  } : attachProgressUi();
18719
19231
  let deltaChars = 0;
18720
19232
  try {
18721
- await session.ask(input, {
19233
+ const prepared = allowImageDrop ? await prepareImageInput(input) : { text: input };
19234
+ await session.ask(prepared.text, {
19235
+ ...prepared.image ? { image: prepared.image } : {},
18722
19236
  ...signal ? { signal } : {},
18723
19237
  ...persist ? {
18724
19238
  onCheckpoint: async () => {
@@ -18731,13 +19245,22 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
18731
19245
  },
18732
19246
  onModelStart: () => {
18733
19247
  deltaChars = 0;
18734
- if (!quiet) spinner.start(isComposing);
19248
+ thinkingStarted = performance.now();
19249
+ if (!quiet) {
19250
+ spinner.update("thinking");
19251
+ spinner.start(isComposing);
19252
+ }
18735
19253
  },
18736
19254
  onModelDelta: (text) => {
18737
19255
  deltaChars += text.length;
18738
19256
  if (!quiet) spinner.update(`thinking (${deltaChars} chars)`);
18739
19257
  },
18740
- onModelSettled: () => spinner.stop(),
19258
+ onModelSettled: () => {
19259
+ if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
19260
+ thinkingStarted = void 0;
19261
+ spinner.stop(activity ? void 0 : signal?.aborted ? "Thinking stopped after" : "Thought for");
19262
+ refreshActivity();
19263
+ },
18741
19264
  onAssistantMessage: (message) => {
18742
19265
  if (message.trim()) {
18743
19266
  console.log(`
@@ -18745,9 +19268,24 @@ ${message.trim()}`);
18745
19268
  }
18746
19269
  },
18747
19270
  onToolStart: (call) => {
18748
- if (!quiet) console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
19271
+ activeTools.set(call, performance.now());
19272
+ renderWorking();
19273
+ if (!stdout5.isTTY && !quiet) console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
19274
+ },
19275
+ onToolResult: (call, result) => {
19276
+ workSpinner.stop();
19277
+ const started = activeTools.get(call);
19278
+ activeTools.delete(call);
19279
+ const duration = started === void 0 ? 0 : performance.now() - started;
19280
+ entries.push(activityEntry(call, result, duration));
19281
+ refreshActivity();
19282
+ if (!activity && !quiet) {
19283
+ const dot = result.ok ? chalk11.green("\u25CF") : chalk11.red("\u25CF");
19284
+ console.log(` ${dot} ${toolActivityLabel(call)}${duration >= 1e3 ? ` \xB7 Worked for ${formatSeconds(duration)}` : ""}`);
19285
+ }
19286
+ printToolResult2(result, Boolean(activity));
19287
+ renderWorking();
18749
19288
  },
18750
- onToolResult: (_call, result) => printToolResult2(result),
18751
19289
  ...drainQueuedMessages ? {
18752
19290
  drainQueuedMessages,
18753
19291
  onQueuedMessage: (message) => {
@@ -18771,42 +19309,55 @@ ${message.trim()}`);
18771
19309
  console.log(chalk11.gray(` ${hint}`));
18772
19310
  },
18773
19311
  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";
19312
+ permissionPrompts += 1;
19313
+ workSpinner.stop();
19314
+ try {
19315
+ const prompt = permission === "shell" && call.tool === "run_shell" ? shellApprovalPrompt(call, session.toSession().workspace) : permission === "shell" ? ` ${call.tool} wants shell access. [o]nce (this call only) / [a]lways (all shell tools for this session) / [n]o (this call) / n[e]ver (this session) \u203A ` : ` ${call.tool} wants ${permission} access. [o]nce / [a]lways / [n]o / n[e]ver \u203A `;
19316
+ const rawAnswer = await question(chalk11.yellow(prompt), signal);
19317
+ if (signal?.aborted || rawAnswer === null) return "deny";
19318
+ const answer = rawAnswer.trim().toLowerCase();
19319
+ if (answer === "a" || answer === "always") return "always";
19320
+ if (answer === "o" || answer === "once" || answer === "y" || answer === "yes") return "once";
19321
+ if (answer === "e" || answer === "never") {
19322
+ console.log(
19323
+ chalk11.gray(
19324
+ ` ${permission} refused for the rest of the session \u2014 nothing will ask again. /allow ${permission} undoes it.`
19325
+ )
19326
+ );
19327
+ return "never";
19328
+ }
19329
+ return "deny";
19330
+ } finally {
19331
+ permissionPrompts -= 1;
19332
+ renderWorking();
18787
19333
  }
18788
- return "deny";
18789
19334
  }
18790
19335
  });
18791
19336
  return true;
18792
19337
  } catch (error) {
19338
+ turnFailed = true;
18793
19339
  spinner.stop();
18794
19340
  printTurnError(error);
18795
19341
  return false;
18796
19342
  } finally {
19343
+ spinner.stop();
19344
+ workSpinner.stop();
19345
+ activeTools.clear();
19346
+ if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
19347
+ refreshActivity(true);
18797
19348
  detachProgress();
18798
19349
  }
18799
19350
  }
18800
- async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing, maxRounds = 1) {
19351
+ async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing, maxRounds = 1, allowImageDrop = true) {
18801
19352
  try {
18802
- if (maxRounds === 1) return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing);
19353
+ if (maxRounds === 1) return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop);
18803
19354
  let successful = true;
18804
19355
  const result = await runAutonomousLoop({
18805
19356
  maxRounds,
18806
19357
  signal,
18807
19358
  classifyFailure: () => ({ kind: "unknown", retryable: false, failoverable: false, reason: "Interactive provider retries already exhausted." }),
18808
19359
  runRound: async (round, _previous, roundSignal) => {
18809
- successful = await runTurn(session, round === 1 ? input : AUTONOMOUS_CONTINUE_TASK, question, roundSignal, drainQueuedMessages, persist, isComposing);
19360
+ successful = await runTurn(session, round === 1 ? input : AUTONOMOUS_CONTINUE_TASK, question, roundSignal, drainQueuedMessages, persist, isComposing, allowImageDrop);
18810
19361
  await persist(session);
18811
19362
  const snapshot = session.toSession();
18812
19363
  return successful ? snapshot : { ...snapshot, stopReason: roundSignal.aborted ? "aborted" : "error" };
@@ -19000,7 +19551,7 @@ ${extra}`);
19000
19551
  else {
19001
19552
  for (const field of ["inputTokens", "outputTokens", "totalTokens"]) {
19002
19553
  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"}`);
19554
+ console.log(`${field}: ${counts.length ? counts.reduce((sum, count3) => sum + count3, 0) : "not reported"}`);
19004
19555
  }
19005
19556
  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
19557
  }
@@ -19127,6 +19678,8 @@ ${extra}`);
19127
19678
  }
19128
19679
  const entry = results[selected - 1];
19129
19680
  console.log(`${selected}. ${toolActivityLabel(entry.call)} (${entry.result.ok ? "ok" : "failed"})`);
19681
+ const detail = activityEntry(entry.call, entry.result, 0);
19682
+ if (detail.request) console.log(detail.request);
19130
19683
  console.log(entry.result.content);
19131
19684
  for (const warning of getMcpArchiveWarnings(entry.result)) console.log(chalk11.yellow(`Warning: ${warning}`));
19132
19685
  return "ok";
@@ -19205,7 +19758,7 @@ async function runSkillCommand(session, raw, workspace, question, runPrompt) {
19205
19758
  chalk11.gray(` /${resolved.command.command} \u2192 skill ${resolved.command.skillName} (${resolved.command.scope})`)
19206
19759
  );
19207
19760
  if (runPrompt) await runPrompt(resolved.prompt);
19208
- else await runTurn(session, resolved.prompt, question);
19761
+ else await runTurn(session, resolved.prompt, question, void 0, void 0, void 0, void 0, false);
19209
19762
  return;
19210
19763
  }
19211
19764
  if (resolved.status === "conflict") {
@@ -19270,7 +19823,7 @@ var LineReader = class {
19270
19823
  const messages = [];
19271
19824
  while (this.queue.length) {
19272
19825
  const next = this.queue[0].trim();
19273
- if (next.startsWith("/")) break;
19826
+ if (next.startsWith("/") || hasDroppedImage(next)) break;
19274
19827
  this.queue.shift();
19275
19828
  if (next) messages.push(next);
19276
19829
  }
@@ -19283,8 +19836,8 @@ var LineReader = class {
19283
19836
  if (buffered !== void 0) return Promise.resolve(buffered);
19284
19837
  }
19285
19838
  if (this.closed) return Promise.resolve(null);
19286
- return new Promise((resolve15) => {
19287
- const waiter = { resolve: resolve15, ...signal ? { signal } : {} };
19839
+ return new Promise((resolve17) => {
19840
+ const waiter = { resolve: resolve17, ...signal ? { signal } : {} };
19288
19841
  if (signal) {
19289
19842
  waiter.onAbort = () => {
19290
19843
  const index = this.waiters.indexOf(waiter);
@@ -19317,11 +19870,12 @@ function handleReplExit(activeTurn) {
19317
19870
  activeTurn?.abort();
19318
19871
  return "exit-session";
19319
19872
  }
19320
- function printBanner(version, providerName, model, mode, workspace, protocol, budget, diagnosticPath2) {
19873
+ function printBanner(version, providerName, model, mode, workspace, protocol, budget, diagnosticPath2, planName) {
19321
19874
  console.log(renderWelcome(
19322
19875
  {
19323
19876
  version,
19324
19877
  providerName,
19878
+ planName,
19325
19879
  model,
19326
19880
  mode,
19327
19881
  workspace,
@@ -19346,9 +19900,9 @@ function describeAdvertisedTools(mode, selection, registry = createDefaultToolRe
19346
19900
  names: advertised.map((tool) => tool.name)
19347
19901
  };
19348
19902
  }
19349
- function printToolResult2(result) {
19903
+ function printToolResult2(result, collapsed = false) {
19350
19904
  const failure = toolFailurePreview(result);
19351
- if (failure) console.log(chalk11.red(` Failed: ${failure}`));
19905
+ if (failure && !collapsed) console.log(chalk11.red(` Failed: ${failure}`));
19352
19906
  for (const warning of getMcpArchiveWarnings(result)) {
19353
19907
  console.log(chalk11.yellow(indent3(`Warning: ${warning}`)));
19354
19908
  }
@@ -19624,7 +20178,7 @@ ${summary}` : ""}`).catch((error) => console.error(chalk12.yellow(`Could not sen
19624
20178
  }
19625
20179
 
19626
20180
  // src/commands/sessions.ts
19627
- import { resolve as resolve12 } from "path";
20181
+ import { resolve as resolve14 } from "path";
19628
20182
  function registerSessionsCommand(program2) {
19629
20183
  const sessions = program2.command("sessions").description("Inspect saved Ultrac session transcripts");
19630
20184
  sessions.command("list").description("List saved sessions").option("-w, --workspace <path>", "Workspace path", ".").option("--json", "Print JSON output", false).option("--max-results <number>", "Maximum sessions to show", "20").action(async (options) => {
@@ -19641,7 +20195,7 @@ function registerSessionsCommand(program2) {
19641
20195
  );
19642
20196
  });
19643
20197
  sessions.command("export").description("Export a saved session as an auditable episode package").argument("<id>", "Session id").option("-w, --workspace <path>", "Workspace path", ".").option("--format <format>", "Output format: markdown or json", "markdown").option("--max-chars <number>", "Maximum characters per model/tool text block in markdown", "4000").option("--outcome <outcome>", "Outcome status: pass, fail, blocked, or unknown", "unknown").option("--notes <text>", "Outcome notes to include in the episode package").option("--out <path>", "Write export to a path inside the workspace instead of stdout").action(async (id, options) => {
19644
- const workspace = resolve12(options.workspace);
20198
+ const workspace = resolve14(options.workspace);
19645
20199
  const format3 = parseExportFormat(options.format);
19646
20200
  const maxChars = parsePositiveInt2(options.maxChars);
19647
20201
  const outcome = parseOutcomeStatus(options.outcome);
@@ -19658,7 +20212,7 @@ function registerSessionsCommand(program2) {
19658
20212
  });
19659
20213
  }
19660
20214
  async function runSessionTool(tool, input, options) {
19661
- const workspace = resolve12(options.workspace);
20215
+ const workspace = resolve14(options.workspace);
19662
20216
  const result = await createDefaultToolRegistry().execute(
19663
20217
  { tool, input },
19664
20218
  { workspace, permissions: { write: false, shell: false, network: false } }
@@ -19834,11 +20388,11 @@ function collectToolArtifacts(session, toolName) {
19834
20388
  }
19835
20389
 
19836
20390
  // src/commands/skills.ts
19837
- import { resolve as resolve13 } from "path";
20391
+ import { resolve as resolve15 } from "path";
19838
20392
  function registerSkillsCommand(program2) {
19839
20393
  const skills = program2.command("skills").description("Manage Ultrac skills (SKILL.md playbooks)");
19840
20394
  skills.command("list").description("List installed skills").option("-w, --workspace <path>", "Workspace path", ".").action(async (options) => {
19841
- const installed = await listSkills(resolve13(options.workspace));
20395
+ const installed = await listSkills(resolve15(options.workspace));
19842
20396
  if (!installed.length) {
19843
20397
  console.log("No skills installed. Create one with: ultrac skills create <name> --description <text>");
19844
20398
  return;
@@ -19849,7 +20403,7 @@ function registerSkillsCommand(program2) {
19849
20403
  }
19850
20404
  });
19851
20405
  skills.command("show").description("Show a skill's full instructions").argument("<name>", "Skill name").option("-w, --workspace <path>", "Workspace path", ".").action(async (name, options) => {
19852
- const skill = await readSkill(resolve13(options.workspace), name);
20406
+ const skill = await readSkill(resolve15(options.workspace), name);
19853
20407
  if (!skill) throw new Error(`No skill named "${name}".`);
19854
20408
  console.log(`# ${skill.name} (${skill.scope})`);
19855
20409
  console.log(`Path: ${skill.path}`);
@@ -19864,7 +20418,7 @@ function registerSkillsCommand(program2) {
19864
20418
  if (options.argumentHint && !command) throw new Error("--argument-hint requires --command.");
19865
20419
  const argumentHint = options.argumentHint?.trim();
19866
20420
  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), {
20421
+ const skill = await createSkill(resolve15(options.workspace), {
19868
20422
  name,
19869
20423
  description: options.description,
19870
20424
  whenToUse: options.when,
@@ -19879,7 +20433,7 @@ function registerSkillsCommand(program2) {
19879
20433
  }
19880
20434
  );
19881
20435
  skills.command("path").description("Print the skills directory path for a scope").option("-w, --workspace <path>", "Workspace path", ".").option("--scope <scope>", "global or project", "project").action((options) => {
19882
- console.log(skillsDir(parseScope2(options.scope), resolve13(options.workspace)));
20436
+ console.log(skillsDir(parseScope2(options.scope), resolve15(options.workspace)));
19883
20437
  });
19884
20438
  }
19885
20439
  function parseScope2(scope) {
@@ -20181,13 +20735,13 @@ var FollowAbortedError = class extends Error {
20181
20735
  }
20182
20736
  };
20183
20737
  function defaultSleep2(ms, signal) {
20184
- return new Promise((resolve15) => {
20185
- if (signal?.aborted) return resolve15();
20738
+ return new Promise((resolve17) => {
20739
+ if (signal?.aborted) return resolve17();
20186
20740
  const timer = setTimeout(done, ms);
20187
20741
  function done() {
20188
20742
  signal?.removeEventListener("abort", done);
20189
20743
  clearTimeout(timer);
20190
- resolve15();
20744
+ resolve17();
20191
20745
  }
20192
20746
  signal?.addEventListener("abort", done, { once: true });
20193
20747
  });
@@ -20620,8 +21174,8 @@ Leaving run ${runId} as it is.
20620
21174
  case "succeeded":
20621
21175
  finishHuman(result.text);
20622
21176
  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.
21177
+ const count3 = result.toolCalls.length;
21178
+ r.err(chalk13.gray(`Done with ${count3} tool call${count3 === 1 ? "" : "s"} and no text.
20625
21179
  `));
20626
21180
  }
20627
21181
  return result.outputSource === "incomplete" ? EXIT_FAILED : EXIT_DONE;
@@ -20683,11 +21237,11 @@ async function interrupted(r, runId, options, printed) {
20683
21237
  }
20684
21238
  async function resolveDeps(options, deps) {
20685
21239
  if (options.baseUrl) setBaseUrlFlag(options.baseUrl);
20686
- const resolve15 = deps.credentials ?? (async () => {
21240
+ const resolve17 = deps.credentials ?? (async () => {
20687
21241
  const credentials2 = await resolveUltracCredentials();
20688
21242
  return credentials2 ? { apiKey: credentials2.apiKey, baseUrl: credentials2.baseUrl } : null;
20689
21243
  });
20690
- const credentials = await resolve15();
21244
+ const credentials = await resolve17();
20691
21245
  if (!credentials) {
20692
21246
  throw new Error("Not signed in to Ultrac. Run `ultrac login` (or set ULTRAC_API_KEY).");
20693
21247
  }
@@ -20786,7 +21340,7 @@ ${run.output}${run.output.endsWith("\n") ? "" : "\n"}`;
20786
21340
  }
20787
21341
 
20788
21342
  // src/commands/tools.ts
20789
- import { resolve as resolve14 } from "path";
21343
+ import { resolve as resolve16 } from "path";
20790
21344
  import chalk14 from "chalk";
20791
21345
  function registerToolsCommand(program2) {
20792
21346
  const toolsCommand = program2.command("tools");
@@ -20821,7 +21375,7 @@ ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${ser
20821
21375
  const result = await createDefaultToolRegistry().execute(
20822
21376
  { tool, input },
20823
21377
  {
20824
- workspace: resolve14(options.workspace),
21378
+ workspace: resolve16(options.workspace),
20825
21379
  permissions: {
20826
21380
  write: options.allowWrite,
20827
21381
  shell: options.allowShell,