@standardagents/code 0.6.4 → 0.6.7

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.
package/dist/index.js CHANGED
@@ -319,6 +319,25 @@ var ApiClient = class {
319
319
  return null;
320
320
  }
321
321
  }
322
+ /** Preview the cost of changing to `sessions` parallel sessions (default +1).
323
+ * Returns null when billing is unavailable / there's no subscription. */
324
+ async sessionsQuote(threadId, sessions) {
325
+ try {
326
+ return await this.json(`/api/threads/${threadId}/sessions_quote`, {
327
+ method: "POST",
328
+ body: JSON.stringify(typeof sessions === "number" ? { sessions } : {})
329
+ });
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
334
+ /** Apply a parallel-session change (charges via Stripe). Throws on failure. */
335
+ async sessionsUpgrade(threadId, sessions) {
336
+ return this.json(`/api/threads/${threadId}/sessions_upgrade`, {
337
+ method: "POST",
338
+ body: JSON.stringify({ sessions })
339
+ });
340
+ }
322
341
  // ── skills (instance-global Agent Skills library) ─────────────────────────
323
342
  /** Installed skills — metadata only. Includes disabled skills. */
324
343
  async listSkills() {
@@ -365,11 +384,17 @@ var ApiClient = class {
365
384
  };
366
385
 
367
386
  // src/permissions.ts
387
+ function riskCeiling(state) {
388
+ let ceiling = state.level;
389
+ for (const granted of state.allowRisk) {
390
+ if (granted > ceiling) ceiling = granted;
391
+ }
392
+ return ceiling;
393
+ }
368
394
  function decide(state, tool, risk, hasPermissionRequest) {
369
395
  const effectiveRisk = typeof risk === "number" ? Math.min(5, Math.max(1, risk)) : hasPermissionRequest ? 3 : 1;
370
396
  if (state.alwaysAllow.has(tool)) return "allow";
371
- if (state.allowRisk.has(effectiveRisk)) return "allow";
372
- return effectiveRisk <= state.level ? "allow" : "ask";
397
+ return effectiveRisk <= riskCeiling(state) ? "allow" : "ask";
373
398
  }
374
399
  var CATASTROPHIC_PATTERNS = [
375
400
  /\brm\s+(-[a-z]*\s+)*-[a-z]*f[a-z]*\s+(-[a-z]*\s+)*(\/|~|\$HOME|\/\*|\.\s*$|\/\s*$)/i,
@@ -1891,6 +1916,90 @@ var SystemEvents = class {
1891
1916
  }
1892
1917
  };
1893
1918
 
1919
+ // src/subagent-streams.ts
1920
+ var MAX_PHRASE = 120;
1921
+ function cleanPhrase(raw) {
1922
+ const s = raw.replace(/\s+/g, " ").trim();
1923
+ return s.length > MAX_PHRASE ? `${s.slice(0, MAX_PHRASE - 1)}\u2026` : s;
1924
+ }
1925
+ function prettyToolName(name) {
1926
+ return name.replace(/^provider:/, "").replace(/[_:-]+/g, " ").trim();
1927
+ }
1928
+ var SubagentActivity = class {
1929
+ constructor(api, onChange, makeStream = (threadId, hooks) => new MessageStream(api, threadId, hooks)) {
1930
+ this.onChange = onChange;
1931
+ this.makeStream = makeStream;
1932
+ }
1933
+ onChange;
1934
+ makeStream;
1935
+ entries = /* @__PURE__ */ new Map();
1936
+ /** The current activity phrase for a subagent's child thread, if any. */
1937
+ phraseFor(threadId) {
1938
+ return this.entries.get(threadId)?.phrase ?? null;
1939
+ }
1940
+ /**
1941
+ * Reconcile the open streams against the authoritative set of RUNNING
1942
+ * subagent child-thread ids (from the parent's registry): open newcomers,
1943
+ * close and forget departed ones.
1944
+ */
1945
+ sync(activeIds) {
1946
+ const want = new Set(activeIds);
1947
+ for (const [id, entry] of [...this.entries]) {
1948
+ if (!want.has(id)) {
1949
+ entry.stream.close();
1950
+ this.entries.delete(id);
1951
+ }
1952
+ }
1953
+ for (const id of want) {
1954
+ if (!this.entries.has(id)) this.open(id);
1955
+ }
1956
+ }
1957
+ open(threadId) {
1958
+ const entry = { stream: null, steps: /* @__PURE__ */ new Map(), phrase: null };
1959
+ const setPhrase = (phrase) => {
1960
+ if (phrase === entry.phrase) return;
1961
+ entry.phrase = phrase;
1962
+ this.onChange(threadId);
1963
+ };
1964
+ const phraseFromSteps = () => {
1965
+ let last = null;
1966
+ for (const v of entry.steps.values()) last = v;
1967
+ return last ?? entry.phrase;
1968
+ };
1969
+ entry.stream = this.makeStream(threadId, {
1970
+ // Streamed output with no tool in flight means the model is composing —
1971
+ // reasoning reads as "thinking", visible answer text as "writing". These
1972
+ // only fire on TRANSITIONS (phrase comparison), not per chunk.
1973
+ onChunk: () => {
1974
+ if (entry.steps.size === 0) setPhrase("writing");
1975
+ },
1976
+ onReasoningChunk: () => {
1977
+ if (entry.steps.size === 0) setPhrase("thinking");
1978
+ },
1979
+ onAssistantText: () => {
1980
+ },
1981
+ onEvent: (eventType, data) => {
1982
+ if (eventType === "tool_call_started" && data?.id) {
1983
+ entry.steps.set(String(data.id), cleanPhrase(String(data.progress || prettyToolName(String(data.name || "")) || "working")));
1984
+ setPhrase(phraseFromSteps());
1985
+ } else if (eventType === "tool_call_done" && data?.id) {
1986
+ entry.steps.delete(String(data.id));
1987
+ setPhrase(phraseFromSteps());
1988
+ }
1989
+ },
1990
+ onError: () => {
1991
+ }
1992
+ });
1993
+ this.entries.set(threadId, entry);
1994
+ void entry.stream.connect();
1995
+ }
1996
+ /** Close every stream (session teardown). */
1997
+ closeAll() {
1998
+ for (const entry of this.entries.values()) entry.stream.close();
1999
+ this.entries.clear();
2000
+ }
2001
+ };
2002
+
1894
2003
  // src/wordmill.ts
1895
2004
  var MILL_WORDS = [
1896
2005
  "Working",
@@ -3350,7 +3459,7 @@ var Tui = class _Tui {
3350
3459
  const options = [
3351
3460
  { value: "allow", label: "Allow once", shortcut: "y", color: C.green },
3352
3461
  { value: "always", label: "Always allow this tool", shortcut: "a", color: C.cyan },
3353
- { value: "always_risk", label: `Allow all level ${risk} this session`, shortcut: "l", color: C.cyan },
3462
+ { value: "always_risk", label: `Allow level ${risk} and below this session`, shortcut: "l", color: C.cyan },
3354
3463
  { value: "deny", label: "Deny", shortcut: "n", color: C.red }
3355
3464
  ];
3356
3465
  let idx = 0;
@@ -4917,6 +5026,8 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
4917
5026
  let editingQueued = false;
4918
5027
  const shownIds = /* @__PURE__ */ new Set();
4919
5028
  const pendingSent = /* @__PURE__ */ new Map();
5029
+ let lastSent = null;
5030
+ let upgradeInFlight = false;
4920
5031
  let tokensIn = 0;
4921
5032
  let tokensOut = 0;
4922
5033
  let liveOut = 0;
@@ -4986,7 +5097,11 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4986
5097
  tui.setGoal(data);
4987
5098
  }
4988
5099
  },
4989
- onError: () => {
5100
+ // A failed turn whose message is the lease service's at-limit denial → offer
5101
+ // an in-terminal upgrade right where the friction happened. (offerUpgrade /
5102
+ // isSessionLimitError are defined below; this callback only fires mid-session.)
5103
+ onError: (err) => {
5104
+ if (isSessionLimitError(err)) void offerUpgrade({ auto: true });
4990
5105
  }
4991
5106
  });
4992
5107
  const activeSubagents = /* @__PURE__ */ new Map();
@@ -4994,8 +5109,12 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4994
5109
  const agentTitlesReady = api.listAgents().then((list) => list.forEach((a) => agentTitles.set(a.name, a.title))).catch(() => {
4995
5110
  });
4996
5111
  const pushSubagents = () => tui.setSubagents(
4997
- [...activeSubagents.entries()].map(([id, s]) => ({ id, label: s.label, agentName: s.agentName }))
5112
+ [...activeSubagents.entries()].map(([id, s]) => {
5113
+ const detail = subActivity.phraseFor(id) ?? s.registryDetail;
5114
+ return { id, label: `${s.title}${detail ? ` \u2014 ${detail}` : ""}`, agentName: s.agentName };
5115
+ })
4998
5116
  );
5117
+ const subActivity = new SubagentActivity(api, () => pushSubagents());
4999
5118
  const reconcileSubagents = async () => {
5000
5119
  try {
5001
5120
  await agentTitlesReady;
@@ -5005,12 +5124,13 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5005
5124
  const status = (s.status || "").trim();
5006
5125
  if (status === "idle" || status === "terminated") continue;
5007
5126
  const oneLineStatus = status.replace(/\s+/g, " ");
5008
- const detail = oneLineStatus && oneLineStatus !== "running" ? ` \u2014 ${oneLineStatus.slice(0, 80)}` : "";
5009
5127
  activeSubagents.set(s.id, {
5010
- label: `${subagentLabel(s, agentTitles)}${detail}`,
5128
+ title: subagentLabel(s, agentTitles),
5129
+ registryDetail: oneLineStatus && oneLineStatus !== "running" ? oneLineStatus.slice(0, 80) : "",
5011
5130
  agentName: s.agent_name ?? void 0
5012
5131
  });
5013
5132
  }
5133
+ subActivity.sync(activeSubagents.keys());
5014
5134
  pushSubagents();
5015
5135
  } catch {
5016
5136
  }
@@ -5057,6 +5177,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5057
5177
  bridge.close();
5058
5178
  stream.close();
5059
5179
  events.close();
5180
+ subActivity.closeAll();
5060
5181
  mcp.closeAll();
5061
5182
  const [, killed2] = await Promise.race([
5062
5183
  Promise.all([stopped2, procsStopped2]),
@@ -5119,6 +5240,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5119
5240
  const extFor = (mime) => ({ "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" })[mime] ?? "bin";
5120
5241
  const toAttachments = (images) => images.map((img) => ({ name: `image-${img.seq}.${extFor(img.mime)}`, mimeType: img.mime, data: img.data }));
5121
5242
  const sendNow = async (text, images = []) => {
5243
+ lastSent = { text, images };
5122
5244
  tui.printUserMessage(text);
5123
5245
  const key = text.trim();
5124
5246
  pendingSent.set(key, (pendingSent.get(key) ?? 0) + 1);
@@ -5157,6 +5279,88 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5157
5279
  link?.preauthed ? `${c.gray}\u2192 account dashboard opened in your browser (signed in)${c.reset}` : `${c.gray}\u2192 opened ${target} \u2014 sign in with your account email${c.reset}`
5158
5280
  );
5159
5281
  };
5282
+ const ordinal = (n) => {
5283
+ const s = ["th", "st", "nd", "rd"], v = n % 100;
5284
+ return `${n}${s[(v - 20) % 10] ?? s[v] ?? s[0]}`;
5285
+ };
5286
+ const fmtCost = (q) => typeof q.amount_cents === "number" && q.amount_cents !== 0 ? `$${(Math.abs(q.amount_cents) / 100).toFixed(2)}` : null;
5287
+ const renderUpgradePanel = (q) => {
5288
+ const dots = [];
5289
+ for (let i = 0; i < q.max; i++) {
5290
+ if (i < q.current) dots.push(`${c.teal}\u25CF${c.reset}`);
5291
+ else if (i === q.current) dots.push(`${c.bold}${gradientText("\uFF0B")}${c.reset}`);
5292
+ else dots.push(`${c.dim}\xB7${c.reset}`);
5293
+ }
5294
+ const cost = fmtCost(q);
5295
+ const lines = [
5296
+ "",
5297
+ `${c.bold}${gradientText("\u2726 Add a parallel session")}${c.reset}`,
5298
+ "",
5299
+ `${dots.join(" ")} ${c.dim}${q.current} of ${q.current} session${q.current === 1 ? "" : "s"} in use${c.reset}`
5300
+ ];
5301
+ if (q.ends_trial) {
5302
+ lines.push(
5303
+ `${c.yellow}Your $5 trial covers 1 session. Adding a ${ordinal(q.sessions)} ends the trial${c.reset}`,
5304
+ `${c.yellow}and starts your $49/mo plan now${cost ? ` \u2014 ${c.bold}${cost} charged today${c.reset}${c.yellow}` : ""}.${c.reset}`
5305
+ );
5306
+ } else if (cost) {
5307
+ lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 ${c.bold}${cost} charged now${c.reset}.`);
5308
+ } else {
5309
+ lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 prorated on your next invoice.`);
5310
+ }
5311
+ lines.push("");
5312
+ return lines.join("\n");
5313
+ };
5314
+ const offerUpgrade = async (opts = {}) => {
5315
+ if (upgradeInFlight) return;
5316
+ upgradeInFlight = true;
5317
+ try {
5318
+ const quote = await api.sessionsQuote(threadId);
5319
+ if (!quote) {
5320
+ const link = await api.accountLink(threadId).catch(() => null);
5321
+ const target = link?.url ?? "https://standardcode.ai/account";
5322
+ openUrl(target);
5323
+ tui.print(`${c.gray}\u2192 opened ${target} to manage your plan${c.reset}`);
5324
+ return;
5325
+ }
5326
+ if (quote.current >= quote.max) {
5327
+ tui.print(`${c.yellow}You're at the maximum of ${quote.max} parallel sessions.${c.reset}`);
5328
+ return;
5329
+ }
5330
+ tui.print(renderUpgradePanel(quote));
5331
+ const cost = fmtCost(quote);
5332
+ const confirmLabel = quote.ends_trial ? `End trial & start now${cost ? ` \u2014 pay ${cost} today` : ""}` : `Confirm${cost ? ` \u2014 pay ${cost} now` : " \u2014 add the session"}`;
5333
+ const choice = await tui.select(`Add a ${ordinal(quote.sessions)} parallel session?`, [
5334
+ { label: confirmLabel, value: "go" },
5335
+ { label: "Not now", value: "no" }
5336
+ ]);
5337
+ if (choice !== "go") {
5338
+ tui.print(`${c.gray}No change made \u2014 you can upgrade anytime with /upgrade.${c.reset}`);
5339
+ return;
5340
+ }
5341
+ tui.print(`${c.gray}Applying\u2026${c.reset}`);
5342
+ let applied;
5343
+ try {
5344
+ applied = await api.sessionsUpgrade(threadId, quote.sessions);
5345
+ } catch (e) {
5346
+ tui.print(`${c.red}\u2717${c.reset} Upgrade failed: ${e instanceof Error ? e.message : String(e)}`);
5347
+ return;
5348
+ }
5349
+ if (!applied?.ok) {
5350
+ tui.print(`${c.red}\u2717${c.reset} Upgrade failed: ${applied?.error ?? "unknown error"}`);
5351
+ return;
5352
+ }
5353
+ const n = applied.sessions ?? quote.sessions;
5354
+ tui.print(`${c.green}\u2713${c.reset} ${c.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c.reset}`);
5355
+ if (opts.auto && lastSent) {
5356
+ tui.print(`${c.gray}Continuing\u2026${c.reset}`);
5357
+ await sendNow(lastSent.text, lastSent.images);
5358
+ }
5359
+ } finally {
5360
+ upgradeInFlight = false;
5361
+ }
5362
+ };
5363
+ const isSessionLimitError = (msg) => /thread limit reached|simultaneous (thread|session)s?.*in use|no active standard code subscription/i.test(msg);
5160
5364
  const skillsCtl = {
5161
5365
  list: () => api.listSkills(),
5162
5366
  setEnabled: (name, enabled) => api.setSkillEnabled(name, enabled),
@@ -5214,6 +5418,12 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5214
5418
  hint: "billing & sessions, opens in browser",
5215
5419
  run: () => runAccountCommand()
5216
5420
  },
5421
+ {
5422
+ name: "upgrade",
5423
+ label: "Add a parallel session",
5424
+ hint: "run more sessions at once",
5425
+ run: () => offerUpgrade()
5426
+ },
5217
5427
  { name: "background", label: "Background processes", hint: "list / stop", run: () => runProcessMenu(tui, bgMgr) },
5218
5428
  { name: "keybindings", label: "Keyboard shortcuts", run: () => showKeybindings(tui) },
5219
5429
  { name: "update", label: "Check for updates", hint: "check for a newer version", run: () => runUpdateCommand(tui) },
@@ -5314,6 +5524,11 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5314
5524
  if (shownIds.has(m.id) || m.status === "pending") continue;
5315
5525
  shownIds.add(m.id);
5316
5526
  const text = messageText(m.content).trim();
5527
+ const denial = typeof m.error === "string" && m.error || text;
5528
+ if (denial && isSessionLimitError(denial)) {
5529
+ void offerUpgrade({ auto: true });
5530
+ continue;
5531
+ }
5317
5532
  if (m.role === "assistant" && text) printAssistant(tui, text);
5318
5533
  else if (m.role === "system" && text) tui.print(`${c.dim}${text}${c.reset}`);
5319
5534
  else if (m.role === "user" && text) {
@@ -5383,6 +5598,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5383
5598
  bridge.close();
5384
5599
  stream.close();
5385
5600
  events.close();
5601
+ subActivity.closeAll();
5386
5602
  mcp.closeAll();
5387
5603
  const [, killed] = await Promise.race([
5388
5604
  Promise.all([stopped, procsStopped]),
@@ -5552,7 +5768,7 @@ async function runApprovalsMenu(tui, perm, save) {
5552
5768
  }
5553
5769
  const items = [
5554
5770
  ...tools.map((t) => ({ label: `Tool: ${t}`, hint: "always allowed", value: `tool:${t}` })),
5555
- ...risks.map((r) => ({ label: `All level ${r} risk`, hint: "always allowed", value: `risk:${r}` })),
5771
+ ...risks.map((r) => ({ label: `Risk level ${r} and below`, hint: "auto-accepted", value: `risk:${r}` })),
5556
5772
  { label: "Clear all approvals", hint: "", value: "clear" }
5557
5773
  ];
5558
5774
  const picked = await tui.select(