@spinalfluid/ultrac 0.1.18 → 0.1.21

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 +14 -3
  2. package/dist/cli.js +168 -85
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,12 +10,12 @@ between input and output. The hosted API supports streaming and native tool call
10
10
 
11
11
  ```bash
12
12
  npm install -g @spinalfluid/ultrac
13
- ultrac login
14
- ultrac doctor
15
13
  ultrac
16
14
  ```
17
15
 
18
- The npm package is public. Model access still requires an approved Ultrac account and `ultrac login`.
16
+ The npm package is public. The first interactive `ultrac` launch opens browser sign-in automatically
17
+ and continues into the same terminal session after verification. Model access requires an approved
18
+ Ultrac account. You can also sign in explicitly with `ultrac login`.
19
19
  Requires Node.js >= 22.12.0. The command is `ultrac`; the package is `@spinalfluid/ultrac`.
20
20
  The unrelated public packages named `ultrac` and `ultracode` are different products.
21
21
 
@@ -764,3 +764,14 @@ when a tool needs an ungranted permission.
764
764
  These presets do not bypass operating-system permissions or the restrictions of a read-only
765
765
  agent mode. Approval dialogs offer separate choices for one call, the session, denial, or
766
766
  session-long refusal, with keyboard selection in interactive terminals.
767
+
768
+ After a terminal permission selection, the approval dialog collapses to the tool, permission,
769
+ and selected choice. Activity and progress messages stay in chronological transcript order,
770
+ with spacing between groups; command details remain available through the activity dropdown.
771
+
772
+ Progress prose comes from the model and describes the actual work or verified findings. The CLI
773
+ does not generate periodic tool-count, failure-count, or model-waiting messages.
774
+
775
+ Use `/permissions` to open the interactive permission-mode picker, `/permissions status` to
776
+ inspect grants, or `/permissions readonly`, `/permissions workspace`, and `/permissions full`
777
+ to change modes directly for the current session.
package/dist/cli.js CHANGED
@@ -10834,6 +10834,8 @@ ${mode.instructions.map((instruction) => `- ${instruction}`).join("\n")}
10834
10834
  ${autonomyLines}
10835
10835
  ${UNTRUSTED_CONTENT_GUIDANCE}
10836
10836
 
10837
+ For long tasks, occasionally give a brief user-facing update about the current work or verified findings. Avoid repetitive updates, tool-call counts, failure counts, and boilerplate about waiting for the model. Do not invent findings or claim work that has not happened.
10838
+
10837
10839
  ${nativeTools ? "Write complete code files or append complete sections across tool calls for large implementations. Verify files with tests or a compiler. Never replace requested code with placeholders or ellipses. Keep final explanations concise. Use the provided native function tools for actions. Do not put a JSON tool-call protocol in your answer. When the requested work is complete, give the final answer without calling more tools." : `Respond only with a JSON object:
10838
10840
  {
10839
10841
  "message": "short user-facing status or final answer",
@@ -13809,13 +13811,9 @@ function registerLoginCommand(program2) {
13809
13811
  );
13810
13812
  }
13811
13813
  let apiKey = options.key?.trim();
13812
- let email;
13813
- let keyId;
13814
13814
  if (!apiKey && !options.manual) {
13815
- const signedIn = await browserLogin(baseUrl, options.browser);
13816
- apiKey = signedIn.apiKey;
13817
- email = signedIn.email;
13818
- keyId = signedIn.keyId;
13815
+ await signInWithBrowser({ baseUrl, browser: options.browser });
13816
+ return;
13819
13817
  } else if (!apiKey) {
13820
13818
  const loginUrl = loginUrlFor(baseUrl);
13821
13819
  console.log(`Create an API key at ${chalk4.cyan(loginUrl)} and paste it below.`);
@@ -13828,7 +13826,7 @@ function registerLoginCommand(program2) {
13828
13826
  stdin3.isTTY ? "No API key provided. Nothing was saved." : "No API key provided on stdin. Nothing was saved. Set ULTRAC_API_KEY for non-interactive use or pass an existing key with --key."
13829
13827
  );
13830
13828
  }
13831
- await verifyAndStore({ apiKey, baseUrl, email, keyId, pasted: Boolean(options.manual || options.key) });
13829
+ await verifyAndStore({ apiKey, baseUrl, pasted: Boolean(options.manual || options.key) });
13832
13830
  });
13833
13831
  program2.command("logout").description("Remove stored Ultrac credentials").action(async () => {
13834
13832
  const stored = await readCredentials();
@@ -13859,6 +13857,11 @@ function registerLoginCommand(program2) {
13859
13857
  if (stored?.createdAt) console.log(` since: ${stored.createdAt}`);
13860
13858
  });
13861
13859
  }
13860
+ async function signInWithBrowser(input) {
13861
+ const baseUrl = assertAllowedUltracBaseUrl(input.baseUrl);
13862
+ const signedIn = await browserLogin(baseUrl, input.browser !== false);
13863
+ await verifyAndStore({ ...signedIn, baseUrl, showStartupHint: !input.continueSession });
13864
+ }
13862
13865
  async function browserLogin(baseUrl, shouldOpen) {
13863
13866
  try {
13864
13867
  return await loginWithBrowser({
@@ -13903,7 +13906,7 @@ async function verifyAndStore(input) {
13903
13906
  });
13904
13907
  console.log(chalk4.green(`Signed in. Credentials saved to ${path}`));
13905
13908
  if (input.email) console.log(chalk4.gray(` ${input.email}`));
13906
- console.log(chalk4.gray("Run `ultrac` to start an interactive session."));
13909
+ if (input.showStartupHint !== false) console.log(chalk4.gray("Run `ultrac` to start an interactive session."));
13907
13910
  }
13908
13911
  async function revokeStoredKey(apiKey, baseUrl) {
13909
13912
  const url = currentKeyUrl(baseUrl);
@@ -17005,6 +17008,8 @@ ${mode.instructions.map((instruction) => `- ${instruction}`).join("\n")}
17005
17008
 
17006
17009
  ${UNTRUSTED_CONTENT_GUIDANCE}
17007
17010
 
17011
+ For long tasks, occasionally give a brief user-facing update about the current work or verified findings. Avoid repetitive updates, tool-call counts, failure counts, and boilerplate about waiting for the model. Do not invent findings or claim work that has not happened.
17012
+
17008
17013
  For large code generation, write complete files or append complete sections across multiple tool calls. Verify the files with the project tests or compiler. Never use ellipses or placeholders for requested implementation, and do not declare success with unfinished code. Keep final explanations concise; code completeness takes priority over response brevity.
17009
17014
 
17010
17015
  Use the provided tools to inspect and change the workspace. Prefer narrow, reversible changes and
@@ -17636,6 +17641,16 @@ function formatSeconds(milliseconds) {
17636
17641
 
17637
17642
  // src/ui/activity.ts
17638
17643
  var sink;
17644
+ var messageSink;
17645
+ function setActivityMessageSink(value) {
17646
+ messageSink = value;
17647
+ }
17648
+ function writeActivityMessage(text) {
17649
+ if (messageSink) messageSink(text);
17650
+ else console.log(`
17651
+ ${text}
17652
+ `);
17653
+ }
17639
17654
  var workingSink;
17640
17655
  function setWorkingSink(value) {
17641
17656
  workingSink = value;
@@ -17670,7 +17685,7 @@ function activityRows(snapshot, expanded, selected, end) {
17670
17685
  const summary = `${expanded ? "\u25BE" : "\u25B8"} \u25CF ${snapshot.entries.length} ${snapshot.entries.length === 1 ? "tool call" : "tool calls"}${failures ? ` \xB7 ${failures} failed` : ""}${snapshot.finished ? elapsed : ""} \xB7 Ctrl+O details`;
17671
17686
  const rows = [{ text: summary, ok: failures || snapshot.failed ? false : snapshot.finished ? true : void 0 }];
17672
17687
  if (!expanded) {
17673
- if (!snapshot.finished) for (let index = Math.max(0, snapshot.entries.length - 3); index < snapshot.entries.length; index += 1) {
17688
+ if (!snapshot.finished || snapshot.preview) for (let index = Math.max(0, snapshot.entries.length - 3); index < snapshot.entries.length; index += 1) {
17674
17689
  const entry = snapshot.entries[index];
17675
17690
  rows.push({ text: ` \u25CF ${entry.label}`, ok: entry.ok, entry: index });
17676
17691
  }
@@ -17690,32 +17705,6 @@ function activityRows(snapshot, expanded, selected, end) {
17690
17705
  return rows;
17691
17706
  }
17692
17707
 
17693
- // src/ui/task-progress.ts
17694
- function startTaskProgress(options) {
17695
- let lastMessage = performance.now();
17696
- let lastState = "";
17697
- let stopped = false;
17698
- const timer = setInterval(() => {
17699
- if (stopped || options.holdWhile?.()) return;
17700
- const state = options.snapshot();
17701
- if (!state.activeTool && !state.waitingForModel) return;
17702
- const fingerprint = `${state.completed}:${state.failed}:${state.activeTool ?? "model"}`;
17703
- if (performance.now() - lastMessage < (fingerprint === lastState ? 6e4 : 3e4)) return;
17704
- const completed = state.completed ? `I have completed ${state.completed} tool ${state.completed === 1 ? "call" : "calls"}.${state.failed ? ` Of those, ${state.failed} failed.` : ""} ` : "";
17705
- const current = state.activeTool ? `I am running the ${state.activeTool} tool.` : "I am waiting for the model's next response.";
17706
- options.write(`${completed}${current}`);
17707
- lastMessage = performance.now();
17708
- lastState = fingerprint;
17709
- }, 5e3);
17710
- timer.unref?.();
17711
- return { markMessage: () => {
17712
- lastMessage = performance.now();
17713
- }, stop: () => {
17714
- stopped = true;
17715
- clearInterval(timer);
17716
- } };
17717
- }
17718
-
17719
17708
  // src/ui/interactive-choice.ts
17720
17709
  var MOUSE_ON = "\x1B[?1006h\x1B[?1000h";
17721
17710
  var MOUSE_OFF = "\x1B[?1000l\x1B[?1006l";
@@ -18117,6 +18106,9 @@ var TerminalLayout = class {
18117
18106
  prompt = "\u203A ";
18118
18107
  inputFormatter;
18119
18108
  transcriptRow = 1;
18109
+ transientBlocks = /* @__PURE__ */ new Set();
18110
+ activityOffset = 0;
18111
+ totalActivityEntries = 0;
18120
18112
  transcriptColumn = 1;
18121
18113
  wrapPending = false;
18122
18114
  savedTranscript;
@@ -18230,7 +18222,61 @@ var TerminalLayout = class {
18230
18222
  };
18231
18223
  this.paintFooter();
18232
18224
  }
18225
+ beginTransientBlock() {
18226
+ const anchor = { row: this.transcriptRow };
18227
+ this.transientBlocks.add(anchor);
18228
+ return anchor;
18229
+ }
18230
+ collapseTransientBlock(anchor, summary) {
18231
+ if (!this.transientBlocks.delete(anchor) || this.closed) return;
18232
+ const start = Math.max(1, Math.min(anchor.row, this.transcriptRow));
18233
+ const removed = Math.max(0, this.transcriptRow - start);
18234
+ if (removed) this.rawWrite(`${cup(start)}${CSI}${removed}M`);
18235
+ this.rawWrite(`${cup(start)}${CSI}2K`);
18236
+ for (const item of this.completedActivities) if (item.row >= start) item.row -= removed;
18237
+ this.completedActivities = this.completedActivities.filter((item) => item.row >= 1);
18238
+ this.transcriptRow = start;
18239
+ this.transcriptColumn = 1;
18240
+ this.wrapPending = false;
18241
+ this.writeTranscript(`${summary}
18242
+
18243
+ `);
18244
+ this.paintFooter();
18245
+ }
18246
+ writeActivityMessage(text) {
18247
+ const item = this.liveActivity;
18248
+ if (item) {
18249
+ if (item.snapshot.entries.length) {
18250
+ item.snapshot = { ...item.snapshot, finished: true, preview: true };
18251
+ if (item.row >= 2) this.rawWrite(`${cup(item.row - 1)}${CSI}2K`);
18252
+ this.redrawCompleted(item);
18253
+ } else {
18254
+ const start = Math.max(1, item.row - 1);
18255
+ const removed = item.row >= 1 ? Math.max(0, Math.min(item.height + (item.row >= 2 ? 1 : 0), this.transcriptRow - start)) : 0;
18256
+ if (removed) this.rawWrite(`${cup(start)}${CSI}${removed}M`);
18257
+ this.transcriptRow -= removed;
18258
+ this.completedActivities = this.completedActivities.filter((entry) => entry !== item);
18259
+ }
18260
+ this.liveActivity = void 0;
18261
+ }
18262
+ this.activityOffset = this.totalActivityEntries;
18263
+ this.writeTranscript(`
18264
+ ${text}
18265
+
18266
+ `);
18267
+ if (item) {
18268
+ const snapshot = { ...item.snapshot, entries: [], finished: false, preview: false };
18269
+ this.writeTranscript(`
18270
+ ${this.colorActivityRow(activityRows(snapshot, false, -1, 0)[0])}
18271
+ `);
18272
+ this.liveActivity = { snapshot, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1, end: 0 };
18273
+ this.completedActivities.push(this.liveActivity);
18274
+ }
18275
+ }
18233
18276
  setActivity(snapshot) {
18277
+ if (!this.liveActivity && snapshot.entries.length < this.totalActivityEntries) this.activityOffset = 0;
18278
+ this.totalActivityEntries = snapshot.entries.length;
18279
+ snapshot = { ...snapshot, entries: snapshot.entries.slice(this.activityOffset) };
18234
18280
  this.activity = snapshot;
18235
18281
  if (!this.enabled || !this.installed || this.closed) return;
18236
18282
  if (!this.liveActivity && !snapshot.finished) {
@@ -18257,9 +18303,13 @@ ${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.lengt
18257
18303
  }
18258
18304
  this.completedActivities = this.completedActivities.filter((entry) => entry !== item);
18259
18305
  this.liveActivity = void 0;
18260
- this.writeTranscript(`${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.length)[0])}
18306
+ this.activityOffset = 0;
18307
+ this.totalActivityEntries = 0;
18308
+ if (snapshot.entries.length) {
18309
+ this.writeTranscript(`${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.length)[0])}
18261
18310
  `);
18262
- this.completedActivities.push({ ...item, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1 });
18311
+ this.completedActivities.push({ ...item, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1 });
18312
+ }
18263
18313
  this.paintFooter();
18264
18314
  }
18265
18315
  setWorkingStatus(text) {
@@ -18290,6 +18340,7 @@ ${this.colorActivityRow(activityRows(item.snapshot, false, -1, item.end)[0])}
18290
18340
  return dot >= 0 && color ? `${text.slice(0, dot)}${color}\u25CF${CSI}39m${text.slice(dot + 1)}` : text;
18291
18341
  }
18292
18342
  shiftCompleted(rows) {
18343
+ for (const anchor of this.transientBlocks) anchor.row -= rows;
18293
18344
  for (const item of this.completedActivities) item.row -= rows;
18294
18345
  this.completedActivities = this.completedActivities.filter((item) => item.row >= 1);
18295
18346
  }
@@ -18313,7 +18364,7 @@ ${this.colorActivityRow(activityRows(item.snapshot, false, -1, item.end)[0])}
18313
18364
  }
18314
18365
  toggleActivity() {
18315
18366
  if (this.liveActivity && (this.liveActivity.row < 2 || this.liveActivity.row + this.liveActivity.height > this.contentRows)) this.setWorkingStatus("working\u2026");
18316
- const item = this.liveActivity ?? this.completedActivities.at(-1);
18367
+ const item = this.liveActivity?.snapshot.entries.length ? this.liveActivity : [...this.completedActivities].reverse().find((entry) => entry.snapshot.entries.length > 0) ?? this.liveActivity;
18317
18368
  if (item) {
18318
18369
  item.expanded = !item.expanded;
18319
18370
  this.redrawCompleted(item);
@@ -18926,7 +18977,7 @@ var HELP = `Commands:
18926
18977
  /autonomous [on|off|status] Continue step-limited turns (up to five rounds)
18927
18978
  /model [name] Show or set the model
18928
18979
  /mode [name] Show or set the agent mode (${agentModeNames.join(", ")})
18929
- /permissions [readonly|workspace|full] Show or set permissions
18980
+ /permissions [readonly|workspace|full|status] Choose or set tool access
18930
18981
  /deny <perm> Revoke a permission for this session
18931
18982
  /plan [task] Plan without workspace edits
18932
18983
  /review [focus] Review current changes without editing
@@ -19049,6 +19100,10 @@ async function runRepl(options, initialTask = "") {
19049
19100
  throw new Error("--max-steps must be a positive integer.");
19050
19101
  }
19051
19102
  const hasFallbackChain = Boolean(config.fallback?.chain?.length);
19103
+ if (providerName === "ultrac" && !hasFallbackChain && !await resolveUltracCredentials() && stdin5.isTTY && stdout5.isTTY) {
19104
+ console.log("Opening browser sign-in\u2026");
19105
+ await signInWithBrowser({ baseUrl: resolveBaseUrl().baseUrl, continueSession: true });
19106
+ }
19052
19107
  if (providerName === "ultrac" && !hasFallbackChain && !await resolveUltracCredentials()) {
19053
19108
  console.log(chalk11.yellow("You are not signed in to Ultrac."));
19054
19109
  console.log(`Run ${chalk11.white("ultrac login")} first, or start with ${chalk11.white("--provider echo")}.`);
@@ -19125,6 +19180,7 @@ async function runRepl(options, initialTask = "") {
19125
19180
  }) : void 0;
19126
19181
  setActivitySink(layout?.enabled ? (snapshot) => layout.setActivity(snapshot) : void 0);
19127
19182
  setWorkingSink(layout?.enabled ? (text) => layout.setWorkingStatus(text) : void 0);
19183
+ setActivityMessageSink(layout?.enabled ? (text) => layout.writeActivityMessage(text) : void 0);
19128
19184
  const refreshDetails = () => {
19129
19185
  if (!layout?.enabled) return;
19130
19186
  const steps = session.toSession().steps;
@@ -19209,36 +19265,44 @@ async function runRepl(options, initialTask = "") {
19209
19265
  };
19210
19266
  const drainQueuedMessages = () => reader.drainQueuedMessages();
19211
19267
  let autonomousRounds = 5;
19212
- setAskUserRuntime(async (text, options2, signal) => {
19213
- console.log(chalk11.white(`
19268
+ setAskUserRuntime(async (text, options2, signal, presentation) => {
19269
+ const block = presentation && layout?.enabled ? layout.beginTransientBlock() : void 0;
19270
+ let resolvedAnswer = null;
19271
+ try {
19272
+ console.log(chalk11.white(`
19214
19273
  ? ${text}`));
19215
- let rawAnswer;
19216
- if (options2?.length && layout?.enabled) {
19217
- slashCompletion?.suspend();
19218
- layout.setPrompt("\u203A ");
19219
- let choices;
19220
- try {
19221
- choices = attachInteractiveChoices(rl, options2, { signal, output: stdout5, prompt: "\u203A ", maxRow: () => layout.contentRows });
19222
- rawAnswer = await reader.nextFresh(signal);
19223
- } finally {
19224
- choices?.cleanup();
19274
+ let rawAnswer;
19275
+ if (options2?.length && layout?.enabled) {
19276
+ slashCompletion?.suspend();
19225
19277
  layout.setPrompt("\u203A ");
19226
- slashCompletion?.resume();
19278
+ let choices;
19279
+ try {
19280
+ choices = attachInteractiveChoices(rl, options2, { signal, output: stdout5, prompt: "\u203A ", maxRow: () => layout.contentRows });
19281
+ rawAnswer = await reader.nextFresh(signal);
19282
+ } finally {
19283
+ choices?.cleanup();
19284
+ layout.setPrompt("\u203A ");
19285
+ slashCompletion?.resume();
19286
+ }
19287
+ } else {
19288
+ if (options2?.length) {
19289
+ options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
19290
+ console.log(chalk11.gray(" (type a number, or answer in your own words)"));
19291
+ }
19292
+ rawAnswer = await question(chalk11.white(" \u203A "), signal);
19227
19293
  }
19228
- } else {
19229
- if (options2?.length) {
19230
- options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
19231
- console.log(chalk11.gray(" (type a number, or answer in your own words)"));
19294
+ if (signal?.aborted) return null;
19295
+ const answer = (rawAnswer ?? "").trim();
19296
+ const picked = Number.parseInt(answer, 10);
19297
+ if (options2?.length && String(picked) === answer && picked >= 1 && picked <= options2.length) {
19298
+ resolvedAnswer = options2[picked - 1];
19299
+ return resolvedAnswer;
19232
19300
  }
19233
- rawAnswer = await question(chalk11.white(" \u203A "), signal);
19234
- }
19235
- if (signal?.aborted) return null;
19236
- const answer = (rawAnswer ?? "").trim();
19237
- const picked = Number.parseInt(answer, 10);
19238
- if (options2?.length && String(picked) === answer && picked >= 1 && picked <= options2.length) {
19239
- return options2[picked - 1];
19301
+ resolvedAnswer = rawAnswer === null ? null : answer;
19302
+ return resolvedAnswer;
19303
+ } finally {
19304
+ if (block && presentation) layout?.collapseTransientBlock(block, presentation.collapseSummary(signal?.aborted ? null : resolvedAnswer));
19240
19305
  }
19241
- return answer;
19242
19306
  });
19243
19307
  let running = true;
19244
19308
  let turnAbort;
@@ -19366,6 +19430,7 @@ async function runRepl(options, initialTask = "") {
19366
19430
  setAskUserRuntime(void 0);
19367
19431
  setActivitySink(void 0);
19368
19432
  setWorkingSink(void 0);
19433
+ setActivityMessageSink(void 0);
19369
19434
  rl.close();
19370
19435
  try {
19371
19436
  await saveTranscript(session, writeCheckpoint);
@@ -19405,17 +19470,6 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
19405
19470
  const detachProgress = quiet ? () => {
19406
19471
  } : attachProgressUi();
19407
19472
  let deltaChars = 0;
19408
- const progress2 = quiet ? void 0 : startTaskProgress({
19409
- snapshot: () => ({
19410
- completed: entries.length,
19411
- failed: entries.filter((entry) => !entry.ok).length,
19412
- activeTool: [...activeTools.keys()].at(-1)?.tool,
19413
- waitingForModel: thinkingStarted !== void 0
19414
- }),
19415
- holdWhile: () => permissionPrompts > 0 || [...activeTools.keys()].some((call) => call.tool === "ask_user"),
19416
- write: (text) => console.log(`
19417
- ${text}`)
19418
- });
19419
19473
  try {
19420
19474
  const prepared = allowImageDrop ? await prepareImageInput(input) : { text: input };
19421
19475
  await session.ask(prepared.text, {
@@ -19450,9 +19504,7 @@ ${text}`)
19450
19504
  },
19451
19505
  onAssistantMessage: (message) => {
19452
19506
  if (message.trim()) {
19453
- progress2?.markMessage();
19454
- console.log(`
19455
- ${message.trim()}`);
19507
+ writeActivityMessage(message.trim());
19456
19508
  }
19457
19509
  },
19458
19510
  onToolStart: (call) => {
@@ -19513,7 +19565,11 @@ ${message.trim()}`);
19513
19565
  Answer: `;
19514
19566
  const choices = ["Allow once", "Allow for this session", "Deny this call", "Never allow this permission this session"];
19515
19567
  const asker = getAskUserRuntime();
19516
- const rawAnswer = asker ? await asker(prompt.split("\n").slice(0, -5).join("\n"), choices, signal) : await question(chalk11.yellow(prompt), signal);
19568
+ const rawAnswer = asker ? await asker(prompt.split("\n").slice(0, -5).join("\n"), choices, signal, { collapseSummary: (value) => {
19569
+ const answer2 = value?.trim().toLowerCase();
19570
+ const label = !answer2 ? "Cancelled" : ["a", "always", "2", choices[1].toLowerCase()].includes(answer2) ? choices[1] : ["o", "once", "y", "yes", "1", choices[0].toLowerCase()].includes(answer2) ? choices[0] : ["e", "never", "4", choices[3].toLowerCase()].includes(answer2) ? choices[3] : choices[2];
19571
+ return ` ${call.tool} \xB7 ${permission} permission: ${label}`;
19572
+ } }) : await question(chalk11.yellow(prompt), signal);
19517
19573
  if (signal?.aborted || rawAnswer === null) return "deny";
19518
19574
  const answer = rawAnswer.trim().toLowerCase();
19519
19575
  if (answer === "a" || answer === "always" || answer === "2" || answer === choices[1].toLowerCase()) return "always";
@@ -19545,7 +19601,6 @@ ${message.trim()}`);
19545
19601
  activeTools.clear();
19546
19602
  if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
19547
19603
  refreshActivity(true);
19548
- progress2?.stop();
19549
19604
  detachProgress();
19550
19605
  }
19551
19606
  }
@@ -19823,15 +19878,43 @@ ${extra}`);
19823
19878
  return "ok";
19824
19879
  }
19825
19880
  case "permissions": {
19826
- if (argument) {
19827
- if (!["readonly", "workspace", "full"].includes(argument)) {
19828
- console.log("Usage: /permissions [readonly|workspace|full]");
19881
+ let selectedMode = argument.toLowerCase();
19882
+ const modes = ["readonly", "workspace", "full"];
19883
+ const choices = ["Readonly: no write, shell, or network access", "Workspace: file writes; shell and network denied", "Full access: no tool approval prompts"];
19884
+ const resolveMode = (value) => {
19885
+ if (value === null) return void 0;
19886
+ const text = value.trim().toLowerCase();
19887
+ const index = choices.findIndex((choice) => choice.toLowerCase() === text);
19888
+ if (index >= 0) return modes[index];
19889
+ if (["1", "2", "3"].includes(text)) return modes[Number(text) - 1];
19890
+ return modes.includes(text) ? text : void 0;
19891
+ };
19892
+ const asker = getAskUserRuntime();
19893
+ if (!selectedMode && asker) {
19894
+ const answer = await asker("Choose tool permissions for this session.", choices, signal, {
19895
+ collapseSummary: (value) => {
19896
+ const mode2 = resolveMode(value);
19897
+ return mode2 ? ` Permission mode: ${mode2}` : " Permission mode unchanged.";
19898
+ }
19899
+ });
19900
+ if (signal?.aborted || answer === null) return "ok";
19901
+ const mode = resolveMode(answer);
19902
+ if (!mode) {
19903
+ console.log("Permission mode unchanged. Use /permissions readonly|workspace|full.");
19904
+ return "ok";
19905
+ }
19906
+ selectedMode = mode;
19907
+ }
19908
+ if (selectedMode && selectedMode !== "status") {
19909
+ if (!modes.includes(selectedMode)) {
19910
+ console.log("Usage: /permissions [readonly|workspace|full|status]");
19829
19911
  return "ok";
19830
19912
  }
19831
19913
  for (const permission of ["write", "shell", "network"]) {
19832
- session.setPermission(permission, argument === "full" || argument === "workspace" && permission === "write");
19914
+ session.setPermission(permission, selectedMode === "full" || selectedMode === "workspace" && permission === "write");
19833
19915
  }
19834
- console.log(argument === "full" ? "Full access: file, shell, and network tools run without approval prompts for this session." : "Permissions updated. Ungranted tools are denied without prompting; /allow restores access.");
19916
+ if (argument) console.log(selectedMode === "full" ? "Full access: file, shell, and network tools run without approval prompts for this session." : `Permission mode: ${selectedMode}. Ungranted tools are denied without prompting.`);
19917
+ return "ok";
19835
19918
  }
19836
19919
  const p = session.currentPermissions;
19837
19920
  console.log(`write: ${p.write} shell: ${p.shell} network: ${p.network}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spinalfluid/ultrac",
3
- "version": "0.1.18",
3
+ "version": "0.1.21",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Ultrac terminal agent harness for Cosmic 1, with local tools, browser sign-in, streamed answers, and resumable sessions.",
6
6
  "author": "Aditya Pundir",