@standardagents/code 0.6.3 → 0.6.6

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
@@ -305,6 +305,39 @@ var ApiClient = class {
305
305
  } catch {
306
306
  }
307
307
  }
308
+ /** Mint a pre-authed standardcode.ai account-dashboard URL for this thread's
309
+ * user. Returns null when the chain is unavailable — the caller falls back
310
+ * to the plain dashboard URL (sign-in by inbox link). */
311
+ async accountLink(threadId) {
312
+ try {
313
+ const res = await this.json(
314
+ `/api/threads/${threadId}/account_link`,
315
+ { method: "POST" }
316
+ );
317
+ return res?.url ? { url: res.url, preauthed: res.preauthed === true } : null;
318
+ } catch {
319
+ return null;
320
+ }
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
+ }
308
341
  // ── skills (instance-global Agent Skills library) ─────────────────────────
309
342
  /** Installed skills — metadata only. Includes disabled skills. */
310
343
  async listSkills() {
@@ -351,11 +384,17 @@ var ApiClient = class {
351
384
  };
352
385
 
353
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
+ }
354
394
  function decide(state, tool, risk, hasPermissionRequest) {
355
395
  const effectiveRisk = typeof risk === "number" ? Math.min(5, Math.max(1, risk)) : hasPermissionRequest ? 3 : 1;
356
396
  if (state.alwaysAllow.has(tool)) return "allow";
357
- if (state.allowRisk.has(effectiveRisk)) return "allow";
358
- return effectiveRisk <= state.level ? "allow" : "ask";
397
+ return effectiveRisk <= riskCeiling(state) ? "allow" : "ask";
359
398
  }
360
399
  var CATASTROPHIC_PATTERNS = [
361
400
  /\brm\s+(-[a-z]*\s+)*-[a-z]*f[a-z]*\s+(-[a-z]*\s+)*(\/|~|\$HOME|\/\*|\.\s*$|\/\s*$)/i,
@@ -3336,7 +3375,7 @@ var Tui = class _Tui {
3336
3375
  const options = [
3337
3376
  { value: "allow", label: "Allow once", shortcut: "y", color: C.green },
3338
3377
  { value: "always", label: "Always allow this tool", shortcut: "a", color: C.cyan },
3339
- { value: "always_risk", label: `Allow all level ${risk} this session`, shortcut: "l", color: C.cyan },
3378
+ { value: "always_risk", label: `Allow level ${risk} and below this session`, shortcut: "l", color: C.cyan },
3340
3379
  { value: "deny", label: "Deny", shortcut: "n", color: C.red }
3341
3380
  ];
3342
3381
  let idx = 0;
@@ -4903,6 +4942,8 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
4903
4942
  let editingQueued = false;
4904
4943
  const shownIds = /* @__PURE__ */ new Set();
4905
4944
  const pendingSent = /* @__PURE__ */ new Map();
4945
+ let lastSent = null;
4946
+ let upgradeInFlight = false;
4906
4947
  let tokensIn = 0;
4907
4948
  let tokensOut = 0;
4908
4949
  let liveOut = 0;
@@ -4972,7 +5013,11 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4972
5013
  tui.setGoal(data);
4973
5014
  }
4974
5015
  },
4975
- onError: () => {
5016
+ // A failed turn whose message is the lease service's at-limit denial → offer
5017
+ // an in-terminal upgrade right where the friction happened. (offerUpgrade /
5018
+ // isSessionLimitError are defined below; this callback only fires mid-session.)
5019
+ onError: (err) => {
5020
+ if (isSessionLimitError(err)) void offerUpgrade({ auto: true });
4976
5021
  }
4977
5022
  });
4978
5023
  const activeSubagents = /* @__PURE__ */ new Map();
@@ -5105,6 +5150,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5105
5150
  const extFor = (mime) => ({ "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" })[mime] ?? "bin";
5106
5151
  const toAttachments = (images) => images.map((img) => ({ name: `image-${img.seq}.${extFor(img.mime)}`, mimeType: img.mime, data: img.data }));
5107
5152
  const sendNow = async (text, images = []) => {
5153
+ lastSent = { text, images };
5108
5154
  tui.printUserMessage(text);
5109
5155
  const key = text.trim();
5110
5156
  pendingSent.set(key, (pendingSent.get(key) ?? 0) + 1);
@@ -5134,6 +5180,97 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5134
5180
  tui.print(`${c.red}\u2717${c.reset} couldn't start compaction: ${err.message}`);
5135
5181
  }
5136
5182
  };
5183
+ const runAccountCommand = async () => {
5184
+ tui.print(`${c.gray}Opening your account\u2026${c.reset}`);
5185
+ const link = await api.accountLink(threadId).catch(() => null);
5186
+ const target = link?.url ?? "https://standardcode.ai/account";
5187
+ openUrl(target);
5188
+ tui.print(
5189
+ 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}`
5190
+ );
5191
+ };
5192
+ const ordinal = (n) => {
5193
+ const s = ["th", "st", "nd", "rd"], v = n % 100;
5194
+ return `${n}${s[(v - 20) % 10] ?? s[v] ?? s[0]}`;
5195
+ };
5196
+ const fmtCost = (q) => typeof q.amount_cents === "number" && q.amount_cents !== 0 ? `$${(Math.abs(q.amount_cents) / 100).toFixed(2)}` : null;
5197
+ const renderUpgradePanel = (q) => {
5198
+ const dots = [];
5199
+ for (let i = 0; i < q.max; i++) {
5200
+ if (i < q.current) dots.push(`${c.teal}\u25CF${c.reset}`);
5201
+ else if (i === q.current) dots.push(`${c.bold}${gradientText("\uFF0B")}${c.reset}`);
5202
+ else dots.push(`${c.dim}\xB7${c.reset}`);
5203
+ }
5204
+ const cost = fmtCost(q);
5205
+ const lines = [
5206
+ "",
5207
+ `${c.bold}${gradientText("\u2726 Add a parallel session")}${c.reset}`,
5208
+ "",
5209
+ `${dots.join(" ")} ${c.dim}${q.current} of ${q.current} session${q.current === 1 ? "" : "s"} in use${c.reset}`
5210
+ ];
5211
+ if (q.ends_trial) {
5212
+ lines.push(
5213
+ `${c.yellow}Your $5 trial covers 1 session. Adding a ${ordinal(q.sessions)} ends the trial${c.reset}`,
5214
+ `${c.yellow}and starts your $49/mo plan now${cost ? ` \u2014 ${c.bold}${cost} charged today${c.reset}${c.yellow}` : ""}.${c.reset}`
5215
+ );
5216
+ } else if (cost) {
5217
+ lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 ${c.bold}${cost} charged now${c.reset}.`);
5218
+ } else {
5219
+ lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 prorated on your next invoice.`);
5220
+ }
5221
+ lines.push("");
5222
+ return lines.join("\n");
5223
+ };
5224
+ const offerUpgrade = async (opts = {}) => {
5225
+ if (upgradeInFlight) return;
5226
+ upgradeInFlight = true;
5227
+ try {
5228
+ const quote = await api.sessionsQuote(threadId);
5229
+ if (!quote) {
5230
+ const link = await api.accountLink(threadId).catch(() => null);
5231
+ const target = link?.url ?? "https://standardcode.ai/account";
5232
+ openUrl(target);
5233
+ tui.print(`${c.gray}\u2192 opened ${target} to manage your plan${c.reset}`);
5234
+ return;
5235
+ }
5236
+ if (quote.current >= quote.max) {
5237
+ tui.print(`${c.yellow}You're at the maximum of ${quote.max} parallel sessions.${c.reset}`);
5238
+ return;
5239
+ }
5240
+ tui.print(renderUpgradePanel(quote));
5241
+ const cost = fmtCost(quote);
5242
+ const confirmLabel = quote.ends_trial ? `End trial & start now${cost ? ` \u2014 pay ${cost} today` : ""}` : `Confirm${cost ? ` \u2014 pay ${cost} now` : " \u2014 add the session"}`;
5243
+ const choice = await tui.select(`Add a ${ordinal(quote.sessions)} parallel session?`, [
5244
+ { label: confirmLabel, value: "go" },
5245
+ { label: "Not now", value: "no" }
5246
+ ]);
5247
+ if (choice !== "go") {
5248
+ tui.print(`${c.gray}No change made \u2014 you can upgrade anytime with /upgrade.${c.reset}`);
5249
+ return;
5250
+ }
5251
+ tui.print(`${c.gray}Applying\u2026${c.reset}`);
5252
+ let applied;
5253
+ try {
5254
+ applied = await api.sessionsUpgrade(threadId, quote.sessions);
5255
+ } catch (e) {
5256
+ tui.print(`${c.red}\u2717${c.reset} Upgrade failed: ${e instanceof Error ? e.message : String(e)}`);
5257
+ return;
5258
+ }
5259
+ if (!applied?.ok) {
5260
+ tui.print(`${c.red}\u2717${c.reset} Upgrade failed: ${applied?.error ?? "unknown error"}`);
5261
+ return;
5262
+ }
5263
+ const n = applied.sessions ?? quote.sessions;
5264
+ tui.print(`${c.green}\u2713${c.reset} ${c.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c.reset}`);
5265
+ if (opts.auto && lastSent) {
5266
+ tui.print(`${c.gray}Continuing\u2026${c.reset}`);
5267
+ await sendNow(lastSent.text, lastSent.images);
5268
+ }
5269
+ } finally {
5270
+ upgradeInFlight = false;
5271
+ }
5272
+ };
5273
+ const isSessionLimitError = (msg) => /thread limit reached|simultaneous (thread|session)s?.*in use|no active standard code subscription/i.test(msg);
5137
5274
  const skillsCtl = {
5138
5275
  list: () => api.listSkills(),
5139
5276
  setEnabled: (name, enabled) => api.setSkillEnabled(name, enabled),
@@ -5185,6 +5322,18 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5185
5322
  hint: "list / install / manage",
5186
5323
  run: () => runSkillsMenu(tui, skillsCtl)
5187
5324
  },
5325
+ {
5326
+ name: "account",
5327
+ label: "Manage your account",
5328
+ hint: "billing & sessions, opens in browser",
5329
+ run: () => runAccountCommand()
5330
+ },
5331
+ {
5332
+ name: "upgrade",
5333
+ label: "Add a parallel session",
5334
+ hint: "run more sessions at once",
5335
+ run: () => offerUpgrade()
5336
+ },
5188
5337
  { name: "background", label: "Background processes", hint: "list / stop", run: () => runProcessMenu(tui, bgMgr) },
5189
5338
  { name: "keybindings", label: "Keyboard shortcuts", run: () => showKeybindings(tui) },
5190
5339
  { name: "update", label: "Check for updates", hint: "check for a newer version", run: () => runUpdateCommand(tui) },
@@ -5285,6 +5434,11 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5285
5434
  if (shownIds.has(m.id) || m.status === "pending") continue;
5286
5435
  shownIds.add(m.id);
5287
5436
  const text = messageText(m.content).trim();
5437
+ const denial = typeof m.error === "string" && m.error || text;
5438
+ if (denial && isSessionLimitError(denial)) {
5439
+ void offerUpgrade({ auto: true });
5440
+ continue;
5441
+ }
5288
5442
  if (m.role === "assistant" && text) printAssistant(tui, text);
5289
5443
  else if (m.role === "system" && text) tui.print(`${c.dim}${text}${c.reset}`);
5290
5444
  else if (m.role === "user" && text) {
@@ -5523,7 +5677,7 @@ async function runApprovalsMenu(tui, perm, save) {
5523
5677
  }
5524
5678
  const items = [
5525
5679
  ...tools.map((t) => ({ label: `Tool: ${t}`, hint: "always allowed", value: `tool:${t}` })),
5526
- ...risks.map((r) => ({ label: `All level ${r} risk`, hint: "always allowed", value: `risk:${r}` })),
5680
+ ...risks.map((r) => ({ label: `Risk level ${r} and below`, hint: "auto-accepted", value: `risk:${r}` })),
5527
5681
  { label: "Clear all approvals", hint: "", value: "clear" }
5528
5682
  ];
5529
5683
  const picked = await tui.select(