@standardagents/code 0.1.3 → 0.3.0

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/README.md CHANGED
@@ -91,14 +91,15 @@ standardcode --endpoint https://your-instance.example.com <dir>
91
91
  standardcode -e -- <dir> # prompt for the endpoint for this run
92
92
  ```
93
93
 
94
- On first run it asks for your Standard Agents instance URL and an API token (stored in
95
- `~/.standardagents/credentials`). It then offers to **resume** a session tagged for this
94
+ On first run it connects to the hosted Standard Code instance
95
+ (`https://api.standardcode.ai`) and walks you through a one-time browser sign-in — press
96
+ Enter, approve in the browser, and your key is stored in `~/.standardagents/credentials`.
97
+ You only do this once per machine. It then offers to **resume** a session tagged for this
96
98
  project + machine, or **start a new one**.
97
99
 
98
100
  Use `--endpoint [url]` or `-e [url]` to point a single CLI run at a different Standard Agents
99
- instance. If you omit the URL, the CLI prompts for it. Tokens are still remembered per endpoint,
100
- but this override does not change the saved default endpoint. Run without `--endpoint` to use the
101
- default endpoint or be prompted for one.
101
+ instance (local dev, self-hosted). If you omit the URL, the CLI prompts for it. Tokens are
102
+ remembered per endpoint, but this override does not change the saved default endpoint.
102
103
 
103
104
  ### Requirements
104
105
 
package/dist/index.js CHANGED
@@ -10,6 +10,69 @@ import crypto from 'crypto';
10
10
  import readline from 'readline';
11
11
 
12
12
  // src/api.ts
13
+ function classifyConnectError(err, endpoint) {
14
+ const host = endpoint.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
15
+ const codes = /* @__PURE__ */ new Set();
16
+ const messages = [];
17
+ for (let e = err; e; e = e.cause) {
18
+ if (typeof e.code === "string") codes.add(e.code);
19
+ if (typeof e.message === "string") messages.push(e.message);
20
+ if (Array.isArray(e.errors)) {
21
+ for (const sub of e.errors) {
22
+ if (typeof sub?.code === "string") codes.add(sub.code);
23
+ if (typeof sub?.message === "string") messages.push(sub.message);
24
+ }
25
+ }
26
+ }
27
+ const text = messages.join(" | ");
28
+ const has = (...wanted) => wanted.some((w) => codes.has(w));
29
+ if (has("ENOTFOUND", "EAI_AGAIN")) {
30
+ return {
31
+ ok: false,
32
+ reason: `Can't resolve ${host} \u2014 no such hostname from this machine.`,
33
+ hint: "The endpoint URL is probably wrong (typo, or that server isn't running). Check the URL, your VPN, and local DNS."
34
+ };
35
+ }
36
+ if (has("ECONNREFUSED")) {
37
+ return {
38
+ ok: false,
39
+ reason: `${host} refused the connection \u2014 nothing is listening there.`,
40
+ hint: "The instance looks stopped. Start it (or check the port in the endpoint URL)."
41
+ };
42
+ }
43
+ if (has("EHOSTUNREACH", "ENETUNREACH", "ENETDOWN")) {
44
+ return {
45
+ ok: false,
46
+ reason: `No route to ${host} \u2014 the network looks down or unreachable.`,
47
+ hint: "Check your internet connection / VPN."
48
+ };
49
+ }
50
+ if (has("ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT") || err?.name === "TimeoutError") {
51
+ return {
52
+ ok: false,
53
+ reason: `${host} didn't answer in time.`,
54
+ hint: "The server may be overloaded or a firewall is eating the traffic. Try again."
55
+ };
56
+ }
57
+ if (has(
58
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
59
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
60
+ "SELF_SIGNED_CERT_IN_CHAIN",
61
+ "CERT_HAS_EXPIRED",
62
+ "ERR_TLS_CERT_ALTNAME_INVALID"
63
+ ) || /certificate|TLS|SSL/i.test(text)) {
64
+ return {
65
+ ok: false,
66
+ reason: `TLS problem talking to ${host}: ${text.slice(0, 120)}`,
67
+ hint: "For local/self-signed endpoints, use the http:// URL or trust the local CA."
68
+ };
69
+ }
70
+ return {
71
+ ok: false,
72
+ reason: `Couldn't reach ${host}${text ? ` (${text.slice(0, 120)})` : ""}.`,
73
+ hint: "This is a connection problem, not a token problem \u2014 check the endpoint URL and your network."
74
+ };
75
+ }
13
76
  var ApiClient = class {
14
77
  constructor(endpoint, token) {
15
78
  this.endpoint = endpoint;
@@ -47,12 +110,54 @@ var ApiClient = class {
47
110
  }
48
111
  }
49
112
  async verify() {
113
+ return (await this.verifyDetailed()).ok;
114
+ }
115
+ /**
116
+ * Check the endpoint + token and say PRECISELY what's wrong when they
117
+ * fail. "That token didn't work" is a lie when the real problem is a typo'd
118
+ * hostname, a stopped server, or a dead network — sign-in errors must let
119
+ * the user tell those apart from an actually-rejected key.
120
+ */
121
+ async verifyDetailed() {
122
+ let res;
50
123
  try {
51
- await this.json("/api/auth/me");
52
- return true;
53
- } catch {
54
- return false;
124
+ res = await fetch(`${this.endpoint}/api/auth/me`, {
125
+ headers: { Authorization: `Bearer ${this.token}` },
126
+ signal: AbortSignal.timeout(15e3)
127
+ });
128
+ } catch (err) {
129
+ return classifyConnectError(err, this.endpoint);
130
+ }
131
+ if (res.ok) return { ok: true };
132
+ if (res.status === 401) {
133
+ return {
134
+ ok: false,
135
+ reason: "The instance rejected this token \u2014 it's invalid, expired, or was revoked.",
136
+ hint: "Create a fresh API key in the instance settings (or press Enter to sign in with your browser). Keys only work on the instance that minted them."
137
+ };
55
138
  }
139
+ if (res.status === 403) {
140
+ return {
141
+ ok: false,
142
+ reason: "The token was recognized, but this account isn't allowed to use the API here.",
143
+ hint: "Ask an instance admin to grant access, or use a different account's key."
144
+ };
145
+ }
146
+ if (res.status === 404 || res.status === 405) {
147
+ return {
148
+ ok: false,
149
+ reason: `${this.endpoint} answered, but it doesn't look like a Standard Agents instance (no /api/auth/me).`,
150
+ hint: "Double-check the endpoint URL \u2014 this may be a different service."
151
+ };
152
+ }
153
+ if (res.status >= 500) {
154
+ return {
155
+ ok: false,
156
+ reason: `The instance hit an error while checking the token (HTTP ${res.status}).`,
157
+ hint: "That's a server-side problem \u2014 check the instance's logs and try again."
158
+ };
159
+ }
160
+ return { ok: false, reason: `The instance answered HTTP ${res.status} while checking the token.` };
56
161
  }
57
162
  /** Agent name → display title map (best effort; for labeling subagents). */
58
163
  async listAgents() {
@@ -69,12 +174,22 @@ var ApiClient = class {
69
174
  if (!id) throw new Error("Thread create returned no id");
70
175
  return id;
71
176
  }
72
- /** List threads for an agent, optionally filtering to those carrying all given tags. */
177
+ /**
178
+ * List threads for an agent (or several variants of it), optionally
179
+ * filtering to those carrying all given tags. Multiple ids exist because
180
+ * the instance's model gearbox hands a session between agent variants — a
181
+ * thread that last ran in another gear must still show up for resume.
182
+ */
73
183
  async listThreads(agentId, requireTags) {
74
- const res = await this.json(
75
- `/api/threads?agent_id=${encodeURIComponent(agentId)}&limit=100`
184
+ const ids = Array.isArray(agentId) ? agentId : [agentId];
185
+ const pages = await Promise.all(
186
+ ids.map(
187
+ (id) => this.json(
188
+ `/api/threads?agent_id=${encodeURIComponent(id)}&limit=100`
189
+ ).catch(() => [])
190
+ )
76
191
  );
77
- const arr = Array.isArray(res) ? res : res.threads || [];
192
+ const arr = pages.flatMap((res) => Array.isArray(res) ? res : res.threads || []);
78
193
  return arr.map((t) => ({
79
194
  id: t.id,
80
195
  tags: Array.isArray(t.tags) ? t.tags : [],
@@ -384,9 +499,6 @@ function newFileLines(content) {
384
499
  return out;
385
500
  }
386
501
  function highlightBash(cmd) {
387
- const CYAN2 = "\x1B[36m";
388
- const GREEN = "\x1B[32m";
389
- const MAGENTA = "\x1B[35m";
390
502
  let out = "";
391
503
  let i = 0;
392
504
  let expectProgram = true;
@@ -398,7 +510,7 @@ function highlightBash(cmd) {
398
510
  if (ch === '"' && cmd[j] === "\\") j++;
399
511
  j++;
400
512
  }
401
- out += `${GREEN}${cmd.slice(i, Math.min(j + 1, cmd.length))}${RESET}`;
513
+ out += `${DIM}${cmd.slice(i, Math.min(j + 1, cmd.length))}${RESET}`;
402
514
  i = j + 1;
403
515
  continue;
404
516
  }
@@ -408,7 +520,7 @@ function highlightBash(cmd) {
408
520
  }
409
521
  const op = cmd.slice(i).match(/^(\|\||&&|\||;|>>|>|<)/);
410
522
  if (op) {
411
- out += `${MAGENTA}${op[1]}${RESET}`;
523
+ out += `${DIM}${op[1]}${RESET}`;
412
524
  i += op[1].length;
413
525
  expectProgram = true;
414
526
  continue;
@@ -416,19 +528,24 @@ function highlightBash(cmd) {
416
528
  const word = cmd.slice(i).match(/^[^\s'"#|;&<>]+/);
417
529
  if (word) {
418
530
  const w = word[0];
419
- if (w.startsWith("-")) out += `${DIM}${w}${RESET}`;
420
- else if (expectProgram) {
421
- out += `${CYAN2}${w}${RESET}`;
531
+ if (expectProgram && !w.startsWith("-")) {
532
+ out += w;
422
533
  expectProgram = false;
423
- } else out += w;
534
+ } else {
535
+ out += `${DIM}${w}${RESET}`;
536
+ }
424
537
  i += w.length;
425
538
  continue;
426
539
  }
427
- out += ch;
540
+ out += `${DIM}${ch}${RESET}`;
428
541
  i++;
429
542
  }
430
543
  return out;
431
544
  }
545
+ function oneLineCommand(cmd, max) {
546
+ const collapsed = cmd.replace(/\s*\n+\s*/g, " \u23CE ").replace(/[ \t]{2,}/g, " ").trim();
547
+ return clamp(collapsed, max);
548
+ }
432
549
  var GRAD_STOPS = [
433
550
  [255, 179, 92],
434
551
  // amber
@@ -664,7 +781,7 @@ var Bridge = class {
664
781
  this.hooks.onStatus?.(callKey, null);
665
782
  }
666
783
  if (result.ok) {
667
- const display = req.tool === "bash" ? `bash: ${highlightBash(String(req.args.command ?? "").slice(0, 200))}` : summary;
784
+ const display = req.tool === "bash" ? `bash: ${highlightBash(oneLineCommand(String(req.args.command ?? ""), Math.max(40, (process.stdout.columns || 80) - 20)))}` : summary;
668
785
  let detail;
669
786
  if (req.tool === "edit_file") {
670
787
  detail = diffLines(String(req.args.old_string ?? ""), String(req.args.new_string ?? ""));
@@ -713,7 +830,7 @@ function describe(req) {
713
830
  case "run_skill_script":
714
831
  return `skill ${a.skill}: run ${a.entry}`;
715
832
  case "bash":
716
- return `bash: ${String(a.command).slice(0, 80)}`;
833
+ return `bash: ${oneLineCommand(String(a.command ?? ""), 80)}`;
717
834
  default:
718
835
  return `${req.tool} ${JSON.stringify(a).slice(0, 80)}`;
719
836
  }
@@ -1195,6 +1312,9 @@ ${truncated}`
1195
1312
  const command = String(args.command || "");
1196
1313
  if (!command.trim()) return { ok: false, error: "command is required" };
1197
1314
  const cwd = args.cwd ? this.resolve(String(args.cwd)) : this.projectDir;
1315
+ if (!fs4.existsSync(cwd)) {
1316
+ return { ok: false, error: `cwd does not exist: ${cwd}` };
1317
+ }
1198
1318
  const id = crypto.randomUUID().slice(0, 8);
1199
1319
  const logPath = path3.join(LOG_DIR, `${id}.log`);
1200
1320
  let out;
@@ -1211,15 +1331,22 @@ ${truncated}`
1211
1331
  fs4.closeSync(out);
1212
1332
  return { ok: false, error: `Failed to start: ${err instanceof Error ? err.message : String(err)}` };
1213
1333
  }
1334
+ let spawnError = null;
1335
+ child.on("error", (err) => {
1336
+ spawnError = err;
1337
+ });
1214
1338
  fs4.closeSync(out);
1215
1339
  const pid = child.pid;
1216
- if (!pid) return { ok: false, error: "Process failed to start (no pid)." };
1217
1340
  let earlyExit;
1218
1341
  const onEarlyExit = (code) => {
1219
1342
  earlyExit = code;
1220
1343
  };
1221
1344
  child.on("exit", onEarlyExit);
1222
1345
  await new Promise((r) => setTimeout(r, STARTUP_GRACE_MS));
1346
+ if (spawnError) {
1347
+ return { ok: false, error: `Failed to start: ${spawnError.message}` };
1348
+ }
1349
+ if (!pid) return { ok: false, error: "Process failed to start (no pid)." };
1223
1350
  if (earlyExit !== void 0 || !isAlive2(pid)) {
1224
1351
  const tail = await readLogTail(logPath, 15);
1225
1352
  const code = earlyExit ?? "unknown";
@@ -2346,24 +2473,22 @@ var Tui = class _Tui {
2346
2473
  spinnerFrame() {
2347
2474
  return `${C.bold}${C.cyan}${FRAMES[Math.floor(Date.now() / 100) % FRAMES.length]}${C.reset}`;
2348
2475
  }
2349
- /**
2350
- * "↑X ↓Y" cumulative token totals (greyed — low-priority), plus a context
2351
- * window gauge "ctx N%" when known. The gauge colour ramps with fill (green →
2352
- * yellow → red) so the user can see compaction approaching at a glance.
2353
- */
2476
+ /** "↑X ↓Y" cumulative token totals (greyed — low-priority). */
2354
2477
  tokensText() {
2355
2478
  const parts = [];
2356
2479
  if (this.tokensIn > 0) parts.push(`\u2191${this.fmtTokens(this.tokensIn)}`);
2357
2480
  if (this.tokensOut > 0) parts.push(`\u2193${this.fmtTokens(this.tokensOut)}`);
2358
- let out = parts.length ? `${C.gray}${parts.join(" ")}${C.reset}` : "";
2359
- if (this.contextPct != null) {
2360
- const pct = this.contextPct;
2361
- const calmGreen = "\x1B[38;5;65m";
2362
- const col = pct >= 85 ? C.red : pct >= 70 ? C.yellow : calmGreen;
2363
- const gauge = `${col}ctx ${pct}%${C.reset}`;
2364
- out = out ? `${out} ${gauge}` : gauge;
2365
- }
2366
- return out;
2481
+ return parts.length ? `${C.gray}${parts.join(" ")}${C.reset}` : "";
2482
+ }
2483
+ /**
2484
+ * Understated context gauge, right-aligned on the summary line: just "N%",
2485
+ * dim, and INVISIBLE until the conversation is more than 85% of the way to
2486
+ * the compaction point (the percentage is scaled so 100% = where background
2487
+ * compaction triggers, not the raw model window). Most sessions never see it.
2488
+ */
2489
+ contextGaugeText() {
2490
+ if (this.contextPct == null || this.contextPct <= 85) return "";
2491
+ return `${C.dim}${C.gray}${this.contextPct}%${C.reset}`;
2367
2492
  }
2368
2493
  /**
2369
2494
  * Set the context-window fill percentage (0–100), or null to hide it.
@@ -2376,9 +2501,9 @@ var Tui = class _Tui {
2376
2501
  this.contextPct = next;
2377
2502
  this.renderBottom();
2378
2503
  }
2379
- /** Plain "ctx N%" label (no ANSI) for menu hints, or "" when unknown. */
2504
+ /** Plain context label (no ANSI) for menu hints, or "" when unknown. */
2380
2505
  contextPctLabel() {
2381
- return this.contextPct == null ? "" : `ctx ${this.contextPct}%`;
2506
+ return this.contextPct == null ? "" : `context ${this.contextPct}%`;
2382
2507
  }
2383
2508
  /** Register the slash commands shown in the inline `/` palette. */
2384
2509
  setCommands(commands) {
@@ -2413,24 +2538,34 @@ var Tui = class _Tui {
2413
2538
  */
2414
2539
  statusLineText(cols2) {
2415
2540
  const tk = this.tokensText();
2541
+ const gauge = this.contextGaugeText();
2542
+ const gaugeReserve = gauge ? this.visibleWidth(gauge) + 1 : 0;
2543
+ let line;
2416
2544
  if (this.working) {
2417
2545
  const el = this.formatElapsed(Date.now() - this.workingStart);
2418
2546
  const right = `${C.dim}${el}${C.reset}${tk ? " " + tk : ""}`;
2419
2547
  const head = `${this.spinnerFrame()} ${C.bold}Working${C.reset}`;
2420
- const avail = Math.max(0, cols2 - this.visibleWidth(head) - this.visibleWidth(right) - 2);
2548
+ const avail = Math.max(
2549
+ 0,
2550
+ cols2 - this.visibleWidth(head) - this.visibleWidth(right) - 2 - gaugeReserve
2551
+ );
2421
2552
  let stepPart = "";
2422
2553
  if (this.step && avail > 1) {
2423
2554
  let s = this.step;
2424
2555
  if (s.length > avail) s = s.slice(0, avail - 1) + "\u2026";
2425
2556
  stepPart = ` ${C.dim}${s}${C.reset}`;
2426
2557
  }
2427
- return `${head}${stepPart} ${right}`;
2428
- }
2429
- if (this.goalComplete) {
2558
+ line = `${head}${stepPart} ${right}`;
2559
+ } else if (this.goalComplete) {
2430
2560
  const done = `${C.bold}${gradientText("\u2713 Goal complete.")}${C.reset}`;
2431
- return tk ? `${done} ${tk}` : done;
2561
+ line = tk ? `${done} ${tk}` : done;
2562
+ } else {
2563
+ line = tk ? tk : null;
2432
2564
  }
2433
- return tk ? tk : null;
2565
+ if (!gauge) return line;
2566
+ const base = line ?? "";
2567
+ const pad = Math.max(1, cols2 - 1 - this.visibleWidth(base) - this.visibleWidth(gauge));
2568
+ return `${base}${" ".repeat(pad)}${gauge}`;
2434
2569
  }
2435
2570
  /** The prompt line prefix (with ANSI colour) that precedes the typed text. */
2436
2571
  promptPrefix() {
@@ -3720,6 +3855,8 @@ function saveDefaultEndpoint(endpoint) {
3720
3855
 
3721
3856
  // src/index.ts
3722
3857
  var AGENT_ID = "standard_code_agent";
3858
+ var AGENT_ID_VARIANTS = [AGENT_ID, "standard_code_high_agent"];
3859
+ var PRODUCTION_ENDPOINT = "https://api.standardcode.ai";
3723
3860
  var c = {
3724
3861
  reset: "\x1B[0m",
3725
3862
  dim: "\x1B[2m",
@@ -3753,7 +3890,8 @@ function printUsage() {
3753
3890
  " standardcode [options] [dir]",
3754
3891
  "",
3755
3892
  `${c.bold}Options${c.reset}`,
3756
- " -e, --endpoint [url] Use a Standard Agents instance for this run only.",
3893
+ " -e, --endpoint [url] Use a different Standard Agents instance for this run",
3894
+ " (default: https://api.standardcode.ai).",
3757
3895
  " If url is omitted, prompt for it.",
3758
3896
  " Credentials are remembered for that endpoint, but",
3759
3897
  " the saved default endpoint is not changed.",
@@ -3967,7 +4105,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
3967
4105
  };
3968
4106
  process.on("SIGINT", onPreflightSigint);
3969
4107
  let endpointPrompted = false;
3970
- let endpoint = endpointArg || (cliArgs.promptEndpoint ? "" : defaultEndpoint() || "");
4108
+ let endpoint = endpointArg || (cliArgs.promptEndpoint ? "" : defaultEndpoint() || PRODUCTION_ENDPOINT);
3971
4109
  if (!endpoint) {
3972
4110
  endpoint = await askEndpoint();
3973
4111
  endpointPrompted = true;
@@ -3982,19 +4120,40 @@ ${c.dim}Press Control-C again to exit${c.reset}
3982
4120
  }
3983
4121
  const stored = getCredential(endpoint);
3984
4122
  let api = stored ? new ApiClient(endpoint, stored.access_token) : null;
3985
- if (!api || !await api.verify()) {
4123
+ const storedCheck = api ? await api.verifyDetailed() : null;
4124
+ if (!api || !storedCheck?.ok) {
3986
4125
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
4126
+ if (storedCheck && !storedCheck.ok) {
4127
+ stdout.write(`${c.red}\u2717${c.reset} ${c.dim}Saved sign-in for this endpoint failed:${c.reset} ${storedCheck.reason}
4128
+ `);
4129
+ if (storedCheck.hint) stdout.write(` ${c.dim}${storedCheck.hint}${c.reset}
4130
+ `);
4131
+ stdout.write("\n");
4132
+ }
4133
+ const explainFailure = (result, prefix) => {
4134
+ stdout.write(`${c.red}\u2717${c.reset} ${prefix}${result.reason}
4135
+ `);
4136
+ if (result.hint) stdout.write(` ${c.dim}${result.hint}${c.reset}
4137
+ `);
4138
+ };
4139
+ stdout.write(`${c.bold}${gradientText("Sign in to Standard Code")}${c.reset}
4140
+ `);
4141
+ if (`https://${host}` !== PRODUCTION_ENDPOINT) {
4142
+ stdout.write(`${c.dim}Connecting to${c.reset} ${c.teal}${host}${c.reset}
4143
+ `);
4144
+ }
3987
4145
  stdout.write(
3988
- `${c.bold}${c.white}Sign in${c.reset} ${c.dim}\u2014 connect to${c.reset} ${c.teal}${host}${c.reset}
4146
+ `${c.dim}You'll only need to do this once on this machine.${c.reset}
4147
+
3989
4148
  `
3990
4149
  );
3991
4150
  stdout.write(
3992
- `${c.dim}Press Enter to sign in with your browser, or paste an API token.${c.reset}
4151
+ `${c.white}Press ${c.bold}Enter${c.reset}${c.white} to open your browser and sign in.${c.reset} ${c.dim}(or paste an API token)${c.reset}
3993
4152
 
3994
4153
  `
3995
4154
  );
3996
4155
  for (; ; ) {
3997
- const token = (await ask(`${c.teal}\u276F${c.reset} ${c.dim}token (or Enter for browser)${c.reset} `)).trim();
4156
+ const token = (await ask(`${c.teal}\u276F${c.reset} `)).trim();
3998
4157
  if (!token) {
3999
4158
  const got = await deviceLogin(endpoint).catch((e) => {
4000
4159
  stdout.write(`${c.red}\u2717${c.reset} ${c.dim}${e instanceof Error ? e.message : String(e)}${c.reset}
@@ -4003,7 +4162,8 @@ ${c.dim}Press Control-C again to exit${c.reset}
4003
4162
  });
4004
4163
  if (!got) continue;
4005
4164
  api = new ApiClient(endpoint, got);
4006
- if (await api.verify()) {
4165
+ const check2 = await api.verifyDetailed();
4166
+ if (check2.ok) {
4007
4167
  saveCredential(
4008
4168
  { endpoint, access_token: got, token_type: "Bearer", saved_at: Date.now() },
4009
4169
  { updateDefault: !endpointOverride }
@@ -4012,12 +4172,12 @@ ${c.dim}Press Control-C again to exit${c.reset}
4012
4172
  `);
4013
4173
  break;
4014
4174
  }
4015
- stdout.write(`${c.red}\u2717${c.reset} ${c.dim}Browser sign-in didn't verify. Try again.${c.reset}
4016
- `);
4175
+ explainFailure(check2, "Browser sign-in didn't verify: ");
4017
4176
  continue;
4018
4177
  }
4019
4178
  api = new ApiClient(endpoint, token);
4020
- if (await api.verify()) {
4179
+ const check = await api.verifyDetailed();
4180
+ if (check.ok) {
4021
4181
  saveCredential(
4022
4182
  { endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
4023
4183
  { updateDefault: !endpointOverride }
@@ -4026,8 +4186,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
4026
4186
  `);
4027
4187
  break;
4028
4188
  }
4029
- stdout.write(`${c.red}\u2717${c.reset} ${c.dim}That token didn't work. Try again.${c.reset}
4030
- `);
4189
+ explainFailure(check, "");
4031
4190
  }
4032
4191
  } else if (endpointPrompted) {
4033
4192
  saveDefaultEndpoint(endpoint);
@@ -4038,7 +4197,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
4038
4197
  const tags = [`path:${projectDir}`, `machine:${machine}`];
4039
4198
  let existing = [];
4040
4199
  try {
4041
- existing = await api.listThreads(AGENT_ID, tags);
4200
+ existing = await api.listThreads(AGENT_ID_VARIANTS, tags);
4042
4201
  } catch {
4043
4202
  existing = [];
4044
4203
  }
@@ -4100,9 +4259,13 @@ async function deviceLogin(endpoint) {
4100
4259
  const start = await fetch(`${endpoint}/api/auth/device/start`, { method: "POST" });
4101
4260
  if (!start.ok) throw new Error(`This instance does not support browser sign-in (HTTP ${start.status}). Paste an API token instead.`);
4102
4261
  const info = await start.json();
4103
- stdout.write(`${c.dim}Opening your browser to approve this sign-in\u2026${c.reset}
4262
+ stdout.write(`${c.dim}Opening your browser to sign in. If it doesn't open, visit:${c.reset}
4263
+ `);
4264
+ stdout.write(`
4265
+ ${c.teal}${info.verify_url}${c.reset}
4266
+
4104
4267
  `);
4105
- stdout.write(`${c.dim}If it doesn't open, visit:${c.reset} ${c.teal}${info.verify_url}${c.reset}
4268
+ stdout.write(`${c.dim}Waiting for sign-in to complete\u2026 (Ctrl-C to cancel)${c.reset}
4106
4269
  `);
4107
4270
  openUrl(info.verify_url);
4108
4271
  const deadline = Date.now() + (info.expires_in ?? 600) * 1e3;
@@ -4293,9 +4456,11 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4293
4456
  const subs = await api.listSubagents(threadId);
4294
4457
  activeSubagents.clear();
4295
4458
  for (const s of subs) {
4296
- if (s.status !== "running") continue;
4459
+ const status = (s.status || "").trim();
4460
+ if (status === "idle" || status === "terminated") continue;
4461
+ const detail = status && status !== "running" ? ` \u2014 ${status.slice(0, 80)}` : "";
4297
4462
  activeSubagents.set(s.id, {
4298
- label: subagentLabel(s, agentTitles),
4463
+ label: `${subagentLabel(s, agentTitles)}${detail}`,
4299
4464
  agentName: s.agent_name ?? void 0
4300
4465
  });
4301
4466
  }
@@ -4331,6 +4496,10 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4331
4496
  if (activeSubagents.has(id)) scheduleReconcile();
4332
4497
  }
4333
4498
  });
4499
+ const heartbeatPoll = setInterval(() => {
4500
+ if (activeSubagents.size > 0) scheduleReconcile();
4501
+ }, 5e3);
4502
+ heartbeatPoll.unref();
4334
4503
  const quit = async () => {
4335
4504
  tui.end();
4336
4505
  const stopped = api.stop(threadId).catch(() => {
@@ -4611,10 +4780,11 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
4611
4780
  refreshStatus();
4612
4781
  } catch {
4613
4782
  }
4783
+ const COMPACTION_TRIGGER_FRACTION = 0.5;
4614
4784
  try {
4615
4785
  const cu = await api.kvGet(threadId, "context_usage");
4616
4786
  const used = Number(cu?.inputTokens) || 0;
4617
- const max = Number(cu?.maxContextTokens) || 0;
4787
+ const max = (Number(cu?.maxContextTokens) || 0) * COMPACTION_TRIGGER_FRACTION;
4618
4788
  tui.setContextPct(max > 0 && used > 0 ? used / max * 100 : null);
4619
4789
  } catch {
4620
4790
  }