@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 +6 -5
- package/dist/index.js +231 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
95
|
-
|
|
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
|
|
100
|
-
but this override does not change the saved default endpoint.
|
|
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.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
/**
|
|
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
|
|
75
|
-
|
|
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 += `${
|
|
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 += `${
|
|
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("-"))
|
|
420
|
-
|
|
421
|
-
out += `${CYAN2}${w}${RESET}`;
|
|
531
|
+
if (expectProgram && !w.startsWith("-")) {
|
|
532
|
+
out += w;
|
|
422
533
|
expectProgram = false;
|
|
423
|
-
} else
|
|
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 ?? "").
|
|
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)
|
|
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
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
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
|
|
2504
|
+
/** Plain context label (no ANSI) for menu hints, or "" when unknown. */
|
|
2380
2505
|
contextPctLabel() {
|
|
2381
|
-
return this.contextPct == null ? "" : `
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
2561
|
+
line = tk ? `${done} ${tk}` : done;
|
|
2562
|
+
} else {
|
|
2563
|
+
line = tk ? tk : null;
|
|
2432
2564
|
}
|
|
2433
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
4016
|
-
`);
|
|
4175
|
+
explainFailure(check2, "Browser sign-in didn't verify: ");
|
|
4017
4176
|
continue;
|
|
4018
4177
|
}
|
|
4019
4178
|
api = new ApiClient(endpoint, token);
|
|
4020
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
}
|