@standardagents/code 0.10.1 → 0.11.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/dist/index.js +1130 -539
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -356,9 +356,13 @@ var ApiClient = class {
|
|
|
356
356
|
).catch(() => [])
|
|
357
357
|
)
|
|
358
358
|
]);
|
|
359
|
-
const arr = pages.flatMap((res) =>
|
|
359
|
+
const arr = pages.flatMap((res, i) => {
|
|
360
|
+
const list = Array.isArray(res) ? res : res.threads || [];
|
|
361
|
+
return list.map((t) => ({ ...t, agent_id: t.agent_id ?? ids[i] }));
|
|
362
|
+
});
|
|
360
363
|
return arr.map((t) => ({
|
|
361
364
|
id: t.id,
|
|
365
|
+
agent_id: t.agent_id,
|
|
362
366
|
tags: Array.isArray(t.tags) ? t.tags : [],
|
|
363
367
|
created_at: t.created_at,
|
|
364
368
|
title: t.title,
|
|
@@ -1026,7 +1030,7 @@ function truncateMiddle(text, maxColumns) {
|
|
|
1026
1030
|
const headBudget = Math.ceil(available / 2);
|
|
1027
1031
|
const tailBudget = Math.floor(available / 2);
|
|
1028
1032
|
const head = [];
|
|
1029
|
-
const
|
|
1033
|
+
const tail2 = [];
|
|
1030
1034
|
let headWidth = 0;
|
|
1031
1035
|
let tailWidth = 0;
|
|
1032
1036
|
for (let i = 0; i < chars.length; i++) {
|
|
@@ -1038,12 +1042,12 @@ function truncateMiddle(text, maxColumns) {
|
|
|
1038
1042
|
for (let i = chars.length - 1; i >= head.length; i--) {
|
|
1039
1043
|
const width = terminalGlyphWidth(chars[i]);
|
|
1040
1044
|
if (tailWidth + width > tailBudget) break;
|
|
1041
|
-
|
|
1045
|
+
tail2.unshift(chars[i]);
|
|
1042
1046
|
tailWidth += width;
|
|
1043
1047
|
}
|
|
1044
1048
|
const isSeparator = (ch) => ch === "/" || ch === "\\";
|
|
1045
1049
|
let headEnd = head.length;
|
|
1046
|
-
let tailStart = chars.length -
|
|
1050
|
+
let tailStart = chars.length - tail2.length;
|
|
1047
1051
|
for (let i = headEnd - 1; i >= 0; i--) {
|
|
1048
1052
|
if (isSeparator(chars[i])) {
|
|
1049
1053
|
headEnd = i + 1;
|
|
@@ -1059,7 +1063,7 @@ function truncateMiddle(text, maxColumns) {
|
|
|
1059
1063
|
if (headEnd < tailStart && headEnd > 0 && tailStart < chars.length) {
|
|
1060
1064
|
return `${chars.slice(0, headEnd).join("")}\u2026${chars.slice(tailStart).join("")}`;
|
|
1061
1065
|
}
|
|
1062
|
-
return `${head.join("")}\u2026${
|
|
1066
|
+
return `${head.join("")}\u2026${tail2.join("")}`;
|
|
1063
1067
|
}
|
|
1064
1068
|
function clamp(s, max) {
|
|
1065
1069
|
return s.length > max ? s.slice(0, Math.max(0, max - 1)) + "\u2026" : s;
|
|
@@ -2164,14 +2168,14 @@ ${truncated}`
|
|
|
2164
2168
|
}
|
|
2165
2169
|
if (!pid) return { ok: false, error: "Process failed to start (no pid)." };
|
|
2166
2170
|
if (earlyExit !== void 0 || !isAlive2(pid)) {
|
|
2167
|
-
const
|
|
2171
|
+
const tail2 = await readLogTail(logPath, 15);
|
|
2168
2172
|
const code = earlyExit ?? "unknown";
|
|
2169
2173
|
return {
|
|
2170
2174
|
ok: false,
|
|
2171
|
-
error: `The process exited immediately (exit code ${code}) \u2014 it did not stay running, so nothing was started or tracked.` + (
|
|
2175
|
+
error: `The process exited immediately (exit code ${code}) \u2014 it did not stay running, so nothing was started or tracked.` + (tail2 ? `
|
|
2172
2176
|
|
|
2173
2177
|
Output:
|
|
2174
|
-
${
|
|
2178
|
+
${tail2}` : " No output was captured.")
|
|
2175
2179
|
};
|
|
2176
2180
|
}
|
|
2177
2181
|
child.removeListener("exit", onEarlyExit);
|
|
@@ -2288,8 +2292,8 @@ ${tail}` : " No output was captured.")
|
|
|
2288
2292
|
} catch {
|
|
2289
2293
|
return { ok: true, result: `(no output captured yet for ${id})` };
|
|
2290
2294
|
}
|
|
2291
|
-
const
|
|
2292
|
-
return { ok: true, result:
|
|
2295
|
+
const tail2 = content.split("\n").slice(-maxLines).join("\n");
|
|
2296
|
+
return { ok: true, result: tail2 || `(no output yet for ${id})` };
|
|
2293
2297
|
}
|
|
2294
2298
|
if (action === "stop") {
|
|
2295
2299
|
if (proc.status !== "running") {
|
|
@@ -2673,7 +2677,7 @@ var McpManager = class {
|
|
|
2673
2677
|
}
|
|
2674
2678
|
}
|
|
2675
2679
|
closeAll() {
|
|
2676
|
-
for (const [,
|
|
2680
|
+
for (const [, c5] of this.clients) c5.close();
|
|
2677
2681
|
this.clients.clear();
|
|
2678
2682
|
}
|
|
2679
2683
|
get(name) {
|
|
@@ -2684,13 +2688,13 @@ var McpManager = class {
|
|
|
2684
2688
|
}
|
|
2685
2689
|
toolCount() {
|
|
2686
2690
|
let n = 0;
|
|
2687
|
-
for (const [,
|
|
2691
|
+
for (const [, c5] of this.clients) n += c5.tools.length;
|
|
2688
2692
|
return n;
|
|
2689
2693
|
}
|
|
2690
2694
|
/** A JSON-serializable catalog of every connected server for the KV/context. */
|
|
2691
2695
|
catalog() {
|
|
2692
2696
|
return {
|
|
2693
|
-
servers: Array.from(this.clients.values()).map((
|
|
2697
|
+
servers: Array.from(this.clients.values()).map((c5) => c5.catalogEntry()),
|
|
2694
2698
|
generatedAt: Date.now()
|
|
2695
2699
|
};
|
|
2696
2700
|
}
|
|
@@ -2780,9 +2784,9 @@ function flattenContent(content, structured) {
|
|
|
2780
2784
|
}
|
|
2781
2785
|
function flattenResourceContents(contents) {
|
|
2782
2786
|
const parts = [];
|
|
2783
|
-
for (const
|
|
2784
|
-
if (typeof
|
|
2785
|
-
else if (typeof
|
|
2787
|
+
for (const c5 of contents || []) {
|
|
2788
|
+
if (typeof c5.text === "string") parts.push(c5.text);
|
|
2789
|
+
else if (typeof c5.blob === "string") parts.push(`[binary resource ${String(c5.uri ?? "")} (${c5.blob.length} b64 chars)]`);
|
|
2786
2790
|
}
|
|
2787
2791
|
return parts.join("\n").trim();
|
|
2788
2792
|
}
|
|
@@ -3593,8 +3597,8 @@ function parseStreamingMarkdown(source) {
|
|
|
3593
3597
|
let codeEnd = closing?.start ?? source.length;
|
|
3594
3598
|
if (!closing) {
|
|
3595
3599
|
const tailStart = Math.max(codeStart, source.lastIndexOf("\n") + 1);
|
|
3596
|
-
const
|
|
3597
|
-
const pending = /^ {0,3}(`+|~+)[ \t]*$/.exec(
|
|
3600
|
+
const tail2 = source.slice(tailStart);
|
|
3601
|
+
const pending = /^ {0,3}(`+|~+)[ \t]*$/.exec(tail2);
|
|
3598
3602
|
if (pending && pending[1][0] === opening.marker && pending[1].length < opening.length) codeEnd = tailStart;
|
|
3599
3603
|
}
|
|
3600
3604
|
if (codeEnd > codeStart && source[codeEnd - 1] === "\n") codeEnd--;
|
|
@@ -3813,17 +3817,17 @@ function wrapBlock(out, cols2, leadFirst, leadRest, leadWidth, text) {
|
|
|
3813
3817
|
function renderTable(rows) {
|
|
3814
3818
|
const cols2 = Math.max(...rows.map((r) => r.length));
|
|
3815
3819
|
const widths = [];
|
|
3816
|
-
for (let
|
|
3817
|
-
widths[
|
|
3820
|
+
for (let c5 = 0; c5 < cols2; c5++) {
|
|
3821
|
+
widths[c5] = Math.max(...rows.map((r) => visibleWidth(inline(r[c5] ?? ""))));
|
|
3818
3822
|
}
|
|
3819
3823
|
const sep = `${GRAY} \u2502 ${R}`;
|
|
3820
3824
|
const out = [];
|
|
3821
3825
|
rows.forEach((r, ri) => {
|
|
3822
3826
|
const cells = [];
|
|
3823
|
-
for (let
|
|
3824
|
-
const raw = r[
|
|
3827
|
+
for (let c5 = 0; c5 < cols2; c5++) {
|
|
3828
|
+
const raw = r[c5] ?? "";
|
|
3825
3829
|
const styled = ri === 0 ? `${BOLD}${inline(raw)}${R}` : inline(raw);
|
|
3826
|
-
cells.push(padEndVisible(styled, widths[
|
|
3830
|
+
cells.push(padEndVisible(styled, widths[c5]));
|
|
3827
3831
|
}
|
|
3828
3832
|
out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
|
|
3829
3833
|
if (ri === 0) {
|
|
@@ -4260,7 +4264,11 @@ function buildInputBoxRows(opts) {
|
|
|
4260
4264
|
if (phase == null || !Number.isFinite(phase)) {
|
|
4261
4265
|
const dots2 = borderLevelDots(level);
|
|
4262
4266
|
const dotsStyled2 = borderLevelDotsStyled(level, levelColor);
|
|
4263
|
-
const
|
|
4267
|
+
const agent2 = (opts.agentLabel ?? "").trim();
|
|
4268
|
+
const withAgent2 = agent2.length > 0 && ` ${agent2} ${dots2} `.length <= geo.boxW - 3;
|
|
4269
|
+
const levelText = withAgent2 ? `${agent2} ${dots2}` : dots2;
|
|
4270
|
+
const levelStyled = withAgent2 ? `\x1B[2m${themeGray}${agent2}\x1B[0m ${dotsStyled2}` : dotsStyled2;
|
|
4271
|
+
const top2 = buildBoxTop(geo.boxW, levelText, borderColor, levelColor, "\x1B[0m", levelStyled);
|
|
4264
4272
|
const bottom2 = buildBoxBottom(geo.boxW, borderColor);
|
|
4265
4273
|
return [pad + top2, ...body.map((b) => pad + buildBoxBody(b, geo.contentW, borderColor)), pad + bottom2];
|
|
4266
4274
|
}
|
|
@@ -4271,7 +4279,11 @@ function buildInputBoxRows(opts) {
|
|
|
4271
4279
|
const colorAt = (i) => rotatingBorderCellColor(i, P, phase);
|
|
4272
4280
|
const dots = borderLevelDots(level);
|
|
4273
4281
|
borderLevelDotsStyled(level, levelColor);
|
|
4274
|
-
const
|
|
4282
|
+
const agent = (opts.agentLabel ?? "").trim();
|
|
4283
|
+
const withAgent = agent.length > 0 && ` ${agent} ${dots} `.length <= W - 3;
|
|
4284
|
+
const labelPlain = withAgent ? ` ${agent} ${dots} ` : ` ${dots} `;
|
|
4285
|
+
const agentSpanStart = withAgent ? 1 : -1;
|
|
4286
|
+
const agentSpanEnd = withAgent ? 1 + agent.length : -1;
|
|
4275
4287
|
const rightFillN = 1;
|
|
4276
4288
|
const budget = Math.max(1, W - 2 - rightFillN);
|
|
4277
4289
|
const plain = labelPlain.length > budget ? labelPlain.slice(0, Math.max(1, budget)) : labelPlain;
|
|
@@ -4285,15 +4297,20 @@ function buildInputBoxRows(opts) {
|
|
|
4285
4297
|
{
|
|
4286
4298
|
const plainChars = [...plain];
|
|
4287
4299
|
let di = 0;
|
|
4288
|
-
for (
|
|
4289
|
-
const
|
|
4300
|
+
for (let ci = 0; ci < plainChars.length; ci++) {
|
|
4301
|
+
const ch = plainChars[ci];
|
|
4302
|
+
const c5 = colorAt(perimeterIndex("top", xi++, W, bodyH));
|
|
4303
|
+
if (ci >= agentSpanStart && ci < agentSpanEnd) {
|
|
4304
|
+
top += `\x1B[2m${themeGray}${ch}\x1B[0m`;
|
|
4305
|
+
continue;
|
|
4306
|
+
}
|
|
4290
4307
|
if (ch === "\u25CF" || ch === "\u25CB") {
|
|
4291
4308
|
const levelN = Math.max(1, Math.min(5, level));
|
|
4292
4309
|
const filled = di < levelN;
|
|
4293
4310
|
di++;
|
|
4294
|
-
top += (filled ? levelColor ||
|
|
4311
|
+
top += (filled ? levelColor || c5 : "\x1B[38;5;240m") + (filled ? "\u25CF" : "\u25CB") + reset;
|
|
4295
4312
|
} else {
|
|
4296
|
-
top +=
|
|
4313
|
+
top += c5 + ch + reset;
|
|
4297
4314
|
}
|
|
4298
4315
|
}
|
|
4299
4316
|
}
|
|
@@ -4314,10 +4331,10 @@ function buildInputBoxRows(opts) {
|
|
|
4314
4331
|
let bottom = "";
|
|
4315
4332
|
for (let x = 0; x < W; x++) {
|
|
4316
4333
|
const idx = perimeterIndex("bottom", W - 1 - x, W, bodyH);
|
|
4317
|
-
const
|
|
4318
|
-
if (x === 0) bottom +=
|
|
4319
|
-
else if (x === W - 1) bottom +=
|
|
4320
|
-
else bottom +=
|
|
4334
|
+
const c5 = colorAt(idx);
|
|
4335
|
+
if (x === 0) bottom += c5 + "\u2570" + reset;
|
|
4336
|
+
else if (x === W - 1) bottom += c5 + "\u256F" + reset;
|
|
4337
|
+
else bottom += c5 + "\u2500" + reset;
|
|
4321
4338
|
}
|
|
4322
4339
|
return [pad + top, ...bodyRows, pad + bottom];
|
|
4323
4340
|
}
|
|
@@ -4334,6 +4351,36 @@ var C = {
|
|
|
4334
4351
|
gray: themeGray,
|
|
4335
4352
|
teal: "\x1B[38;5;37m"
|
|
4336
4353
|
};
|
|
4354
|
+
function renderSelectItemRows(it, sel, contentW, busyGlyph) {
|
|
4355
|
+
const hint = it.hint ?? "";
|
|
4356
|
+
const hintW = boxVisibleWidth(hint);
|
|
4357
|
+
const pointerW = 2;
|
|
4358
|
+
const labelMax = Math.max(4, contentW - pointerW - (hintW ? hintW + 2 : 0));
|
|
4359
|
+
let label = it.label.replace(/\s+/g, " ").trim();
|
|
4360
|
+
if (boxVisibleWidth(label) > labelMax) {
|
|
4361
|
+
while (label.length > 0 && boxVisibleWidth(label) > labelMax - 1) label = label.slice(0, -1);
|
|
4362
|
+
label += "\u2026";
|
|
4363
|
+
}
|
|
4364
|
+
const pointer = busyGlyph ? `${busyGlyph} ` : sel ? `${C.magenta}\u276F${C.reset} ` : " ";
|
|
4365
|
+
const styledLabel = it.disabled ? `${C.gray}${label}${C.reset}` : sel ? `${C.bold}${C.cyan}${label}${C.reset}` : `${C.dim}${label}${C.reset}`;
|
|
4366
|
+
let content = `${pointer}${styledLabel}`;
|
|
4367
|
+
if (hintW) {
|
|
4368
|
+
const used = pointerW + boxVisibleWidth(label);
|
|
4369
|
+
const gap = Math.max(1, contentW - used - hintW);
|
|
4370
|
+
content += `${" ".repeat(gap)}${sel ? C.gray : C.dim}${hint}${C.reset}`;
|
|
4371
|
+
}
|
|
4372
|
+
const rows = [content];
|
|
4373
|
+
if (it.detail) {
|
|
4374
|
+
let detail = it.detail.replace(/\s+/g, " ").trim();
|
|
4375
|
+
const detailMax = Math.max(4, contentW - pointerW - 1);
|
|
4376
|
+
if (boxVisibleWidth(detail) > detailMax) {
|
|
4377
|
+
while (detail.length > 0 && boxVisibleWidth(detail) > detailMax - 1) detail = detail.slice(0, -1);
|
|
4378
|
+
detail += "\u2026";
|
|
4379
|
+
}
|
|
4380
|
+
rows.push(`${" ".repeat(pointerW)}${sel ? `${C.dim}${C.cyan}` : `${C.dim}${C.gray}`}${detail}${C.reset}`);
|
|
4381
|
+
}
|
|
4382
|
+
return rows;
|
|
4383
|
+
}
|
|
4337
4384
|
var SYNC_OUTPUT_BEGIN = "\x1B[?2026h";
|
|
4338
4385
|
var SYNC_OUTPUT_END = "\x1B[?2026l";
|
|
4339
4386
|
var CURSOR_HIDE = "\x1B[?25l";
|
|
@@ -4486,7 +4533,7 @@ var Tui = class _Tui {
|
|
|
4486
4533
|
// event hooks (wired by index.ts)
|
|
4487
4534
|
onSubmit = () => {
|
|
4488
4535
|
};
|
|
4489
|
-
/**
|
|
4536
|
+
/** Option/Alt+Return sends a steering input through the portable messaging endpoint. */
|
|
4490
4537
|
onSteer = () => {
|
|
4491
4538
|
};
|
|
4492
4539
|
onInterrupt = () => {
|
|
@@ -4584,7 +4631,7 @@ var Tui = class _Tui {
|
|
|
4584
4631
|
dispatch(str, key) {
|
|
4585
4632
|
const seq = key && key.sequence || str || "";
|
|
4586
4633
|
if (seq === "\n" || seq === "\x1B[13;2u" || seq === "\x1B[27;2;13~") {
|
|
4587
|
-
if (!this.takeoverHandler && !this.pasting && !this.paletteOpen()) this.
|
|
4634
|
+
if (!this.takeoverHandler && !this.pasting && !this.paletteOpen()) this.insertAtCursor("\n");
|
|
4588
4635
|
return;
|
|
4589
4636
|
}
|
|
4590
4637
|
if (key && key.ctrl && key.name === "c") {
|
|
@@ -4717,11 +4764,11 @@ var Tui = class _Tui {
|
|
|
4717
4764
|
}
|
|
4718
4765
|
if (key.name === "return" || key.name === "enter") {
|
|
4719
4766
|
if (key.meta) {
|
|
4720
|
-
this.
|
|
4767
|
+
this.submitInput(true);
|
|
4721
4768
|
return;
|
|
4722
4769
|
}
|
|
4723
4770
|
if (key.shift) {
|
|
4724
|
-
this.
|
|
4771
|
+
this.insertAtCursor("\n");
|
|
4725
4772
|
return;
|
|
4726
4773
|
}
|
|
4727
4774
|
this.submitInput(false);
|
|
@@ -5022,7 +5069,7 @@ var Tui = class _Tui {
|
|
|
5022
5069
|
const q = this.inputBuffer.slice(1).trim().toLowerCase();
|
|
5023
5070
|
if (q === "") return this.commands;
|
|
5024
5071
|
return this.commands.filter(
|
|
5025
|
-
(
|
|
5072
|
+
(c5) => c5.name.startsWith(q) || c5.name.includes(q) || c5.label.toLowerCase().includes(q)
|
|
5026
5073
|
);
|
|
5027
5074
|
}
|
|
5028
5075
|
runCommand(cmd) {
|
|
@@ -5276,7 +5323,8 @@ var Tui = class _Tui {
|
|
|
5276
5323
|
level: this.level,
|
|
5277
5324
|
levelColor: this.levelColor(),
|
|
5278
5325
|
borderColor: inputBoxBorderColor(),
|
|
5279
|
-
borderPhase
|
|
5326
|
+
borderPhase,
|
|
5327
|
+
agentLabel: this.agentLabel
|
|
5280
5328
|
});
|
|
5281
5329
|
const boxTop = boxRows[0];
|
|
5282
5330
|
const boxBottom = boxRows[boxRows.length - 1];
|
|
@@ -5550,6 +5598,14 @@ var Tui = class _Tui {
|
|
|
5550
5598
|
});
|
|
5551
5599
|
this.print("");
|
|
5552
5600
|
}
|
|
5601
|
+
// ─── agent label (under the input box) ─────────────────────────────────────
|
|
5602
|
+
/** The loaded agent's display title, shown low-contrast under the input. */
|
|
5603
|
+
agentLabel = "";
|
|
5604
|
+
setAgentLabel(label) {
|
|
5605
|
+
if (label === this.agentLabel) return;
|
|
5606
|
+
this.agentLabel = label;
|
|
5607
|
+
this.renderBottom();
|
|
5608
|
+
}
|
|
5553
5609
|
// ─── working indicator (turn state) ───────────────────────────────────────
|
|
5554
5610
|
setWorking(on) {
|
|
5555
5611
|
if (on === this.working) return;
|
|
@@ -5930,8 +5986,13 @@ var Tui = class _Tui {
|
|
|
5930
5986
|
* Arrow-key selection menu (slash menu, process menu, resume). Pauses input.
|
|
5931
5987
|
* Boxed chrome matches the bottom input HUD: side margin + grey rounded
|
|
5932
5988
|
* border, selected row with a brand-tinted pointer.
|
|
5989
|
+
*
|
|
5990
|
+
* An item may carry a `detail` — a dim second physical line under its label
|
|
5991
|
+
* for context that doesn't fit the one-row label+hint layout (e.g. a
|
|
5992
|
+
* machine's hostname · daemon status). `opts.spaced` inserts a blank row
|
|
5993
|
+
* between items for readability in short, dense menus.
|
|
5933
5994
|
*/
|
|
5934
|
-
select(title, items) {
|
|
5995
|
+
select(title, items, opts) {
|
|
5935
5996
|
return new Promise((resolve) => {
|
|
5936
5997
|
let idx = Math.max(0, items.findIndex((it) => !it.disabled));
|
|
5937
5998
|
this.beginTakeover();
|
|
@@ -5944,41 +6005,28 @@ var Tui = class _Tui {
|
|
|
5944
6005
|
${pad}${title}
|
|
5945
6006
|
`);
|
|
5946
6007
|
else process.stdout.write("\n");
|
|
5947
|
-
const
|
|
5948
|
-
const it = items[i];
|
|
5949
|
-
const sel = i === idx;
|
|
5950
|
-
const hint = it.hint ?? "";
|
|
5951
|
-
const hintW = hint.length;
|
|
5952
|
-
const pointerW = 2;
|
|
5953
|
-
const labelMax = Math.max(4, geo.contentW - pointerW - (hintW ? hintW + 2 : 0));
|
|
5954
|
-
let label = it.label.replace(/\s+/g, " ").trim();
|
|
5955
|
-
if (label.length > labelMax) label = label.slice(0, Math.max(0, labelMax - 1)) + "\u2026";
|
|
5956
|
-
const pointer = sel ? `${C.magenta}\u276F${C.reset} ` : " ";
|
|
5957
|
-
const styledLabel = it.disabled ? `${C.gray}${label}${C.reset}` : sel ? `${C.bold}${C.cyan}${label}${C.reset}` : `${C.dim}${label}${C.reset}`;
|
|
5958
|
-
let content = `${pointer}${styledLabel}`;
|
|
5959
|
-
if (hintW) {
|
|
5960
|
-
const used = pointerW + label.length;
|
|
5961
|
-
const gap = Math.max(1, geo.contentW - used - hintW);
|
|
5962
|
-
content += `${" ".repeat(gap)}${sel ? C.gray : C.dim}${hint}${C.reset}`;
|
|
5963
|
-
}
|
|
5964
|
-
return content;
|
|
5965
|
-
};
|
|
6008
|
+
const renderItemRows = (i) => renderSelectItemRows(items[i], i === idx, geo.contentW);
|
|
5966
6009
|
const writeRow = (line) => {
|
|
5967
6010
|
const row = this.clampVisible(sanitizeHudRow(line), rowCap);
|
|
5968
6011
|
process.stdout.write(`\r\x1B[K${row}
|
|
5969
6012
|
`);
|
|
5970
6013
|
};
|
|
6014
|
+
const spaced = opts?.spaced ?? false;
|
|
6015
|
+
const contentRows = items.reduce((n, it) => n + 1 + (it.detail ? 1 : 0), 0) + (spaced ? Math.max(0, items.length - 1) : 0);
|
|
5971
6016
|
const draw = (moveUp) => {
|
|
5972
|
-
if (moveUp) process.stdout.write(`\x1B[${
|
|
6017
|
+
if (moveUp) process.stdout.write(`\x1B[${contentRows + 1}A`);
|
|
5973
6018
|
else writeRow(pad + buildBoxTopPlain(geo.boxW, border));
|
|
5974
6019
|
for (let i = 0; i < items.length; i++) {
|
|
5975
|
-
writeRow(pad + buildBoxBody(
|
|
6020
|
+
if (spaced && i > 0) writeRow(pad + buildBoxBody("", geo.contentW, border));
|
|
6021
|
+
for (const row of renderItemRows(i)) {
|
|
6022
|
+
writeRow(pad + buildBoxBody(row, geo.contentW, border));
|
|
6023
|
+
}
|
|
5976
6024
|
}
|
|
5977
6025
|
writeRow(pad + buildBoxBottom(geo.boxW, border));
|
|
5978
6026
|
};
|
|
5979
6027
|
draw(false);
|
|
5980
6028
|
const titleRows = title ? 2 : 1;
|
|
5981
|
-
const boxRows = 1 +
|
|
6029
|
+
const boxRows = 1 + contentRows + 1;
|
|
5982
6030
|
const erase = () => {
|
|
5983
6031
|
process.stdout.write("\r");
|
|
5984
6032
|
const up = titleRows + boxRows;
|
|
@@ -6006,6 +6054,182 @@ ${pad}${title}
|
|
|
6006
6054
|
};
|
|
6007
6055
|
});
|
|
6008
6056
|
}
|
|
6057
|
+
/**
|
|
6058
|
+
* A select menu that stays OPEN while its contents change — the primitive
|
|
6059
|
+
* behind in-place navigation UIs like the directory browser. Instead of
|
|
6060
|
+
* resolving once like `select`, it returns a handle:
|
|
6061
|
+
*
|
|
6062
|
+
* - `next()` awaits the next interaction: a pick, a registered extra key
|
|
6063
|
+
* (e.g. "." to toggle hidden files), or Escape (cancel).
|
|
6064
|
+
* - `update()` swaps title/items/footer and redraws the SAME screen region
|
|
6065
|
+
* (erase + repaint in one write), so navigating levels never feels like
|
|
6066
|
+
* leaving the menu.
|
|
6067
|
+
* - `setBusy(i)` animates a spinner in row i's pointer column while the
|
|
6068
|
+
* caller loads what's behind it; input (except Escape) is swallowed until
|
|
6069
|
+
* the next update()/setBusy(null).
|
|
6070
|
+
* - `close()` erases the region and releases the takeover.
|
|
6071
|
+
*
|
|
6072
|
+
* The optional `footer` is a dim line under the box — the home for key
|
|
6073
|
+
* hints and transient error text.
|
|
6074
|
+
*/
|
|
6075
|
+
openLiveSelect(opts) {
|
|
6076
|
+
let { title, items } = opts;
|
|
6077
|
+
let footer = opts.footer ?? "";
|
|
6078
|
+
const spaced = opts.spaced ?? false;
|
|
6079
|
+
const extraKeys = opts.keys ?? [];
|
|
6080
|
+
let idx = Math.max(0, items.findIndex((it) => !it.disabled));
|
|
6081
|
+
let busyIdx = null;
|
|
6082
|
+
let busyTimer = null;
|
|
6083
|
+
let closed = false;
|
|
6084
|
+
let edit = null;
|
|
6085
|
+
this.beginTakeover();
|
|
6086
|
+
const cols2 = process.stdout.columns || 80;
|
|
6087
|
+
const geo = inputBoxGeometry(cols2);
|
|
6088
|
+
const border = inputBoxBorderColor();
|
|
6089
|
+
const pad = " ".repeat(geo.margin);
|
|
6090
|
+
const rowCap = Math.max(1, cols2 - 1);
|
|
6091
|
+
let regionRows = 0;
|
|
6092
|
+
let lifted = 0;
|
|
6093
|
+
const draw = () => {
|
|
6094
|
+
if (closed) return;
|
|
6095
|
+
const rows = [""];
|
|
6096
|
+
let editCursor = null;
|
|
6097
|
+
if (title) rows.push(pad + title);
|
|
6098
|
+
rows.push(pad + buildBoxTopPlain(geo.boxW, border));
|
|
6099
|
+
for (let i = 0; i < items.length; i++) {
|
|
6100
|
+
const it = items[i];
|
|
6101
|
+
if (spaced && i > 0) rows.push(pad + buildBoxBody("", geo.contentW, border));
|
|
6102
|
+
const glyph = busyIdx === i ? this.spinnerFrame() : void 0;
|
|
6103
|
+
if (edit && edit.index === i) {
|
|
6104
|
+
const pointer = `${glyph ?? `${C.magenta}\u276F${C.reset}`} `;
|
|
6105
|
+
const max = Math.max(4, geo.contentW - 2 - boxVisibleWidth(edit.prefix) - 1);
|
|
6106
|
+
let buf = edit.buf;
|
|
6107
|
+
while (buf.length > 0 && boxVisibleWidth(buf) > max) buf = buf.slice(1);
|
|
6108
|
+
const body = buf ? `${C.bold}${C.cyan}${buf}${C.reset}` : `${C.dim}${edit.placeholder}${C.reset}`;
|
|
6109
|
+
rows.push(pad + buildBoxBody(`${pointer}${C.dim}${edit.prefix}${C.reset}${body}`, geo.contentW, border));
|
|
6110
|
+
if (!glyph) {
|
|
6111
|
+
editCursor = {
|
|
6112
|
+
rowIndex: rows.length - 1,
|
|
6113
|
+
// 1-based ANSI column: margin + "│ " + pointer(2) + prefix + typed text.
|
|
6114
|
+
col: geo.margin + 2 + 2 + boxVisibleWidth(edit.prefix) + boxVisibleWidth(buf) + 1
|
|
6115
|
+
};
|
|
6116
|
+
}
|
|
6117
|
+
continue;
|
|
6118
|
+
}
|
|
6119
|
+
for (const r of renderSelectItemRows(it, i === idx, geo.contentW, glyph)) {
|
|
6120
|
+
rows.push(pad + buildBoxBody(r, geo.contentW, border));
|
|
6121
|
+
}
|
|
6122
|
+
}
|
|
6123
|
+
rows.push(pad + buildBoxBottom(geo.boxW, border));
|
|
6124
|
+
if (footer) rows.push(pad + footer);
|
|
6125
|
+
let out = lifted > 0 ? `\x1B[${lifted}B` : "";
|
|
6126
|
+
lifted = 0;
|
|
6127
|
+
out += regionRows > 0 ? `\r\x1B[${regionRows}A\x1B[J` : "";
|
|
6128
|
+
for (const line of rows) out += `\r\x1B[K${this.clampVisible(sanitizeHudRow(line), rowCap)}
|
|
6129
|
+
`;
|
|
6130
|
+
if (editCursor) {
|
|
6131
|
+
lifted = rows.length - editCursor.rowIndex;
|
|
6132
|
+
out += `\x1B[${lifted}A\x1B[${editCursor.col}G\x1B[?25h`;
|
|
6133
|
+
} else {
|
|
6134
|
+
out += "\x1B[?25l";
|
|
6135
|
+
}
|
|
6136
|
+
process.stdout.write(out);
|
|
6137
|
+
regionRows = rows.length;
|
|
6138
|
+
};
|
|
6139
|
+
draw();
|
|
6140
|
+
const queue = [];
|
|
6141
|
+
let waiter = null;
|
|
6142
|
+
const emit = (e) => {
|
|
6143
|
+
if (waiter) {
|
|
6144
|
+
const w = waiter;
|
|
6145
|
+
waiter = null;
|
|
6146
|
+
w(e);
|
|
6147
|
+
} else queue.push(e);
|
|
6148
|
+
};
|
|
6149
|
+
const stopSpinner = () => {
|
|
6150
|
+
if (busyTimer) clearInterval(busyTimer);
|
|
6151
|
+
busyTimer = null;
|
|
6152
|
+
busyIdx = null;
|
|
6153
|
+
};
|
|
6154
|
+
this.takeoverHandler = (str, key) => {
|
|
6155
|
+
if (edit && busyIdx === null) {
|
|
6156
|
+
if (key?.name === "escape") {
|
|
6157
|
+
edit = null;
|
|
6158
|
+
draw();
|
|
6159
|
+
} else if (key?.name === "return" || key?.name === "enter") {
|
|
6160
|
+
const text = edit.buf.trim();
|
|
6161
|
+
if (!text) {
|
|
6162
|
+
edit = null;
|
|
6163
|
+
draw();
|
|
6164
|
+
} else {
|
|
6165
|
+
emit({ kind: "edit", text, index: edit.index });
|
|
6166
|
+
}
|
|
6167
|
+
} else if (key?.name === "backspace") {
|
|
6168
|
+
edit.buf = edit.buf.slice(0, -1);
|
|
6169
|
+
draw();
|
|
6170
|
+
} else if (str && !key?.ctrl && !key?.meta) {
|
|
6171
|
+
const clean2 = str.replace(/[\x00-\x1f\x7f]/g, "");
|
|
6172
|
+
if (clean2) {
|
|
6173
|
+
edit.buf += clean2;
|
|
6174
|
+
draw();
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
return;
|
|
6178
|
+
}
|
|
6179
|
+
if (key?.name === "escape") return emit({ kind: "cancel" });
|
|
6180
|
+
if (busyIdx !== null) return;
|
|
6181
|
+
if (key?.name === "up" || str === "k") {
|
|
6182
|
+
idx = (idx - 1 + items.length) % items.length;
|
|
6183
|
+
draw();
|
|
6184
|
+
} else if (key?.name === "down" || str === "j") {
|
|
6185
|
+
idx = (idx + 1) % items.length;
|
|
6186
|
+
draw();
|
|
6187
|
+
} else if (key?.name === "return" || key?.name === "enter") {
|
|
6188
|
+
if (!items[idx]?.disabled) emit({ kind: "pick", value: items[idx].value, index: idx });
|
|
6189
|
+
} else if (str && extraKeys.includes(str)) {
|
|
6190
|
+
emit({ kind: "key", name: str, index: idx });
|
|
6191
|
+
}
|
|
6192
|
+
};
|
|
6193
|
+
return {
|
|
6194
|
+
next: () => queue.length > 0 ? Promise.resolve(queue.shift()) : new Promise((r) => waiter = r),
|
|
6195
|
+
update: (state) => {
|
|
6196
|
+
stopSpinner();
|
|
6197
|
+
edit = null;
|
|
6198
|
+
if (state.title !== void 0) title = state.title;
|
|
6199
|
+
if (state.items) {
|
|
6200
|
+
items = state.items;
|
|
6201
|
+
if (idx >= items.length || items[idx]?.disabled) {
|
|
6202
|
+
idx = Math.max(0, items.findIndex((it) => !it.disabled));
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
if (state.footer !== void 0) footer = state.footer;
|
|
6206
|
+
draw();
|
|
6207
|
+
},
|
|
6208
|
+
setBusy: (index) => {
|
|
6209
|
+
stopSpinner();
|
|
6210
|
+
if (index !== null) {
|
|
6211
|
+
busyIdx = index;
|
|
6212
|
+
busyTimer = setInterval(draw, SPINNER_FRAME_MS);
|
|
6213
|
+
}
|
|
6214
|
+
draw();
|
|
6215
|
+
},
|
|
6216
|
+
beginEdit: (index, editOpts) => {
|
|
6217
|
+
stopSpinner();
|
|
6218
|
+
edit = { index, buf: "", placeholder: editOpts?.placeholder ?? "", prefix: editOpts?.prefix ?? "" };
|
|
6219
|
+
idx = index;
|
|
6220
|
+
draw();
|
|
6221
|
+
},
|
|
6222
|
+
close: () => {
|
|
6223
|
+
if (closed) return;
|
|
6224
|
+
closed = true;
|
|
6225
|
+
stopSpinner();
|
|
6226
|
+
if (lifted > 0) process.stdout.write(`\x1B[${lifted}B`);
|
|
6227
|
+
lifted = 0;
|
|
6228
|
+
if (regionRows > 0) process.stdout.write(`\r\x1B[${regionRows}A\x1B[J`);
|
|
6229
|
+
this.endTakeover();
|
|
6230
|
+
}
|
|
6231
|
+
};
|
|
6232
|
+
}
|
|
6009
6233
|
/**
|
|
6010
6234
|
* Free-text prompt (single line). Pauses the main input and reads a line —
|
|
6011
6235
|
* used where a menu can't, e.g. entering an MCP server command. Enter submits,
|
|
@@ -6257,6 +6481,7 @@ var NAME_SUFFIX = ".name";
|
|
|
6257
6481
|
var ICON_SUFFIX = ".icon";
|
|
6258
6482
|
var FSREQ_SUFFIX = ".fsreq";
|
|
6259
6483
|
var FSRES_SUFFIX = ".fsres";
|
|
6484
|
+
var PROJNAMES_SUFFIX = ".projnames";
|
|
6260
6485
|
var DAEMON_ONLINE_WINDOW_MS = 90 * 1e3;
|
|
6261
6486
|
function machineKey(machineId) {
|
|
6262
6487
|
return `${KEY_PREFIX}${machineId}`;
|
|
@@ -6270,6 +6495,9 @@ function nameKey(machineId) {
|
|
|
6270
6495
|
function iconKey(machineId) {
|
|
6271
6496
|
return `${KEY_PREFIX}${machineId}${ICON_SUFFIX}`;
|
|
6272
6497
|
}
|
|
6498
|
+
function projectNamesKey(machineId) {
|
|
6499
|
+
return `${KEY_PREFIX}${machineId}${PROJNAMES_SUFFIX}`;
|
|
6500
|
+
}
|
|
6273
6501
|
function fsRequestKey(machineId) {
|
|
6274
6502
|
return `${KEY_PREFIX}${machineId}${FSREQ_SUFFIX}`;
|
|
6275
6503
|
}
|
|
@@ -6312,25 +6540,6 @@ async function readFsRequest(api, machineId) {
|
|
|
6312
6540
|
async function writeFsResponse(api, machineId, res) {
|
|
6313
6541
|
await api.userKvSet(fsResponseKey(machineId), { ...res, responded_at: Date.now() });
|
|
6314
6542
|
}
|
|
6315
|
-
async function resolveRemoteProjectPath(api, machineId, typedPath, timeoutMs = 12e3) {
|
|
6316
|
-
const trimmed = typedPath.trim();
|
|
6317
|
-
if (!trimmed.startsWith("~")) return trimmed;
|
|
6318
|
-
const nonce = crypto.randomBytes(8).toString("hex");
|
|
6319
|
-
try {
|
|
6320
|
-
await writeFsRequest(api, machineId, { nonce, op: "list", path: trimmed });
|
|
6321
|
-
const deadline = Date.now() + timeoutMs;
|
|
6322
|
-
while (Date.now() < deadline) {
|
|
6323
|
-
await new Promise((r) => setTimeout(r, 700));
|
|
6324
|
-
const res = await readFsResponse(api, machineId).catch(() => null);
|
|
6325
|
-
if (res && res.nonce === nonce) {
|
|
6326
|
-
const resolved = res.result && typeof res.result.path === "string" ? res.result.path : "";
|
|
6327
|
-
return resolved || trimmed;
|
|
6328
|
-
}
|
|
6329
|
-
}
|
|
6330
|
-
} catch {
|
|
6331
|
-
}
|
|
6332
|
-
return trimmed;
|
|
6333
|
-
}
|
|
6334
6543
|
function machineIcon(record2) {
|
|
6335
6544
|
if (record2.icon && record2.icon.trim()) return record2.icon.trim();
|
|
6336
6545
|
if (record2.platform === "darwin") return "\u{1F4BB}";
|
|
@@ -6360,7 +6569,10 @@ function parseMachineRecord(value) {
|
|
|
6360
6569
|
arch: typeof r.arch === "string" ? r.arch : "",
|
|
6361
6570
|
version: typeof r.version === "string" ? r.version : void 0,
|
|
6362
6571
|
daemon: r.daemon && typeof r.daemon === "object" && !Array.isArray(r.daemon) ? r.daemon : null,
|
|
6363
|
-
|
|
6572
|
+
// Clone rather than alias the caller's object: read-side overlays (e.g.
|
|
6573
|
+
// user-assigned project names) mutate the parsed record's entries and
|
|
6574
|
+
// must never write through into the source value.
|
|
6575
|
+
projects: r.projects && typeof r.projects === "object" && !Array.isArray(r.projects) ? structuredClone(r.projects) : {},
|
|
6364
6576
|
created_at: typeof r.created_at === "number" ? r.created_at : 0,
|
|
6365
6577
|
updated_at: typeof r.updated_at === "number" ? r.updated_at : 0
|
|
6366
6578
|
};
|
|
@@ -6373,6 +6585,7 @@ async function loadMachines(api) {
|
|
|
6373
6585
|
const records = [];
|
|
6374
6586
|
const names = /* @__PURE__ */ new Map();
|
|
6375
6587
|
const icons = /* @__PURE__ */ new Map();
|
|
6588
|
+
const projNames = /* @__PURE__ */ new Map();
|
|
6376
6589
|
for (const e of entries) {
|
|
6377
6590
|
const rest = e.key.slice(KEY_PREFIX.length);
|
|
6378
6591
|
if (rest.endsWith(CMD_SUFFIX) || rest.endsWith(FSREQ_SUFFIX) || rest.endsWith(FSRES_SUFFIX)) continue;
|
|
@@ -6386,6 +6599,12 @@ async function loadMachines(api) {
|
|
|
6386
6599
|
if (typeof e.value === "string" && e.value.trim()) icons.set(id, e.value.trim());
|
|
6387
6600
|
continue;
|
|
6388
6601
|
}
|
|
6602
|
+
if (rest.endsWith(PROJNAMES_SUFFIX)) {
|
|
6603
|
+
const id = rest.slice(0, -PROJNAMES_SUFFIX.length);
|
|
6604
|
+
const map = parseProjectNames(e.value);
|
|
6605
|
+
if (map) projNames.set(id, map);
|
|
6606
|
+
continue;
|
|
6607
|
+
}
|
|
6389
6608
|
const rec = parseMachineRecord(e.value);
|
|
6390
6609
|
if (rec) records.push(rec);
|
|
6391
6610
|
}
|
|
@@ -6394,9 +6613,58 @@ async function loadMachines(api) {
|
|
|
6394
6613
|
if (override) rec.name = override;
|
|
6395
6614
|
const icon = icons.get(rec.id);
|
|
6396
6615
|
if (icon) rec.icon = icon;
|
|
6616
|
+
overlayProjectNames(rec, projNames.get(rec.id));
|
|
6397
6617
|
}
|
|
6398
6618
|
return records;
|
|
6399
6619
|
}
|
|
6620
|
+
function parseProjectNames(value) {
|
|
6621
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
6622
|
+
const out = {};
|
|
6623
|
+
for (const [dir2, name] of Object.entries(value)) {
|
|
6624
|
+
if (typeof name === "string" && name.trim()) out[dir2] = name.trim();
|
|
6625
|
+
}
|
|
6626
|
+
return out;
|
|
6627
|
+
}
|
|
6628
|
+
function overlayProjectNames(record2, names) {
|
|
6629
|
+
if (!names) return;
|
|
6630
|
+
for (const [dir2, name] of Object.entries(names)) {
|
|
6631
|
+
const project = record2.projects[dir2];
|
|
6632
|
+
if (project) project.name = name;
|
|
6633
|
+
}
|
|
6634
|
+
}
|
|
6635
|
+
function projectDisplayName(dir2, project) {
|
|
6636
|
+
return project?.name?.trim() || path4.basename(dir2) || dir2;
|
|
6637
|
+
}
|
|
6638
|
+
function projectDisplayLabels(projects) {
|
|
6639
|
+
const names = /* @__PURE__ */ new Map();
|
|
6640
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6641
|
+
for (const [dir2, p] of Object.entries(projects)) {
|
|
6642
|
+
const n = projectDisplayName(dir2, p);
|
|
6643
|
+
names.set(dir2, n);
|
|
6644
|
+
counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
6645
|
+
}
|
|
6646
|
+
const out = /* @__PURE__ */ new Map();
|
|
6647
|
+
for (const [dir2, n] of names) {
|
|
6648
|
+
if ((counts.get(n) ?? 0) > 1) {
|
|
6649
|
+
const parts = dir2.split(/[\\/]/).filter(Boolean);
|
|
6650
|
+
const parent = parts.length > 1 ? parts[parts.length - 2] : "";
|
|
6651
|
+
out.set(dir2, parent ? `${parent}/${n}` : n);
|
|
6652
|
+
} else {
|
|
6653
|
+
out.set(dir2, n);
|
|
6654
|
+
}
|
|
6655
|
+
}
|
|
6656
|
+
return out;
|
|
6657
|
+
}
|
|
6658
|
+
async function getProjectNames(api, machineId) {
|
|
6659
|
+
return parseProjectNames(await api.userKvGet(projectNamesKey(machineId))) ?? {};
|
|
6660
|
+
}
|
|
6661
|
+
async function setProjectName(api, machineId, dir2, name) {
|
|
6662
|
+
const map = await getProjectNames(api, machineId);
|
|
6663
|
+
const trimmed = name.trim();
|
|
6664
|
+
if (trimmed) map[dir2] = trimmed;
|
|
6665
|
+
else delete map[dir2];
|
|
6666
|
+
await api.userKvSet(projectNamesKey(machineId), Object.keys(map).length ? map : null);
|
|
6667
|
+
}
|
|
6400
6668
|
async function getMachineIcon(api, machineId) {
|
|
6401
6669
|
const v = await api.userKvGet(iconKey(machineId));
|
|
6402
6670
|
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
@@ -6408,9 +6676,14 @@ async function setMachineIcon(api, machineId, icon) {
|
|
|
6408
6676
|
async function loadMachine(api, machineId) {
|
|
6409
6677
|
const rec = await loadRawMachine(api, machineId);
|
|
6410
6678
|
if (!rec) return null;
|
|
6411
|
-
const [override, icon] = await Promise.all([
|
|
6679
|
+
const [override, icon, projNames] = await Promise.all([
|
|
6680
|
+
getMachineName(api, machineId),
|
|
6681
|
+
getMachineIcon(api, machineId),
|
|
6682
|
+
getProjectNames(api, machineId)
|
|
6683
|
+
]);
|
|
6412
6684
|
if (override) rec.name = override;
|
|
6413
6685
|
if (icon) rec.icon = icon;
|
|
6686
|
+
overlayProjectNames(rec, projNames);
|
|
6414
6687
|
return rec;
|
|
6415
6688
|
}
|
|
6416
6689
|
function daemonOnline(record2, now = Date.now()) {
|
|
@@ -6508,7 +6781,7 @@ async function clearDaemon(api, identity) {
|
|
|
6508
6781
|
function parseCommands(value) {
|
|
6509
6782
|
if (!Array.isArray(value)) return [];
|
|
6510
6783
|
return value.filter(
|
|
6511
|
-
(
|
|
6784
|
+
(c5) => !!c5 && typeof c5 === "object" && typeof c5.id === "string" && typeof c5.kind === "string"
|
|
6512
6785
|
);
|
|
6513
6786
|
}
|
|
6514
6787
|
async function enqueueMachineCommand(api, machineId, kind, args) {
|
|
@@ -6531,7 +6804,7 @@ async function clearMachineCommands(api, machineId, appliedIds) {
|
|
|
6531
6804
|
if (appliedIds.length === 0) return;
|
|
6532
6805
|
const key = commandKey(machineId);
|
|
6533
6806
|
const remaining = parseCommands(await api.userKvGet(key)).filter(
|
|
6534
|
-
(
|
|
6807
|
+
(c5) => !appliedIds.includes(c5.id)
|
|
6535
6808
|
);
|
|
6536
6809
|
await api.userKvSet(key, remaining.length ? remaining : null);
|
|
6537
6810
|
}
|
|
@@ -6555,6 +6828,290 @@ async function applyMachineCommand(api, identity, cmd) {
|
|
|
6555
6828
|
return `unknown command ${cmd.kind}`;
|
|
6556
6829
|
}
|
|
6557
6830
|
}
|
|
6831
|
+
var PROJECT_MARKERS = [
|
|
6832
|
+
".git",
|
|
6833
|
+
"package.json",
|
|
6834
|
+
"pyproject.toml",
|
|
6835
|
+
"Cargo.toml",
|
|
6836
|
+
"go.mod",
|
|
6837
|
+
"pom.xml",
|
|
6838
|
+
"build.gradle",
|
|
6839
|
+
"Gemfile",
|
|
6840
|
+
"composer.json",
|
|
6841
|
+
"requirements.txt",
|
|
6842
|
+
".standardagents"
|
|
6843
|
+
];
|
|
6844
|
+
var MAX_ENTRIES3 = 500;
|
|
6845
|
+
function resolveBrowsePath(input3) {
|
|
6846
|
+
const home = os8.homedir();
|
|
6847
|
+
let p = (input3 ?? "").trim();
|
|
6848
|
+
if (!p) return home;
|
|
6849
|
+
if (p === "~") return home;
|
|
6850
|
+
if (p.startsWith("~/")) p = path4.join(home, p.slice(2));
|
|
6851
|
+
return path4.resolve(p);
|
|
6852
|
+
}
|
|
6853
|
+
function markers(dirPath) {
|
|
6854
|
+
let repo = false;
|
|
6855
|
+
let project = false;
|
|
6856
|
+
for (const marker of PROJECT_MARKERS) {
|
|
6857
|
+
let hit = false;
|
|
6858
|
+
try {
|
|
6859
|
+
hit = fs5.existsSync(path4.join(dirPath, marker));
|
|
6860
|
+
} catch {
|
|
6861
|
+
hit = false;
|
|
6862
|
+
}
|
|
6863
|
+
if (!hit) continue;
|
|
6864
|
+
project = true;
|
|
6865
|
+
if (marker === ".git") repo = true;
|
|
6866
|
+
if (repo) break;
|
|
6867
|
+
}
|
|
6868
|
+
return { project, repo };
|
|
6869
|
+
}
|
|
6870
|
+
function browseDirectory(input3, opts = {}) {
|
|
6871
|
+
const home = os8.homedir();
|
|
6872
|
+
const abs = resolveBrowsePath(input3);
|
|
6873
|
+
const parent = path4.dirname(abs);
|
|
6874
|
+
const base = {
|
|
6875
|
+
path: abs,
|
|
6876
|
+
parent: parent === abs ? null : parent,
|
|
6877
|
+
home
|
|
6878
|
+
};
|
|
6879
|
+
let dirents;
|
|
6880
|
+
try {
|
|
6881
|
+
const stat = fs5.statSync(abs);
|
|
6882
|
+
if (!stat.isDirectory()) {
|
|
6883
|
+
return { ...base, entries: [], truncated: false, error: "Not a directory" };
|
|
6884
|
+
}
|
|
6885
|
+
dirents = fs5.readdirSync(abs, { withFileTypes: true });
|
|
6886
|
+
} catch (err) {
|
|
6887
|
+
const code = err?.code;
|
|
6888
|
+
const message = code === "EACCES" || code === "EPERM" ? "Permission denied" : code === "ENOENT" ? "Folder not found" : "Could not read this folder";
|
|
6889
|
+
return { ...base, entries: [], truncated: false, error: message };
|
|
6890
|
+
}
|
|
6891
|
+
const showHidden = !!opts.showHidden;
|
|
6892
|
+
const rawDirs = [];
|
|
6893
|
+
const rawFiles = [];
|
|
6894
|
+
for (const d of dirents) {
|
|
6895
|
+
const name = d.name;
|
|
6896
|
+
if (!showHidden && name.startsWith(".")) continue;
|
|
6897
|
+
let isDir = d.isDirectory();
|
|
6898
|
+
if (d.isSymbolicLink()) {
|
|
6899
|
+
try {
|
|
6900
|
+
isDir = fs5.statSync(path4.join(abs, name)).isDirectory();
|
|
6901
|
+
} catch {
|
|
6902
|
+
isDir = false;
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
if (isDir) {
|
|
6906
|
+
const { project, repo } = markers(path4.join(abs, name));
|
|
6907
|
+
rawDirs.push({ name, dir: true, project: project || void 0, repo: repo || void 0 });
|
|
6908
|
+
} else {
|
|
6909
|
+
rawFiles.push({ name, dir: false });
|
|
6910
|
+
}
|
|
6911
|
+
}
|
|
6912
|
+
const cmp = (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
6913
|
+
rawDirs.sort(cmp);
|
|
6914
|
+
rawFiles.sort(cmp);
|
|
6915
|
+
const all = [...rawDirs, ...rawFiles];
|
|
6916
|
+
const truncated = all.length > MAX_ENTRIES3;
|
|
6917
|
+
return { ...base, entries: truncated ? all.slice(0, MAX_ENTRIES3) : all, truncated };
|
|
6918
|
+
}
|
|
6919
|
+
function mkdirDirectory(parentInput, name, opts = {}) {
|
|
6920
|
+
const parent = resolveBrowsePath(parentInput);
|
|
6921
|
+
const clean2 = (name ?? "").trim();
|
|
6922
|
+
const listParent = () => browseDirectory(parent, opts);
|
|
6923
|
+
if (!clean2 || clean2 === "." || clean2 === ".." || clean2.includes("/") || clean2.includes("\\") || clean2.includes("\0")) {
|
|
6924
|
+
return { ...listParent(), error: "Invalid folder name." };
|
|
6925
|
+
}
|
|
6926
|
+
const target = path4.join(parent, clean2);
|
|
6927
|
+
if (path4.dirname(target) !== parent) {
|
|
6928
|
+
return { ...listParent(), error: "Invalid folder name." };
|
|
6929
|
+
}
|
|
6930
|
+
try {
|
|
6931
|
+
fs5.mkdirSync(target, { recursive: false });
|
|
6932
|
+
} catch (err) {
|
|
6933
|
+
const code = err?.code;
|
|
6934
|
+
const message = code === "EEXIST" ? "A folder with that name already exists." : code === "EACCES" || code === "EPERM" ? "Permission denied." : code === "ENOENT" ? "The parent folder no longer exists." : "Could not create the folder.";
|
|
6935
|
+
return { ...listParent(), error: message };
|
|
6936
|
+
}
|
|
6937
|
+
return listParent();
|
|
6938
|
+
}
|
|
6939
|
+
|
|
6940
|
+
// src/dir-picker.ts
|
|
6941
|
+
var c2 = {
|
|
6942
|
+
reset: "\x1B[0m",
|
|
6943
|
+
dim: "\x1B[2m",
|
|
6944
|
+
bold: "\x1B[1m",
|
|
6945
|
+
gray: themeGray,
|
|
6946
|
+
yellow: "\x1B[33m",
|
|
6947
|
+
red: "\x1B[31m",
|
|
6948
|
+
teal: "\x1B[38;5;37m"
|
|
6949
|
+
};
|
|
6950
|
+
function localBrowseBackend(label = "this machine") {
|
|
6951
|
+
return {
|
|
6952
|
+
label,
|
|
6953
|
+
list: async (p, showHidden) => browseDirectory(p, { showHidden }),
|
|
6954
|
+
mkdir: async (parent, name, showHidden) => mkdirDirectory(parent, name, { showHidden })
|
|
6955
|
+
};
|
|
6956
|
+
}
|
|
6957
|
+
function parseBrowseResult(value) {
|
|
6958
|
+
if (!value || typeof value !== "object") return null;
|
|
6959
|
+
const r = value;
|
|
6960
|
+
if (typeof r.path !== "string" || !Array.isArray(r.entries)) return null;
|
|
6961
|
+
return {
|
|
6962
|
+
path: r.path,
|
|
6963
|
+
parent: typeof r.parent === "string" ? r.parent : null,
|
|
6964
|
+
home: typeof r.home === "string" ? r.home : "",
|
|
6965
|
+
entries: r.entries.filter(
|
|
6966
|
+
(e) => !!e && typeof e.name === "string"
|
|
6967
|
+
),
|
|
6968
|
+
truncated: r.truncated === true,
|
|
6969
|
+
error: typeof r.error === "string" ? r.error : void 0
|
|
6970
|
+
};
|
|
6971
|
+
}
|
|
6972
|
+
function remoteBrowseBackend(api, machineId, label) {
|
|
6973
|
+
const rpc = async (req) => {
|
|
6974
|
+
const nonce = crypto.randomBytes(8).toString("hex");
|
|
6975
|
+
await writeFsRequest(api, machineId, { nonce, ...req });
|
|
6976
|
+
const deadline = Date.now() + 15e3;
|
|
6977
|
+
while (Date.now() < deadline) {
|
|
6978
|
+
await new Promise((r) => setTimeout(r, 700));
|
|
6979
|
+
const res = await readFsResponse(api, machineId).catch(() => null);
|
|
6980
|
+
if (res && res.nonce === nonce) {
|
|
6981
|
+
if (!res.ok) throw new Error(res.error || "Browse failed on the remote machine.");
|
|
6982
|
+
const parsed = parseBrowseResult(res.result);
|
|
6983
|
+
if (!parsed) throw new Error("The remote machine sent an unreadable listing.");
|
|
6984
|
+
return parsed;
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
throw new Error(`${label} didn't answer \u2014 is its daemon online?`);
|
|
6988
|
+
};
|
|
6989
|
+
return {
|
|
6990
|
+
label,
|
|
6991
|
+
list: (p, showHidden) => rpc({ op: "list", path: p ?? "~", show_hidden: showHidden }),
|
|
6992
|
+
mkdir: (parent, name, showHidden) => rpc({ op: "mkdir", path: parent, name, show_hidden: showHidden })
|
|
6993
|
+
};
|
|
6994
|
+
}
|
|
6995
|
+
function joinBrowsed(parent, name) {
|
|
6996
|
+
const sep = parent.includes("\\") && !parent.includes("/") ? "\\" : "/";
|
|
6997
|
+
return parent.endsWith(sep) ? parent + name : parent + sep + name;
|
|
6998
|
+
}
|
|
6999
|
+
function tail(p, max = 44) {
|
|
7000
|
+
return p.length > max ? "\u2026" + p.slice(-(max - 1)) : p;
|
|
7001
|
+
}
|
|
7002
|
+
var KEY_HINTS = `enter open \xB7 s use this directory \xB7 . hidden files \xB7 esc back`;
|
|
7003
|
+
async function pickDirectory(tui, backend, opts = {}) {
|
|
7004
|
+
let showHidden = false;
|
|
7005
|
+
let page = 0;
|
|
7006
|
+
const spin = opts.loader?.(`Reading ${backend.label}`);
|
|
7007
|
+
let listing;
|
|
7008
|
+
try {
|
|
7009
|
+
listing = await backend.list(opts.startPath ?? null, showHidden);
|
|
7010
|
+
} catch (e) {
|
|
7011
|
+
spin?.stop();
|
|
7012
|
+
tui.print(`${c2.red}\u2717${c2.reset} ${c2.gray}${e instanceof Error ? e.message : String(e)}${c2.reset}`);
|
|
7013
|
+
return null;
|
|
7014
|
+
}
|
|
7015
|
+
spin?.stop();
|
|
7016
|
+
const title = () => `${c2.bold}${gradientText(`Browse ${backend.label}`)}${c2.reset} ${c2.teal}${tail(listing.path, 48)}${c2.reset}` + (showHidden ? ` ${c2.dim}\xB7 hidden shown${c2.reset}` : "");
|
|
7017
|
+
const footer = (error) => error ? `${c2.yellow}${error}${c2.reset} ${c2.dim}${KEY_HINTS}${c2.reset}` : `${c2.dim}${KEY_HINTS}${c2.reset}`;
|
|
7018
|
+
const buildItems = () => {
|
|
7019
|
+
const pageSize = Math.max(5, Math.min(14, (process.stdout.rows || 24) - 9));
|
|
7020
|
+
const pages = Math.max(1, Math.ceil(listing.entries.length / pageSize));
|
|
7021
|
+
if (page >= pages) page = 0;
|
|
7022
|
+
const slice = listing.entries.slice(page * pageSize, (page + 1) * pageSize);
|
|
7023
|
+
const items = [];
|
|
7024
|
+
if (listing.parent) items.push({ label: "..", hint: "up a level", value: { kind: "up", to: listing.parent } });
|
|
7025
|
+
for (const entry of slice) {
|
|
7026
|
+
if (entry.dir) {
|
|
7027
|
+
items.push({
|
|
7028
|
+
label: `${entry.name}/`,
|
|
7029
|
+
hint: entry.repo ? "git repo" : entry.project ? "project" : "",
|
|
7030
|
+
value: { kind: "open", to: joinBrowsed(listing.path, entry.name) }
|
|
7031
|
+
});
|
|
7032
|
+
} else {
|
|
7033
|
+
items.push({ label: entry.name, value: { kind: "noop" }, disabled: true });
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
7036
|
+
if (listing.entries.length === 0) {
|
|
7037
|
+
items.push({ label: "(empty directory)", value: { kind: "noop" }, disabled: true });
|
|
7038
|
+
}
|
|
7039
|
+
if (pages > 1) {
|
|
7040
|
+
const remaining = listing.entries.length - slice.length;
|
|
7041
|
+
items.push({
|
|
7042
|
+
label: `\u2192 more (page ${page + 1}/${pages}, ${remaining} more entr${remaining === 1 ? "y" : "ies"})`,
|
|
7043
|
+
value: { kind: "page" }
|
|
7044
|
+
});
|
|
7045
|
+
}
|
|
7046
|
+
if (listing.truncated) {
|
|
7047
|
+
items.push({ label: "\u2026listing capped \u2014 very large directory", value: { kind: "noop" }, disabled: true });
|
|
7048
|
+
}
|
|
7049
|
+
items.push({ label: "\uFF0B New folder here\u2026", value: { kind: "mkdir" } });
|
|
7050
|
+
return items;
|
|
7051
|
+
};
|
|
7052
|
+
const menu = tui.openLiveSelect({
|
|
7053
|
+
title: title(),
|
|
7054
|
+
items: buildItems(),
|
|
7055
|
+
footer: footer(listing.error),
|
|
7056
|
+
keys: ["s", "."]
|
|
7057
|
+
});
|
|
7058
|
+
const refresh = () => menu.update({ title: title(), items: buildItems(), footer: footer(listing.error) });
|
|
7059
|
+
const navigate = async (to, index) => {
|
|
7060
|
+
menu.setBusy(index);
|
|
7061
|
+
try {
|
|
7062
|
+
listing = await backend.list(to, showHidden);
|
|
7063
|
+
page = 0;
|
|
7064
|
+
refresh();
|
|
7065
|
+
} catch (e) {
|
|
7066
|
+
menu.update({ footer: footer(e instanceof Error ? e.message : String(e)) });
|
|
7067
|
+
}
|
|
7068
|
+
};
|
|
7069
|
+
for (; ; ) {
|
|
7070
|
+
const ev = await menu.next();
|
|
7071
|
+
if (ev.kind === "cancel") {
|
|
7072
|
+
menu.close();
|
|
7073
|
+
return null;
|
|
7074
|
+
}
|
|
7075
|
+
if (ev.kind === "key") {
|
|
7076
|
+
if (ev.name === "s") {
|
|
7077
|
+
menu.close();
|
|
7078
|
+
return listing.path;
|
|
7079
|
+
}
|
|
7080
|
+
if (ev.name === ".") {
|
|
7081
|
+
showHidden = !showHidden;
|
|
7082
|
+
await navigate(listing.path, ev.index);
|
|
7083
|
+
}
|
|
7084
|
+
continue;
|
|
7085
|
+
}
|
|
7086
|
+
if (ev.kind === "edit") {
|
|
7087
|
+
menu.setBusy(ev.index);
|
|
7088
|
+
try {
|
|
7089
|
+
const after = await backend.mkdir(listing.path, ev.text, showHidden);
|
|
7090
|
+
if (after.error) {
|
|
7091
|
+
listing = after;
|
|
7092
|
+
page = 0;
|
|
7093
|
+
menu.update({ title: title(), items: buildItems(), footer: footer(after.error) });
|
|
7094
|
+
} else {
|
|
7095
|
+
listing = await backend.list(joinBrowsed(listing.path, ev.text), showHidden);
|
|
7096
|
+
page = 0;
|
|
7097
|
+
refresh();
|
|
7098
|
+
}
|
|
7099
|
+
} catch (e) {
|
|
7100
|
+
menu.update({ footer: footer(e instanceof Error ? e.message : String(e)) });
|
|
7101
|
+
}
|
|
7102
|
+
continue;
|
|
7103
|
+
}
|
|
7104
|
+
const move = ev.value;
|
|
7105
|
+
if (move.kind === "up" || move.kind === "open") {
|
|
7106
|
+
await navigate(move.to, ev.index);
|
|
7107
|
+
} else if (move.kind === "page") {
|
|
7108
|
+
page += 1;
|
|
7109
|
+
refresh();
|
|
7110
|
+
} else if (move.kind === "mkdir") {
|
|
7111
|
+
menu.beginEdit(ev.index, { placeholder: "folder name\u2026", prefix: "\uFF0B " });
|
|
7112
|
+
}
|
|
7113
|
+
}
|
|
7114
|
+
}
|
|
6558
7115
|
|
|
6559
7116
|
// src/relay.ts
|
|
6560
7117
|
var APPROVAL_REQUEST_KEY = "approval_request";
|
|
@@ -6704,114 +7261,6 @@ var HubSocket = class {
|
|
|
6704
7261
|
this.ws?.close();
|
|
6705
7262
|
}
|
|
6706
7263
|
};
|
|
6707
|
-
var PROJECT_MARKERS = [
|
|
6708
|
-
".git",
|
|
6709
|
-
"package.json",
|
|
6710
|
-
"pyproject.toml",
|
|
6711
|
-
"Cargo.toml",
|
|
6712
|
-
"go.mod",
|
|
6713
|
-
"pom.xml",
|
|
6714
|
-
"build.gradle",
|
|
6715
|
-
"Gemfile",
|
|
6716
|
-
"composer.json",
|
|
6717
|
-
"requirements.txt",
|
|
6718
|
-
".standardagents"
|
|
6719
|
-
];
|
|
6720
|
-
var MAX_ENTRIES3 = 500;
|
|
6721
|
-
function resolveBrowsePath(input3) {
|
|
6722
|
-
const home = os8.homedir();
|
|
6723
|
-
let p = (input3 ?? "").trim();
|
|
6724
|
-
if (!p) return home;
|
|
6725
|
-
if (p === "~") return home;
|
|
6726
|
-
if (p.startsWith("~/")) p = path4.join(home, p.slice(2));
|
|
6727
|
-
return path4.resolve(p);
|
|
6728
|
-
}
|
|
6729
|
-
function markers(dirPath) {
|
|
6730
|
-
let repo = false;
|
|
6731
|
-
let project = false;
|
|
6732
|
-
for (const marker of PROJECT_MARKERS) {
|
|
6733
|
-
let hit = false;
|
|
6734
|
-
try {
|
|
6735
|
-
hit = fs5.existsSync(path4.join(dirPath, marker));
|
|
6736
|
-
} catch {
|
|
6737
|
-
hit = false;
|
|
6738
|
-
}
|
|
6739
|
-
if (!hit) continue;
|
|
6740
|
-
project = true;
|
|
6741
|
-
if (marker === ".git") repo = true;
|
|
6742
|
-
if (repo) break;
|
|
6743
|
-
}
|
|
6744
|
-
return { project, repo };
|
|
6745
|
-
}
|
|
6746
|
-
function browseDirectory(input3, opts = {}) {
|
|
6747
|
-
const home = os8.homedir();
|
|
6748
|
-
const abs = resolveBrowsePath(input3);
|
|
6749
|
-
const parent = path4.dirname(abs);
|
|
6750
|
-
const base = {
|
|
6751
|
-
path: abs,
|
|
6752
|
-
parent: parent === abs ? null : parent,
|
|
6753
|
-
home
|
|
6754
|
-
};
|
|
6755
|
-
let dirents;
|
|
6756
|
-
try {
|
|
6757
|
-
const stat = fs5.statSync(abs);
|
|
6758
|
-
if (!stat.isDirectory()) {
|
|
6759
|
-
return { ...base, entries: [], truncated: false, error: "Not a directory" };
|
|
6760
|
-
}
|
|
6761
|
-
dirents = fs5.readdirSync(abs, { withFileTypes: true });
|
|
6762
|
-
} catch (err) {
|
|
6763
|
-
const code = err?.code;
|
|
6764
|
-
const message = code === "EACCES" || code === "EPERM" ? "Permission denied" : code === "ENOENT" ? "Folder not found" : "Could not read this folder";
|
|
6765
|
-
return { ...base, entries: [], truncated: false, error: message };
|
|
6766
|
-
}
|
|
6767
|
-
const showHidden = !!opts.showHidden;
|
|
6768
|
-
const rawDirs = [];
|
|
6769
|
-
const rawFiles = [];
|
|
6770
|
-
for (const d of dirents) {
|
|
6771
|
-
const name = d.name;
|
|
6772
|
-
if (!showHidden && name.startsWith(".")) continue;
|
|
6773
|
-
let isDir = d.isDirectory();
|
|
6774
|
-
if (d.isSymbolicLink()) {
|
|
6775
|
-
try {
|
|
6776
|
-
isDir = fs5.statSync(path4.join(abs, name)).isDirectory();
|
|
6777
|
-
} catch {
|
|
6778
|
-
isDir = false;
|
|
6779
|
-
}
|
|
6780
|
-
}
|
|
6781
|
-
if (isDir) {
|
|
6782
|
-
const { project, repo } = markers(path4.join(abs, name));
|
|
6783
|
-
rawDirs.push({ name, dir: true, project: project || void 0, repo: repo || void 0 });
|
|
6784
|
-
} else {
|
|
6785
|
-
rawFiles.push({ name, dir: false });
|
|
6786
|
-
}
|
|
6787
|
-
}
|
|
6788
|
-
const cmp = (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
6789
|
-
rawDirs.sort(cmp);
|
|
6790
|
-
rawFiles.sort(cmp);
|
|
6791
|
-
const all = [...rawDirs, ...rawFiles];
|
|
6792
|
-
const truncated = all.length > MAX_ENTRIES3;
|
|
6793
|
-
return { ...base, entries: truncated ? all.slice(0, MAX_ENTRIES3) : all, truncated };
|
|
6794
|
-
}
|
|
6795
|
-
function mkdirDirectory(parentInput, name, opts = {}) {
|
|
6796
|
-
const parent = resolveBrowsePath(parentInput);
|
|
6797
|
-
const clean2 = (name ?? "").trim();
|
|
6798
|
-
const listParent = () => browseDirectory(parent, opts);
|
|
6799
|
-
if (!clean2 || clean2 === "." || clean2 === ".." || clean2.includes("/") || clean2.includes("\\") || clean2.includes("\0")) {
|
|
6800
|
-
return { ...listParent(), error: "Invalid folder name." };
|
|
6801
|
-
}
|
|
6802
|
-
const target = path4.join(parent, clean2);
|
|
6803
|
-
if (path4.dirname(target) !== parent) {
|
|
6804
|
-
return { ...listParent(), error: "Invalid folder name." };
|
|
6805
|
-
}
|
|
6806
|
-
try {
|
|
6807
|
-
fs5.mkdirSync(target, { recursive: false });
|
|
6808
|
-
} catch (err) {
|
|
6809
|
-
const code = err?.code;
|
|
6810
|
-
const message = code === "EEXIST" ? "A folder with that name already exists." : code === "EACCES" || code === "EPERM" ? "Permission denied." : code === "ENOENT" ? "The parent folder no longer exists." : "Could not create the folder.";
|
|
6811
|
-
return { ...listParent(), error: message };
|
|
6812
|
-
}
|
|
6813
|
-
return listParent();
|
|
6814
|
-
}
|
|
6815
7264
|
var PKG_NAME = "@standardagents/code";
|
|
6816
7265
|
var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}`;
|
|
6817
7266
|
var CACHE_REL_DIR = ".config/standardagents-cli";
|
|
@@ -7576,7 +8025,7 @@ function serviceStatus() {
|
|
|
7576
8025
|
}
|
|
7577
8026
|
|
|
7578
8027
|
// src/daemon-cli.ts
|
|
7579
|
-
var
|
|
8028
|
+
var c3 = {
|
|
7580
8029
|
reset: "\x1B[0m",
|
|
7581
8030
|
dim: "\x1B[2m",
|
|
7582
8031
|
bold: "\x1B[1m",
|
|
@@ -7589,9 +8038,9 @@ function usage() {
|
|
|
7589
8038
|
stdout.write(
|
|
7590
8039
|
[
|
|
7591
8040
|
"",
|
|
7592
|
-
`${
|
|
8041
|
+
`${c3.bold}standardcode daemon${c3.reset} \u2014 headless execution client for this machine`,
|
|
7593
8042
|
"",
|
|
7594
|
-
`${
|
|
8043
|
+
`${c3.bold}Commands${c3.reset}`,
|
|
7595
8044
|
" install [--endpoint url] Sign in (if needed), name this machine, and install",
|
|
7596
8045
|
" the always-on service (launchd / systemd).",
|
|
7597
8046
|
" uninstall Stop and remove the service.",
|
|
@@ -7631,11 +8080,11 @@ async function ensureSignedIn(endpoint) {
|
|
|
7631
8080
|
const api2 = new ApiClient(endpoint, stored.access_token);
|
|
7632
8081
|
const check2 = await api2.verifyDetailed();
|
|
7633
8082
|
if (check2.ok) return api2;
|
|
7634
|
-
stdout.write(`${
|
|
8083
|
+
stdout.write(`${c3.yellow}Saved sign-in for ${host} failed:${c3.reset} ${check2.reason}
|
|
7635
8084
|
|
|
7636
8085
|
`);
|
|
7637
8086
|
}
|
|
7638
|
-
stdout.write(`${
|
|
8087
|
+
stdout.write(`${c3.bold}Sign in to Standard Code${c3.reset} ${c3.dim}(${host})${c3.reset}
|
|
7639
8088
|
|
|
7640
8089
|
`);
|
|
7641
8090
|
const token = await deviceLogin(endpoint);
|
|
@@ -7646,7 +8095,7 @@ async function ensureSignedIn(endpoint) {
|
|
|
7646
8095
|
{ endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
|
|
7647
8096
|
{ updateDefault: false }
|
|
7648
8097
|
);
|
|
7649
|
-
stdout.write(`${
|
|
8098
|
+
stdout.write(`${c3.green}\u2713${c3.reset} Signed in to ${c3.teal}${host}${c3.reset}
|
|
7650
8099
|
|
|
7651
8100
|
`);
|
|
7652
8101
|
return api;
|
|
@@ -7659,26 +8108,26 @@ async function installCommand(endpointFlag) {
|
|
|
7659
8108
|
const suggested = machineDisplayName(existing ?? { hostname: os8.hostname(), id: identity.machine_id });
|
|
7660
8109
|
const rl = readline2.createInterface({ input: stdin, output: stdout });
|
|
7661
8110
|
const answer = (await rl.question(
|
|
7662
|
-
`${
|
|
8111
|
+
`${c3.bold}Machine name${c3.reset} ${c3.dim}(shown in the session picker)${c3.reset} [${suggested}]: `
|
|
7663
8112
|
)).trim();
|
|
7664
8113
|
rl.close();
|
|
7665
8114
|
if (answer) await setMachineName(api, identity.machine_id, answer);
|
|
7666
8115
|
await updateOwnMachineRecord(api, identity);
|
|
7667
8116
|
const displayName = answer || suggested;
|
|
7668
|
-
stdout.write(`${
|
|
8117
|
+
stdout.write(`${c3.dim}Installing the always-on service\u2026${c3.reset}
|
|
7669
8118
|
`);
|
|
7670
8119
|
const extra = endpointFlag ? ["--endpoint", endpoint] : [];
|
|
7671
8120
|
const result = installService(resolveDaemonCommand(extra), endpointFlag ? endpoint : void 0);
|
|
7672
8121
|
if (!result.ok) {
|
|
7673
|
-
stdout.write(`${
|
|
8122
|
+
stdout.write(`${c3.red}\u2717${c3.reset} ${result.detail}
|
|
7674
8123
|
`);
|
|
7675
|
-
if (result.manualHint) stdout.write(`${
|
|
8124
|
+
if (result.manualHint) stdout.write(`${c3.dim}${result.manualHint}${c3.reset}
|
|
7676
8125
|
`);
|
|
7677
8126
|
process.exit(1);
|
|
7678
8127
|
}
|
|
7679
|
-
stdout.write(`${
|
|
8128
|
+
stdout.write(`${c3.green}\u2713${c3.reset} ${result.detail}
|
|
7680
8129
|
`);
|
|
7681
|
-
stdout.write(`${
|
|
8130
|
+
stdout.write(`${c3.dim}Waiting for the daemon's first heartbeat\u2026${c3.reset}
|
|
7682
8131
|
`);
|
|
7683
8132
|
const deadline = Date.now() + 6e4;
|
|
7684
8133
|
let alive = false;
|
|
@@ -7692,17 +8141,17 @@ async function installCommand(endpointFlag) {
|
|
|
7692
8141
|
}
|
|
7693
8142
|
if (alive) {
|
|
7694
8143
|
stdout.write(
|
|
7695
|
-
`${
|
|
8144
|
+
`${c3.green}\u2713${c3.reset} ${c3.bold}${displayName}${c3.reset} is online.
|
|
7696
8145
|
|
|
7697
8146
|
Sessions started elsewhere can now run on this machine.
|
|
7698
|
-
${
|
|
7699
|
-
or add one now: standardcode daemon add-project <path>${
|
|
8147
|
+
${c3.dim}Projects register automatically when you run standardcode in a directory here,
|
|
8148
|
+
or add one now: standardcode daemon add-project <path>${c3.reset}
|
|
7700
8149
|
`
|
|
7701
8150
|
);
|
|
7702
8151
|
} else {
|
|
7703
8152
|
stdout.write(
|
|
7704
|
-
`${
|
|
7705
|
-
${
|
|
8153
|
+
`${c3.yellow}\u26A0${c3.reset} The service installed but no heartbeat arrived yet.
|
|
8154
|
+
${c3.dim}Check ~/.standardagents/daemon.log and \`standardcode daemon status\`.${c3.reset}
|
|
7706
8155
|
`
|
|
7707
8156
|
);
|
|
7708
8157
|
}
|
|
@@ -7710,16 +8159,16 @@ ${c2.dim}Check ~/.standardagents/daemon.log and \`standardcode daemon status\`.$
|
|
|
7710
8159
|
async function statusCommand() {
|
|
7711
8160
|
const status = serviceStatus();
|
|
7712
8161
|
const identity = loadMachineIdentity();
|
|
7713
|
-
stdout.write(`${
|
|
8162
|
+
stdout.write(`${c3.bold}Service:${c3.reset} ${status.detail}
|
|
7714
8163
|
`);
|
|
7715
8164
|
const endpoint = resolveEndpoint();
|
|
7716
8165
|
const cred = getCredential(endpoint);
|
|
7717
8166
|
if (!cred) {
|
|
7718
8167
|
stdout.write(
|
|
7719
|
-
`${
|
|
8168
|
+
`${c3.bold}Machine:${c3.reset} ${os8.hostname()} ${c3.dim}(${identity.machine_id})${c3.reset}
|
|
7720
8169
|
`
|
|
7721
8170
|
);
|
|
7722
|
-
stdout.write(`${
|
|
8171
|
+
stdout.write(`${c3.bold}Account:${c3.reset} ${c3.yellow}not signed in to ${endpoint}${c3.reset}
|
|
7723
8172
|
`);
|
|
7724
8173
|
return;
|
|
7725
8174
|
}
|
|
@@ -7727,33 +8176,33 @@ async function statusCommand() {
|
|
|
7727
8176
|
const api = new ApiClient(endpoint, cred.access_token);
|
|
7728
8177
|
const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
7729
8178
|
stdout.write(
|
|
7730
|
-
`${
|
|
8179
|
+
`${c3.bold}Machine:${c3.reset} ${machineDisplayName(record2 ?? { hostname: os8.hostname(), id: identity.machine_id })} ${c3.dim}(${identity.machine_id})${c3.reset}
|
|
7731
8180
|
`
|
|
7732
8181
|
);
|
|
7733
8182
|
if (!record2) {
|
|
7734
|
-
stdout.write(`${
|
|
8183
|
+
stdout.write(`${c3.bold}Registry:${c3.reset} not registered yet
|
|
7735
8184
|
`);
|
|
7736
8185
|
return;
|
|
7737
8186
|
}
|
|
7738
8187
|
const online = daemonOnline(record2);
|
|
7739
8188
|
const seen = record2.daemon ? `${Math.round((Date.now() - record2.daemon.last_seen_at) / 1e3)}s ago (v${record2.daemon.version})` : "never";
|
|
7740
|
-
stdout.write(`${
|
|
8189
|
+
stdout.write(`${c3.bold}Registry:${c3.reset} ${online ? `${c3.green}online${c3.reset}` : `${c3.yellow}offline${c3.reset}`} \xB7 last heartbeat ${seen}
|
|
7741
8190
|
`);
|
|
7742
8191
|
const projects = Object.keys(record2.projects);
|
|
7743
|
-
stdout.write(`${
|
|
8192
|
+
stdout.write(`${c3.bold}Projects:${c3.reset} ${projects.length ? "" : c3.dim + "none registered" + c3.reset}
|
|
7744
8193
|
`);
|
|
7745
|
-
for (const p of projects.sort()) stdout.write(` ${
|
|
8194
|
+
for (const p of projects.sort()) stdout.write(` ${c3.dim}${p}${c3.reset}
|
|
7746
8195
|
`);
|
|
7747
8196
|
}
|
|
7748
8197
|
async function projectCommand(action, target) {
|
|
7749
8198
|
if (!target) {
|
|
7750
|
-
stdout.write(`${
|
|
8199
|
+
stdout.write(`${c3.red}\u2717${c3.reset} Expected a project path.
|
|
7751
8200
|
`);
|
|
7752
8201
|
process.exit(1);
|
|
7753
8202
|
}
|
|
7754
8203
|
const dir2 = path4.resolve(target);
|
|
7755
8204
|
if (action === "add" && !fs5.existsSync(dir2)) {
|
|
7756
|
-
stdout.write(`${
|
|
8205
|
+
stdout.write(`${c3.red}\u2717${c3.reset} ${dir2} does not exist on this machine.
|
|
7757
8206
|
`);
|
|
7758
8207
|
process.exit(1);
|
|
7759
8208
|
}
|
|
@@ -7764,11 +8213,11 @@ async function projectCommand(action, target) {
|
|
|
7764
8213
|
const displayName = machineDisplayName(record2 ?? { hostname: os8.hostname(), id: identity.machine_id });
|
|
7765
8214
|
if (action === "add") {
|
|
7766
8215
|
await registerProject(api, identity, dir2);
|
|
7767
|
-
stdout.write(`${
|
|
8216
|
+
stdout.write(`${c3.green}\u2713${c3.reset} Registered ${dir2} for remote sessions on ${displayName}.
|
|
7768
8217
|
`);
|
|
7769
8218
|
} else {
|
|
7770
8219
|
await unregisterProject(api, identity, dir2);
|
|
7771
|
-
stdout.write(`${
|
|
8220
|
+
stdout.write(`${c3.green}\u2713${c3.reset} Removed ${dir2} from this machine's projects.
|
|
7772
8221
|
`);
|
|
7773
8222
|
}
|
|
7774
8223
|
}
|
|
@@ -7784,7 +8233,7 @@ async function runDaemonCommand(argv) {
|
|
|
7784
8233
|
return;
|
|
7785
8234
|
case "uninstall": {
|
|
7786
8235
|
const result = uninstallService();
|
|
7787
|
-
stdout.write(`${result.ok ?
|
|
8236
|
+
stdout.write(`${result.ok ? c3.green + "\u2713" : c3.red + "\u2717"}${c3.reset} ${result.detail}
|
|
7788
8237
|
`);
|
|
7789
8238
|
const ep = resolveEndpoint(endpoint);
|
|
7790
8239
|
const cred = getCredential(ep);
|
|
@@ -7817,7 +8266,7 @@ async function runDaemonCommand(argv) {
|
|
|
7817
8266
|
}
|
|
7818
8267
|
|
|
7819
8268
|
// src/index.ts
|
|
7820
|
-
var
|
|
8269
|
+
var c4 = {
|
|
7821
8270
|
reset: "\x1B[0m",
|
|
7822
8271
|
dim: "\x1B[2m",
|
|
7823
8272
|
bold: "\x1B[1m",
|
|
@@ -7846,10 +8295,10 @@ function printUsage() {
|
|
|
7846
8295
|
stdout.write(
|
|
7847
8296
|
[
|
|
7848
8297
|
"",
|
|
7849
|
-
`${
|
|
8298
|
+
`${c4.bold}Usage${c4.reset}`,
|
|
7850
8299
|
" standardcode [options] [dir]",
|
|
7851
8300
|
"",
|
|
7852
|
-
`${
|
|
8301
|
+
`${c4.bold}Options${c4.reset}`,
|
|
7853
8302
|
" -e, --endpoint [url] Use a different Standard Agents instance for this run",
|
|
7854
8303
|
" (default: https://api.standardcode.ai).",
|
|
7855
8304
|
" If url is omitted, prompt for it.",
|
|
@@ -7917,15 +8366,15 @@ function parseArgs2(args) {
|
|
|
7917
8366
|
}
|
|
7918
8367
|
function printCommandBlock(tui, command, output4, ok, where) {
|
|
7919
8368
|
tui.print("");
|
|
7920
|
-
const note = where ? ` ${
|
|
7921
|
-
tui.print(`${
|
|
8369
|
+
const note = where ? ` ${c4.dim}(ran on ${where})${c4.reset}` : "";
|
|
8370
|
+
tui.print(`${c4.magenta}!${c4.reset} ${c4.bold}${command}${c4.reset}${note}`);
|
|
7922
8371
|
const body = (output4 ?? "").replace(/\s+$/, "");
|
|
7923
8372
|
if (body) {
|
|
7924
8373
|
for (const line of body.split("\n")) {
|
|
7925
|
-
tui.print(` ${ok ?
|
|
8374
|
+
tui.print(` ${ok ? c4.dim : c4.red}${line}${c4.reset}`);
|
|
7926
8375
|
}
|
|
7927
8376
|
} else {
|
|
7928
|
-
tui.print(` ${
|
|
8377
|
+
tui.print(` ${c4.dim}(no output)${c4.reset}`);
|
|
7929
8378
|
}
|
|
7930
8379
|
tui.print("");
|
|
7931
8380
|
}
|
|
@@ -7936,7 +8385,7 @@ function printAssistant(tui, text) {
|
|
|
7936
8385
|
let dotted = false;
|
|
7937
8386
|
for (const line of renderStreamingMarkdown(text, cols2)) {
|
|
7938
8387
|
if (!dotted && line.trim()) {
|
|
7939
|
-
tui.print(`${
|
|
8388
|
+
tui.print(`${c4.gray}\u2022${c4.reset} ${line}`);
|
|
7940
8389
|
dotted = true;
|
|
7941
8390
|
} else {
|
|
7942
8391
|
tui.print(` ${line}`);
|
|
@@ -7951,7 +8400,7 @@ function startLoader(label) {
|
|
|
7951
8400
|
const draw = () => {
|
|
7952
8401
|
const now = Date.now();
|
|
7953
8402
|
const f = frames[Math.floor(now / 70) % frames.length];
|
|
7954
|
-
stdout.write(`\r\x1B[K${pad}${brandCycleColor(now)}${f}${
|
|
8403
|
+
stdout.write(`\r\x1B[K${pad}${brandCycleColor(now)}${f}${c4.reset} ${c4.dim}${label}\u2026${c4.reset}`);
|
|
7955
8404
|
};
|
|
7956
8405
|
draw();
|
|
7957
8406
|
const timer = setInterval(draw, 70);
|
|
@@ -7967,12 +8416,12 @@ function farewell(stoppedProcs = 0) {
|
|
|
7967
8416
|
if (stoppedProcs > 0) {
|
|
7968
8417
|
stdout.write(
|
|
7969
8418
|
`
|
|
7970
|
-
${
|
|
8419
|
+
${c4.cyan}\u2699${c4.reset} Stopped ${stoppedProcs} background process${stoppedProcs === 1 ? "" : "es"}.
|
|
7971
8420
|
`
|
|
7972
8421
|
);
|
|
7973
8422
|
}
|
|
7974
8423
|
stdout.write(`
|
|
7975
|
-
${
|
|
8424
|
+
${c4.teal}\u25C7${c4.reset} ${c4.dim}Standard Code \u2014 see you soon.${c4.reset}
|
|
7976
8425
|
`);
|
|
7977
8426
|
}
|
|
7978
8427
|
function printWelcome(endpoint, projectDir) {
|
|
@@ -7986,10 +8435,10 @@ function printWelcome(endpoint, projectDir) {
|
|
|
7986
8435
|
const metaWidth = Math.max(1, terminalColumns - pad.length - markWidth - 3 - 1);
|
|
7987
8436
|
const displayDir = truncateMiddle(dir2, metaWidth);
|
|
7988
8437
|
const meta = [
|
|
7989
|
-
`${
|
|
7990
|
-
`${
|
|
7991
|
-
...endpoint === PRODUCTION_ENDPOINT ? [] : [`${
|
|
7992
|
-
`${
|
|
8438
|
+
`${c4.bold}${gradientText("Standard Code")}${c4.reset}${version ? ` ${c4.dim}v${version}${c4.reset}` : ""}`,
|
|
8439
|
+
`${c4.dim}terminal coding agent${c4.reset}`,
|
|
8440
|
+
...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c4.teal}${host}${c4.reset}`],
|
|
8441
|
+
`${c4.dim}${displayDir}${c4.reset}`
|
|
7993
8442
|
];
|
|
7994
8443
|
const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
|
|
7995
8444
|
stdout.write("\n");
|
|
@@ -8003,11 +8452,11 @@ function printWelcome(endpoint, projectDir) {
|
|
|
8003
8452
|
}
|
|
8004
8453
|
function colorActivity(line) {
|
|
8005
8454
|
const m = line.match(/^(\s*)([✓✗⛔])\s?([\s\S]*)$/);
|
|
8006
|
-
if (!m) return `${
|
|
8455
|
+
if (!m) return `${c4.dim}${line}${c4.reset}`;
|
|
8007
8456
|
const [, indent, glyph, rest] = m;
|
|
8008
8457
|
if (glyph === "\u2713") {
|
|
8009
|
-
const body = rest.replace(/\s(\([^()]*\))\s*$/, ` ${
|
|
8010
|
-
return `${indent}${
|
|
8458
|
+
const body = rest.replace(/\s(\([^()]*\))\s*$/, ` ${c4.dim}$1${c4.reset}`);
|
|
8459
|
+
return `${indent}${c4.green}\u2713${c4.reset} ${body}`;
|
|
8011
8460
|
}
|
|
8012
8461
|
if (glyph === "\u2717") {
|
|
8013
8462
|
const ERR_MAX_LINES = 7;
|
|
@@ -8015,15 +8464,15 @@ function colorActivity(line) {
|
|
|
8015
8464
|
const shown = lines.slice(0, ERR_MAX_LINES);
|
|
8016
8465
|
const hidden = lines.length - shown.length;
|
|
8017
8466
|
const body = shown.map(
|
|
8018
|
-
(l, i) => i === 0 ? `${indent}${
|
|
8467
|
+
(l, i) => i === 0 ? `${indent}${c4.red}\u2717 ${l}${c4.reset}` : `${indent}${c4.red}${c4.dim}${l}${c4.reset}`
|
|
8019
8468
|
).join("\n");
|
|
8020
8469
|
if (hidden > 0) {
|
|
8021
8470
|
return `${body}
|
|
8022
|
-
${indent}${
|
|
8471
|
+
${indent}${c4.dim}\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}${c4.reset}`;
|
|
8023
8472
|
}
|
|
8024
8473
|
return body;
|
|
8025
8474
|
}
|
|
8026
|
-
return `${indent}${
|
|
8475
|
+
return `${indent}${c4.yellow}\u26D4 ${rest}${c4.reset}`;
|
|
8027
8476
|
}
|
|
8028
8477
|
async function main() {
|
|
8029
8478
|
if (process.argv[2] === "daemon") {
|
|
@@ -8034,7 +8483,7 @@ async function main() {
|
|
|
8034
8483
|
try {
|
|
8035
8484
|
cliArgs = parseArgs2(process.argv.slice(2));
|
|
8036
8485
|
} catch (error) {
|
|
8037
|
-
stdout.write(`${
|
|
8486
|
+
stdout.write(`${c4.red}error:${c4.reset} ${error instanceof Error ? error.message : String(error)}
|
|
8038
8487
|
`);
|
|
8039
8488
|
printUsage();
|
|
8040
8489
|
process.exit(1);
|
|
@@ -8046,7 +8495,7 @@ async function main() {
|
|
|
8046
8495
|
const endpointArg = cliArgs.endpoint;
|
|
8047
8496
|
const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
|
|
8048
8497
|
const dirArg = cliArgs.dir;
|
|
8049
|
-
|
|
8498
|
+
let projectDir = path4.resolve(dirArg || process.cwd());
|
|
8050
8499
|
const machine = os8.hostname();
|
|
8051
8500
|
const reader = { rl: null };
|
|
8052
8501
|
let handoffClosing = false;
|
|
@@ -8061,7 +8510,7 @@ async function main() {
|
|
|
8061
8510
|
}
|
|
8062
8511
|
preflightArmed = true;
|
|
8063
8512
|
stdout.write(`
|
|
8064
|
-
${
|
|
8513
|
+
${c4.dim}Press Control-C again to exit${c4.reset}
|
|
8065
8514
|
`);
|
|
8066
8515
|
preflightTimer = setTimeout(() => {
|
|
8067
8516
|
preflightArmed = false;
|
|
@@ -8083,10 +8532,10 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8083
8532
|
const askEndpoint = async () => {
|
|
8084
8533
|
for (; ; ) {
|
|
8085
8534
|
const answer = (await ask(
|
|
8086
|
-
`${
|
|
8535
|
+
`${c4.cyan}Standard Agents instance URL${c4.reset} (e.g. http://localhost:5178): `
|
|
8087
8536
|
)).trim();
|
|
8088
8537
|
if (answer) return answer;
|
|
8089
|
-
stdout.write(`${
|
|
8538
|
+
stdout.write(`${c4.dim}An endpoint URL is required.${c4.reset}
|
|
8090
8539
|
`);
|
|
8091
8540
|
}
|
|
8092
8541
|
};
|
|
@@ -8101,7 +8550,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8101
8550
|
const tlsRelaxed = relaxTlsForLocalEndpoint(endpoint);
|
|
8102
8551
|
printWelcome(endpoint, projectDir);
|
|
8103
8552
|
if (tlsRelaxed) {
|
|
8104
|
-
stdout.write(`${
|
|
8553
|
+
stdout.write(`${c4.dim} TLS verification relaxed for local endpoint.${c4.reset}
|
|
8105
8554
|
|
|
8106
8555
|
`);
|
|
8107
8556
|
}
|
|
@@ -8113,7 +8562,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8113
8562
|
loading.stop();
|
|
8114
8563
|
const applied = consumeAppliedUpdate(version);
|
|
8115
8564
|
if (applied) {
|
|
8116
|
-
stdout.write(` ${
|
|
8565
|
+
stdout.write(` ${c4.green}\u2713${c4.reset} ${c4.dim}Standard Code updated to v${version}.${c4.reset}
|
|
8117
8566
|
|
|
8118
8567
|
`);
|
|
8119
8568
|
}
|
|
@@ -8122,21 +8571,21 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8122
8571
|
const decision = decideAutoUpdate(updateAvailable, { state: readAutoUpdateState(), pm });
|
|
8123
8572
|
if (decision === "start" && pm && startBackgroundUpdate(updateAvailable.latest, pm)) {
|
|
8124
8573
|
stdout.write(
|
|
8125
|
-
` ${
|
|
8574
|
+
` ${c4.teal}\u27F3${c4.reset} ${c4.dim}Standard Code ${c4.reset}${c4.bold}v${updateAvailable.latest}${c4.reset}${c4.dim} is installing in the background \u2014 it applies on your next launch.${c4.reset}
|
|
8126
8575
|
|
|
8127
8576
|
`
|
|
8128
8577
|
);
|
|
8129
8578
|
} else if (decision === "in_flight") {
|
|
8130
8579
|
stdout.write(
|
|
8131
|
-
` ${
|
|
8580
|
+
` ${c4.teal}\u27F3${c4.reset} ${c4.dim}Standard Code v${updateAvailable.latest} is still installing in the background.${c4.reset}
|
|
8132
8581
|
|
|
8133
8582
|
`
|
|
8134
8583
|
);
|
|
8135
8584
|
} else {
|
|
8136
8585
|
const display = updateCommand(pm ?? "npm").display;
|
|
8137
8586
|
stdout.write(
|
|
8138
|
-
` ${
|
|
8139
|
-
${
|
|
8587
|
+
` ${c4.teal}\u25C7${c4.reset} ${c4.dim}Update available:${c4.reset} ${c4.dim}v${updateAvailable.current}${c4.reset} \u2192 ${c4.bold}v${updateAvailable.latest}${c4.reset}
|
|
8588
|
+
${c4.dim}Run ${c4.reset}${c4.bold}${display}${c4.reset}${c4.dim} to update${c4.reset}
|
|
8140
8589
|
|
|
8141
8590
|
`
|
|
8142
8591
|
);
|
|
@@ -8154,39 +8603,39 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8154
8603
|
if (!api || !storedCheck?.ok) {
|
|
8155
8604
|
const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
8156
8605
|
if (storedCheck && !storedCheck.ok) {
|
|
8157
|
-
stdout.write(`${
|
|
8606
|
+
stdout.write(`${c4.red}\u2717${c4.reset} ${c4.dim}Saved sign-in for this endpoint failed:${c4.reset} ${storedCheck.reason}
|
|
8158
8607
|
`);
|
|
8159
|
-
if (storedCheck.hint) stdout.write(` ${
|
|
8608
|
+
if (storedCheck.hint) stdout.write(` ${c4.dim}${storedCheck.hint}${c4.reset}
|
|
8160
8609
|
`);
|
|
8161
8610
|
stdout.write("\n");
|
|
8162
8611
|
}
|
|
8163
8612
|
const explainFailure = (result, prefix) => {
|
|
8164
|
-
stdout.write(`${
|
|
8613
|
+
stdout.write(`${c4.red}\u2717${c4.reset} ${prefix}${result.reason}
|
|
8165
8614
|
`);
|
|
8166
|
-
if (result.hint) stdout.write(` ${
|
|
8615
|
+
if (result.hint) stdout.write(` ${c4.dim}${result.hint}${c4.reset}
|
|
8167
8616
|
`);
|
|
8168
8617
|
};
|
|
8169
|
-
stdout.write(`${
|
|
8618
|
+
stdout.write(`${c4.bold}${gradientText("Sign in to Standard Code")}${c4.reset}
|
|
8170
8619
|
`);
|
|
8171
8620
|
if (`https://${host}` !== PRODUCTION_ENDPOINT) {
|
|
8172
|
-
stdout.write(`${
|
|
8621
|
+
stdout.write(`${c4.dim}Connecting to${c4.reset} ${c4.teal}${host}${c4.reset}
|
|
8173
8622
|
`);
|
|
8174
8623
|
}
|
|
8175
8624
|
stdout.write(
|
|
8176
|
-
`${
|
|
8625
|
+
`${c4.dim}You'll only need to do this once on this machine.${c4.reset}
|
|
8177
8626
|
|
|
8178
8627
|
`
|
|
8179
8628
|
);
|
|
8180
8629
|
stdout.write(
|
|
8181
|
-
`${
|
|
8630
|
+
`${c4.white}Press ${c4.bold}Enter${c4.reset}${c4.white} to open your browser and sign in.${c4.reset} ${c4.dim}(or paste an API token)${c4.reset}
|
|
8182
8631
|
|
|
8183
8632
|
`
|
|
8184
8633
|
);
|
|
8185
8634
|
for (; ; ) {
|
|
8186
|
-
const token = (await ask(`${
|
|
8635
|
+
const token = (await ask(`${c4.teal}\u276F${c4.reset} `)).trim();
|
|
8187
8636
|
if (!token) {
|
|
8188
8637
|
const got = await deviceLogin(endpoint).catch((e) => {
|
|
8189
|
-
stdout.write(`${
|
|
8638
|
+
stdout.write(`${c4.red}\u2717${c4.reset} ${c4.dim}${e instanceof Error ? e.message : String(e)}${c4.reset}
|
|
8190
8639
|
`);
|
|
8191
8640
|
return null;
|
|
8192
8641
|
});
|
|
@@ -8200,7 +8649,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8200
8649
|
{ endpoint, access_token: got, token_type: "Bearer", saved_at: Date.now() },
|
|
8201
8650
|
{ updateDefault: !endpointOverride }
|
|
8202
8651
|
);
|
|
8203
|
-
stdout.write(`${
|
|
8652
|
+
stdout.write(`${c4.green}\u2713${c4.reset} Connected to ${c4.teal}${host}${c4.reset}
|
|
8204
8653
|
`);
|
|
8205
8654
|
break;
|
|
8206
8655
|
}
|
|
@@ -8216,7 +8665,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8216
8665
|
{ endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
|
|
8217
8666
|
{ updateDefault: !endpointOverride }
|
|
8218
8667
|
);
|
|
8219
|
-
stdout.write(`${
|
|
8668
|
+
stdout.write(`${c4.green}\u2713${c4.reset} Connected to ${c4.teal}${host}${c4.reset}
|
|
8220
8669
|
`);
|
|
8221
8670
|
break;
|
|
8222
8671
|
}
|
|
@@ -8232,92 +8681,30 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8232
8681
|
void registerProject(api, identity, projectDir).catch(() => {
|
|
8233
8682
|
});
|
|
8234
8683
|
const tui = new Tui(1);
|
|
8235
|
-
let
|
|
8684
|
+
let agentOverride;
|
|
8236
8685
|
if (cliArgs.agent) {
|
|
8237
8686
|
const wanted = cliArgs.agent.toLowerCase();
|
|
8238
8687
|
if (["sama", "opensama", "sama_one", OPENSAMA_AGENT_ID].includes(wanted)) {
|
|
8239
|
-
|
|
8688
|
+
agentOverride = OPENSAMA_AGENT_ID;
|
|
8240
8689
|
} else if (["unlimited", "standard", "unlimited_one", AGENT_ID].includes(wanted)) {
|
|
8241
|
-
|
|
8690
|
+
agentOverride = AGENT_ID;
|
|
8242
8691
|
} else {
|
|
8243
|
-
stdout.write(`${
|
|
8692
|
+
stdout.write(`${c4.red}error:${c4.reset} Unknown agent "${cliArgs.agent}". Use unlimited or sama.
|
|
8244
8693
|
`);
|
|
8245
8694
|
process.exit(1);
|
|
8246
8695
|
}
|
|
8247
|
-
} else {
|
|
8248
|
-
const picked = await tui.select(
|
|
8249
|
-
`${c3.bold}${gradientText("Which agent?")}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
|
|
8250
|
-
AGENT_CHOICES.map((choice) => ({
|
|
8251
|
-
label: choice.title,
|
|
8252
|
-
hint: choice.description,
|
|
8253
|
-
value: choice.id,
|
|
8254
|
-
disabled: choice.disabled
|
|
8255
|
-
}))
|
|
8256
|
-
);
|
|
8257
|
-
if (!picked) process.exit(0);
|
|
8258
|
-
selectedAgent = picked;
|
|
8259
8696
|
}
|
|
8260
|
-
|
|
8261
|
-
const ok = await ensureSamaAuth(tui, api);
|
|
8262
|
-
if (!ok) process.exit(0);
|
|
8263
|
-
}
|
|
8264
|
-
const home = os8.homedir();
|
|
8265
|
-
const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
8266
|
-
const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
|
|
8697
|
+
let selectedAgent = agentOverride ?? AGENT_ID;
|
|
8267
8698
|
const session = { mode: "local", identity };
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
[
|
|
8278
|
-
{ label: `\u{1F5A5}\uFE0F This machine \u2014 ${shortDir}`, hint: "tools run locally", value: null },
|
|
8279
|
-
...remoteTargets.map((m) => ({
|
|
8280
|
-
label: `${machineIcon(m)} ${m.name}`,
|
|
8281
|
-
hint: `${m.hostname} \xB7 daemon online${m.daemon ? ` \xB7 v${m.daemon.version}` : ""}`,
|
|
8282
|
-
value: m
|
|
8283
|
-
}))
|
|
8284
|
-
]
|
|
8285
|
-
);
|
|
8286
|
-
if (where) {
|
|
8287
|
-
const remotePath = await pickRemoteProject(tui, api, where);
|
|
8288
|
-
if (remotePath) {
|
|
8289
|
-
session.mode = "remote";
|
|
8290
|
-
session.runner = where;
|
|
8291
|
-
session.remotePath = remotePath;
|
|
8292
|
-
}
|
|
8293
|
-
}
|
|
8294
|
-
}
|
|
8295
|
-
}
|
|
8296
|
-
let tags;
|
|
8297
|
-
let resumeTags;
|
|
8298
|
-
if (session.mode === "remote" && session.runner && session.remotePath) {
|
|
8299
|
-
tags = [
|
|
8300
|
-
`path:${session.remotePath}`,
|
|
8301
|
-
`machine:${session.runner.hostname || session.runner.name}`,
|
|
8302
|
-
`runner:${session.runner.id}`
|
|
8303
|
-
];
|
|
8304
|
-
resumeTags = [`path:${session.remotePath}`, `runner:${session.runner.id}`];
|
|
8305
|
-
} else {
|
|
8306
|
-
tags = [`path:${projectDir}`, `machine:${machine}`, `runner:${identity.machine_id}`];
|
|
8307
|
-
resumeTags = [`path:${projectDir}`, `machine:${machine}`];
|
|
8308
|
-
}
|
|
8309
|
-
const loadingSessions = startLoader("Loading sessions");
|
|
8310
|
-
let existing = [];
|
|
8311
|
-
try {
|
|
8312
|
-
existing = await api.listThreads(
|
|
8313
|
-
selectedAgent === OPENSAMA_AGENT_ID ? [OPENSAMA_AGENT_ID] : AGENT_ID_VARIANTS,
|
|
8314
|
-
resumeTags
|
|
8315
|
-
);
|
|
8316
|
-
} catch {
|
|
8317
|
-
existing = [];
|
|
8318
|
-
}
|
|
8319
|
-
const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
|
|
8320
|
-
loadingSessions.stop();
|
|
8699
|
+
const loadingMachines = startLoader("Checking your machines");
|
|
8700
|
+
const machines = await loadMachines(api).catch(() => []);
|
|
8701
|
+
loadingMachines.stop();
|
|
8702
|
+
session.suggestDaemonInstall = machines.every((m) => !m.daemon);
|
|
8703
|
+
const remoteTargets = machines.filter((m) => m.id !== identity.machine_id && daemonOnline(m));
|
|
8704
|
+
const self = machines.find((m) => m.id === identity.machine_id);
|
|
8705
|
+
const launchDir = projectDir;
|
|
8706
|
+
let tags = [];
|
|
8707
|
+
let resumeTags = [];
|
|
8321
8708
|
const createSessionThread = async () => {
|
|
8322
8709
|
const id = await api.createThread(selectedAgent, tags);
|
|
8323
8710
|
if (session.mode === "remote" && session.runner && session.remotePath) {
|
|
@@ -8330,33 +8717,161 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8330
8717
|
}
|
|
8331
8718
|
return id;
|
|
8332
8719
|
};
|
|
8333
|
-
let threadId;
|
|
8720
|
+
let threadId = "";
|
|
8334
8721
|
let resumed = false;
|
|
8335
8722
|
let historySeed;
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8723
|
+
let existing = [];
|
|
8724
|
+
let hadResumeMenu = false;
|
|
8725
|
+
let where = null;
|
|
8726
|
+
let step = remoteTargets.length > 0 ? "machine" : "thread";
|
|
8727
|
+
flow: for (; ; ) {
|
|
8728
|
+
if (step === "machine") {
|
|
8729
|
+
const picked = await tui.select(
|
|
8730
|
+
`${c4.bold}${gradientText("Where should this session run?")}${c4.reset} ${c4.dim}\u2191\u2193 \xB7 enter \xB7 esc${c4.reset}`,
|
|
8731
|
+
[
|
|
8732
|
+
{
|
|
8733
|
+
label: `This machine \u2014 ${self ? machineDisplayName(self) : machine}`,
|
|
8734
|
+
detail: "tools run locally",
|
|
8735
|
+
value: null
|
|
8736
|
+
},
|
|
8737
|
+
...remoteTargets.map((m) => ({
|
|
8738
|
+
label: machineDisplayName(m),
|
|
8739
|
+
detail: `${m.hostname} \xB7 daemon online${m.daemon ? ` \xB7 v${m.daemon.version}` : ""}`,
|
|
8740
|
+
value: m
|
|
8741
|
+
}))
|
|
8742
|
+
],
|
|
8743
|
+
{ spaced: true }
|
|
8744
|
+
);
|
|
8745
|
+
if (picked === void 0) process.exit(0);
|
|
8746
|
+
where = picked;
|
|
8747
|
+
step = "project";
|
|
8748
|
+
} else if (step === "project") {
|
|
8749
|
+
session.mode = "local";
|
|
8750
|
+
session.runner = void 0;
|
|
8751
|
+
session.remotePath = void 0;
|
|
8752
|
+
projectDir = launchDir;
|
|
8753
|
+
if (where) {
|
|
8754
|
+
const remotePath = await pickRemoteProject(tui, api, where);
|
|
8755
|
+
if (!remotePath) {
|
|
8756
|
+
step = "machine";
|
|
8757
|
+
continue;
|
|
8758
|
+
}
|
|
8759
|
+
session.mode = "remote";
|
|
8760
|
+
session.runner = where;
|
|
8761
|
+
session.remotePath = remotePath;
|
|
8762
|
+
} else {
|
|
8763
|
+
const localPath = await pickLocalProject(tui, api, self, launchDir, machine);
|
|
8764
|
+
if (localPath === null) {
|
|
8765
|
+
step = "machine";
|
|
8766
|
+
continue;
|
|
8767
|
+
}
|
|
8768
|
+
if (localPath !== launchDir) {
|
|
8769
|
+
projectDir = localPath;
|
|
8770
|
+
void registerProject(api, identity, projectDir).catch(() => {
|
|
8771
|
+
});
|
|
8772
|
+
}
|
|
8773
|
+
}
|
|
8774
|
+
step = "thread";
|
|
8775
|
+
} else if (step === "thread") {
|
|
8776
|
+
if (session.mode === "remote" && session.runner && session.remotePath) {
|
|
8777
|
+
tags = [
|
|
8778
|
+
`path:${session.remotePath}`,
|
|
8779
|
+
`machine:${session.runner.hostname || session.runner.name}`,
|
|
8780
|
+
`runner:${session.runner.id}`
|
|
8781
|
+
];
|
|
8782
|
+
resumeTags = [`path:${session.remotePath}`, `runner:${session.runner.id}`];
|
|
8783
|
+
} else {
|
|
8784
|
+
tags = [`path:${projectDir}`, `machine:${machine}`, `runner:${identity.machine_id}`];
|
|
8785
|
+
resumeTags = [`path:${projectDir}`, `machine:${machine}`];
|
|
8786
|
+
}
|
|
8787
|
+
const loadingSessions = startLoader("Loading sessions");
|
|
8788
|
+
try {
|
|
8789
|
+
const listIds = agentOverride ? agentOverride === OPENSAMA_AGENT_ID ? [OPENSAMA_AGENT_ID] : AGENT_ID_VARIANTS : [...AGENT_ID_VARIANTS, OPENSAMA_AGENT_ID];
|
|
8790
|
+
existing = await api.listThreads(listIds, resumeTags);
|
|
8791
|
+
existing.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
|
|
8792
|
+
} catch {
|
|
8793
|
+
existing = [];
|
|
8794
|
+
}
|
|
8795
|
+
const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
|
|
8796
|
+
loadingSessions.stop();
|
|
8797
|
+
if (existing.length === 0) {
|
|
8798
|
+
hadResumeMenu = false;
|
|
8799
|
+
step = "agent";
|
|
8800
|
+
continue;
|
|
8801
|
+
}
|
|
8802
|
+
hadResumeMenu = true;
|
|
8803
|
+
const home = os8.homedir();
|
|
8804
|
+
const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
8805
|
+
const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
|
|
8806
|
+
const items = summaries.map((s) => ({
|
|
8807
|
+
label: s.label,
|
|
8808
|
+
hint: s.hint,
|
|
8809
|
+
value: s.id
|
|
8810
|
+
}));
|
|
8811
|
+
items.push({ label: "\uFF0B Start a new session", value: null });
|
|
8812
|
+
const whereLabel = session.mode === "remote" && session.runner ? `${session.runner.name}:${shortenPath(session.remotePath ?? "")}` : shortDir;
|
|
8813
|
+
const picked = await tui.select(
|
|
8814
|
+
`${c4.bold}${gradientText("Resume a session")}${c4.reset} ${c4.gray}${whereLabel}${c4.reset} ${c4.dim}\u2191\u2193 \xB7 enter \xB7 esc${c4.reset}`,
|
|
8815
|
+
items
|
|
8816
|
+
);
|
|
8817
|
+
if (picked === void 0) {
|
|
8818
|
+
if (remoteTargets.length > 0) {
|
|
8819
|
+
step = "project";
|
|
8820
|
+
continue;
|
|
8821
|
+
}
|
|
8822
|
+
process.exit(0);
|
|
8823
|
+
}
|
|
8824
|
+
if (typeof picked === "string") {
|
|
8825
|
+
threadId = picked;
|
|
8826
|
+
resumed = true;
|
|
8827
|
+
const threadAgent = existing.find((t) => t.id === picked)?.agent_id;
|
|
8828
|
+
selectedAgent = threadAgent === OPENSAMA_AGENT_ID ? OPENSAMA_AGENT_ID : AGENT_ID;
|
|
8829
|
+
if (selectedAgent === OPENSAMA_AGENT_ID) {
|
|
8830
|
+
const ok = await ensureSamaAuth(tui, api);
|
|
8831
|
+
if (!ok) continue;
|
|
8832
|
+
}
|
|
8833
|
+
break flow;
|
|
8834
|
+
}
|
|
8835
|
+
historySeed = existing[0]?.id;
|
|
8836
|
+
step = "agent";
|
|
8351
8837
|
} else {
|
|
8838
|
+
if (!agentOverride) {
|
|
8839
|
+
const picked = await tui.select(
|
|
8840
|
+
`${c4.bold}${gradientText("Which agent?")}${c4.reset} ${c4.dim}\u2191\u2193 \xB7 enter \xB7 esc${c4.reset}`,
|
|
8841
|
+
AGENT_CHOICES.map((choice) => ({
|
|
8842
|
+
label: choice.title,
|
|
8843
|
+
hint: choice.description,
|
|
8844
|
+
value: choice.id,
|
|
8845
|
+
disabled: choice.disabled
|
|
8846
|
+
}))
|
|
8847
|
+
);
|
|
8848
|
+
if (!picked) {
|
|
8849
|
+
if (hadResumeMenu) {
|
|
8850
|
+
step = "thread";
|
|
8851
|
+
continue;
|
|
8852
|
+
}
|
|
8853
|
+
if (remoteTargets.length > 0) {
|
|
8854
|
+
step = "project";
|
|
8855
|
+
continue;
|
|
8856
|
+
}
|
|
8857
|
+
process.exit(0);
|
|
8858
|
+
}
|
|
8859
|
+
selectedAgent = picked;
|
|
8860
|
+
}
|
|
8861
|
+
if (selectedAgent === OPENSAMA_AGENT_ID) {
|
|
8862
|
+
const ok = await ensureSamaAuth(tui, api);
|
|
8863
|
+
if (!ok) {
|
|
8864
|
+
if (agentOverride) process.exit(0);
|
|
8865
|
+
continue;
|
|
8866
|
+
}
|
|
8867
|
+
}
|
|
8352
8868
|
threadId = await createSessionThread();
|
|
8353
|
-
|
|
8869
|
+
break flow;
|
|
8354
8870
|
}
|
|
8355
|
-
} else {
|
|
8356
|
-
threadId = await createSessionThread();
|
|
8357
8871
|
}
|
|
8358
8872
|
for (; ; ) {
|
|
8359
|
-
|
|
8873
|
+
const agentTitle = AGENT_CHOICES.find((a) => a.id === selectedAgent)?.title ?? selectedAgent;
|
|
8874
|
+
await runInteractive(tui, api, threadId, projectDir, machine, resumed, session, agentTitle, historySeed);
|
|
8360
8875
|
historySeed = threadId;
|
|
8361
8876
|
threadId = await createSessionThread();
|
|
8362
8877
|
await api.kvSet(threadId, "lease_supersedes", historySeed);
|
|
@@ -8372,7 +8887,7 @@ async function ensureSamaAuth(tui, api) {
|
|
|
8372
8887
|
checking.stop();
|
|
8373
8888
|
if (already) return true;
|
|
8374
8889
|
const picked = await tui.select(
|
|
8375
|
-
`${
|
|
8890
|
+
`${c4.bold}${gradientText("Authenticate with ChatGPT")}${c4.reset} ${c4.dim}Sama One runs on OpenAI using your own ChatGPT Pro account${c4.reset}`,
|
|
8376
8891
|
[
|
|
8377
8892
|
{
|
|
8378
8893
|
label: "Continue with ChatGPT",
|
|
@@ -8385,7 +8900,7 @@ async function ensureSamaAuth(tui, api) {
|
|
|
8385
8900
|
if (picked !== "continue") return false;
|
|
8386
8901
|
openUrl("https://standardcode.ai/app?connect=sama");
|
|
8387
8902
|
tui.print(
|
|
8388
|
-
`${
|
|
8903
|
+
`${c4.dim}Finish connecting ChatGPT in the browser \u2014 waiting here for the authorization to land on your account\u2026${c4.reset}`
|
|
8389
8904
|
);
|
|
8390
8905
|
const waiting = startLoader("Waiting for your ChatGPT authorization");
|
|
8391
8906
|
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
@@ -8394,20 +8909,20 @@ async function ensureSamaAuth(tui, api) {
|
|
|
8394
8909
|
if (await api.openSamaStatus().catch(() => false)) {
|
|
8395
8910
|
waiting.stop();
|
|
8396
8911
|
tui.print(
|
|
8397
|
-
`${
|
|
8912
|
+
`${c4.green}\u2713${c4.reset} ChatGPT connected \u2014 Sama One is now unlocked on your account (terminal, web, and macOS app).`
|
|
8398
8913
|
);
|
|
8399
8914
|
return true;
|
|
8400
8915
|
}
|
|
8401
8916
|
}
|
|
8402
8917
|
waiting.stop();
|
|
8403
8918
|
tui.print(
|
|
8404
|
-
`${
|
|
8919
|
+
`${c4.yellow}Still not connected.${c4.reset} Finish the flow at ${c4.teal}standardcode.ai/app${c4.reset} and pick Sama One again.`
|
|
8405
8920
|
);
|
|
8406
8921
|
return false;
|
|
8407
8922
|
}
|
|
8408
8923
|
async function runAgentSwitchMenu(tui, api, threadId) {
|
|
8409
8924
|
const picked = await tui.select(
|
|
8410
|
-
`${
|
|
8925
|
+
`${c4.bold}${gradientText("Switch agent")}${c4.reset} ${c4.dim}takes effect on the next message${c4.reset}`,
|
|
8411
8926
|
AGENT_CHOICES.map((choice) => ({
|
|
8412
8927
|
label: choice.title,
|
|
8413
8928
|
hint: choice.description,
|
|
@@ -8423,41 +8938,68 @@ async function runAgentSwitchMenu(tui, api, threadId) {
|
|
|
8423
8938
|
const title = AGENT_CHOICES.find((choice) => choice.id === picked)?.title ?? picked;
|
|
8424
8939
|
try {
|
|
8425
8940
|
await api.setThreadAgent(threadId, picked);
|
|
8426
|
-
tui.
|
|
8941
|
+
tui.setAgentLabel(title);
|
|
8942
|
+
tui.print(`${c4.green}\u2713${c4.reset} Session handed to ${c4.bold}${title}${c4.reset} \u2014 applies from your next message.`);
|
|
8427
8943
|
} catch (e) {
|
|
8428
8944
|
tui.print(
|
|
8429
|
-
`${
|
|
8945
|
+
`${c4.red}\u2717 couldn't switch agent:${c4.reset} ${c4.gray}${e instanceof Error ? e.message : String(e)}${c4.reset}`
|
|
8430
8946
|
);
|
|
8431
8947
|
}
|
|
8432
8948
|
}
|
|
8949
|
+
async function pickLocalProject(tui, api, self, cwd, machineName) {
|
|
8950
|
+
const NEW = "__new__";
|
|
8951
|
+
const label = self ? machineDisplayName(self) : machineName;
|
|
8952
|
+
const others = Object.entries(self?.projects ?? {}).filter(([dir2]) => dir2 !== cwd).sort((a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0));
|
|
8953
|
+
const labels = projectDisplayLabels({ ...self?.projects ?? {}, [cwd]: self?.projects?.[cwd] });
|
|
8954
|
+
const items = [
|
|
8955
|
+
{
|
|
8956
|
+
label: labels.get(cwd) ?? projectDisplayName(cwd, self?.projects?.[cwd]),
|
|
8957
|
+
hint: "current directory",
|
|
8958
|
+
detail: shortenPath(cwd, 60),
|
|
8959
|
+
value: cwd
|
|
8960
|
+
},
|
|
8961
|
+
...others.map(([dir2, p]) => ({
|
|
8962
|
+
label: labels.get(dir2) ?? projectDisplayName(dir2, p),
|
|
8963
|
+
hint: p?.last_used_at ? relativeTime(p.last_used_at / 1e3) : "",
|
|
8964
|
+
detail: shortenPath(dir2, 60),
|
|
8965
|
+
value: dir2
|
|
8966
|
+
})),
|
|
8967
|
+
{ label: "\uFF0B New project", hint: "browse or create a directory", value: NEW }
|
|
8968
|
+
];
|
|
8969
|
+
const picked = await tui.select(
|
|
8970
|
+
`${c4.bold}${gradientText(`Project on ${label}`)}${c4.reset} ${c4.dim}\u2191\u2193 \xB7 enter \xB7 esc${c4.reset}`,
|
|
8971
|
+
items,
|
|
8972
|
+
{ spaced: true }
|
|
8973
|
+
);
|
|
8974
|
+
if (!picked) return null;
|
|
8975
|
+
if (picked !== NEW) return picked;
|
|
8976
|
+
return await pickDirectory(tui, localBrowseBackend(label), { startPath: cwd, loader: startLoader });
|
|
8977
|
+
}
|
|
8433
8978
|
async function pickRemoteProject(tui, api, runner) {
|
|
8434
8979
|
const ENTER_PATH = "__enter_path__";
|
|
8435
8980
|
const projects = Object.entries(runner.projects).sort(
|
|
8436
8981
|
(a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
|
|
8437
8982
|
);
|
|
8438
|
-
const
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8983
|
+
const labels = projectDisplayLabels(runner.projects);
|
|
8984
|
+
const items = projects.map(
|
|
8985
|
+
([dir2, p]) => ({
|
|
8986
|
+
label: labels.get(dir2) ?? projectDisplayName(dir2, p),
|
|
8987
|
+
hint: p?.last_used_at ? relativeTime(p.last_used_at / 1e3) : "",
|
|
8988
|
+
detail: shortenPath(dir2, 60),
|
|
8989
|
+
value: dir2
|
|
8990
|
+
})
|
|
8991
|
+
);
|
|
8992
|
+
items.push({ label: "\uFF0B New project", hint: "browse or create a directory", value: ENTER_PATH });
|
|
8444
8993
|
const picked = await tui.select(
|
|
8445
|
-
`${
|
|
8446
|
-
items
|
|
8994
|
+
`${c4.bold}${gradientText(`Project on ${runner.name}`)}${c4.reset} ${c4.dim}\u2191\u2193 \xB7 enter \xB7 esc${c4.reset}`,
|
|
8995
|
+
items,
|
|
8996
|
+
{ spaced: true }
|
|
8447
8997
|
);
|
|
8448
8998
|
if (!picked) return null;
|
|
8449
8999
|
if (picked !== ENTER_PATH) return picked;
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
);
|
|
8454
|
-
if (!typed) return null;
|
|
8455
|
-
const trimmed = typed.trim();
|
|
8456
|
-
if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
|
|
8457
|
-
tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
|
|
8458
|
-
return null;
|
|
8459
|
-
}
|
|
8460
|
-
return await resolveRemoteProjectPath(api, runner.id, trimmed);
|
|
9000
|
+
return await pickDirectory(tui, remoteBrowseBackend(api, runner.id, runner.name), {
|
|
9001
|
+
loader: startLoader
|
|
9002
|
+
});
|
|
8461
9003
|
}
|
|
8462
9004
|
function isSilentMessage(m) {
|
|
8463
9005
|
return m?.silent === true || m?.metadata?.silent === true;
|
|
@@ -8474,14 +9016,15 @@ async function summarizeThreads(api, threads) {
|
|
|
8474
9016
|
}
|
|
8475
9017
|
const label = preview ? preview.length > 64 ? preview.slice(0, 63) + "\u2026" : preview : "(empty session)";
|
|
8476
9018
|
const when = t.created_at ? relativeTime(t.created_at) : "";
|
|
8477
|
-
const
|
|
9019
|
+
const agentTag = t.agent_id === OPENSAMA_AGENT_ID ? AGENT_CHOICES.find((a) => a.id === OPENSAMA_AGENT_ID)?.title ?? "Sama One" : "";
|
|
9020
|
+
const hint = [t.id.slice(0, 8), when, agentTag].filter(Boolean).join(" \xB7 ");
|
|
8478
9021
|
return { id: t.id, label, hint };
|
|
8479
9022
|
})
|
|
8480
9023
|
);
|
|
8481
9024
|
}
|
|
8482
9025
|
function subagentLabel(s, titles) {
|
|
8483
9026
|
const agentName = (s.agent_name || "").trim();
|
|
8484
|
-
const title = (s.title || "").trim() || titles.get(agentName) || (agentName ? agentName.replace(/[_-]+/g, " ").replace(/\b\w/g, (
|
|
9027
|
+
const title = (s.title || "").trim() || titles.get(agentName) || (agentName ? agentName.replace(/[_-]+/g, " ").replace(/\b\w/g, (c5) => c5.toUpperCase()) : "Subagent");
|
|
8485
9028
|
const tagged = (s.threadName || "").trim();
|
|
8486
9029
|
return tagged ? `${title} \xB7 ${tagged}` : title;
|
|
8487
9030
|
}
|
|
@@ -8504,8 +9047,8 @@ async function printHistory(api, threadId, tui) {
|
|
|
8504
9047
|
).sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
|
|
8505
9048
|
if (!convo.length) return;
|
|
8506
9049
|
const shown = convo.slice(-24);
|
|
8507
|
-
tui.print(`${
|
|
8508
|
-
if (shown.length < convo.length) tui.print(`${
|
|
9050
|
+
tui.print(`${c4.dim}\u2500\u2500 resuming session \xB7 ${convo.length} message${convo.length === 1 ? "" : "s"} \u2500\u2500${c4.reset}`);
|
|
9051
|
+
if (shown.length < convo.length) tui.print(`${c4.dim} \u2026 earlier messages omitted${c4.reset}`);
|
|
8509
9052
|
for (const m of shown) {
|
|
8510
9053
|
if (m.metadata?.user_command) {
|
|
8511
9054
|
printCommandBlock(
|
|
@@ -8522,9 +9065,12 @@ async function printHistory(api, threadId, tui) {
|
|
|
8522
9065
|
else printAssistant(tui, text);
|
|
8523
9066
|
}
|
|
8524
9067
|
}
|
|
8525
|
-
async function runInteractive(tui, api, threadId, projectDir, machine, resumed, session, historySeedThreadId) {
|
|
9068
|
+
async function runInteractive(tui, api, threadId, projectDir, machine, resumed, session, agentTitle, historySeedThreadId) {
|
|
8526
9069
|
const remote = session.mode === "remote";
|
|
8527
9070
|
const runnerName = session.runner?.name ?? "the remote machine";
|
|
9071
|
+
let currentOwner = null;
|
|
9072
|
+
let ownershipKnown = false;
|
|
9073
|
+
const describeExecOwner = (o) => o?.client_kind === "daemon" ? "the daemon on this machine" : o?.client_name || "another client";
|
|
8528
9074
|
const registry = new ProcessRegistry(api, threadId, machine);
|
|
8529
9075
|
const refreshBgCount = () => {
|
|
8530
9076
|
void registry.runningCount().then((n) => tui.setBackgroundCount(n)).catch(() => {
|
|
@@ -8550,6 +9096,7 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
|
|
|
8550
9096
|
saveApprovals(api, threadId, perm);
|
|
8551
9097
|
const attaching = startLoader("Attaching to thread");
|
|
8552
9098
|
let busy = false;
|
|
9099
|
+
let optimisticBusyUntil = 0;
|
|
8553
9100
|
let sharedMessaging = emptySharedMessagingSnapshot();
|
|
8554
9101
|
let mirroredDraftRefs = [];
|
|
8555
9102
|
let sharedMessagingReady = false;
|
|
@@ -8609,26 +9156,28 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
|
|
|
8609
9156
|
if (detail) for (const d of detail) tui.print(d);
|
|
8610
9157
|
refreshBgCount();
|
|
8611
9158
|
},
|
|
8612
|
-
onOwnership: (isOwner,
|
|
9159
|
+
onOwnership: (isOwner, owner2) => {
|
|
9160
|
+
const wasKnown = ownershipKnown;
|
|
9161
|
+
ownershipKnown = true;
|
|
9162
|
+
currentOwner = owner2;
|
|
8613
9163
|
if (isOwner) {
|
|
8614
9164
|
bridge?.setClaim("takeover");
|
|
8615
9165
|
exec?.writeSessionInfo();
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
);
|
|
9166
|
+
if (wasKnown) tui.print(`${c4.dim}Tool execution moved to this terminal.${c4.reset}`);
|
|
9167
|
+
} else if (wasKnown) {
|
|
9168
|
+
tui.print(`${c4.dim}Tool execution moved to ${describeExecOwner(owner2)}.${c4.reset}`);
|
|
8620
9169
|
}
|
|
8621
9170
|
},
|
|
8622
9171
|
onClaimRefused: (reason) => {
|
|
8623
9172
|
if (reason === "in_flight") {
|
|
8624
9173
|
tui.print(
|
|
8625
|
-
`${
|
|
9174
|
+
`${c4.dim}The session's current client is mid-operation \u2014 execution stays there until it finishes.${c4.reset}`
|
|
8626
9175
|
);
|
|
8627
9176
|
}
|
|
8628
9177
|
},
|
|
8629
9178
|
onSuperseded: () => {
|
|
8630
9179
|
tui.print(
|
|
8631
|
-
`${
|
|
9180
|
+
`${c4.yellow}\u26A0 Another Standard Code process on this machine took over this session \u2014 this terminal is watching.${c4.reset}`
|
|
8632
9181
|
);
|
|
8633
9182
|
},
|
|
8634
9183
|
onStatus: (id, summary) => {
|
|
@@ -8786,7 +9335,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8786
9335
|
const logout = async () => {
|
|
8787
9336
|
deleteCredential(api.origin);
|
|
8788
9337
|
const instanceHost = api.origin.replace(/^https?:\/\//, "");
|
|
8789
|
-
tui.print(`${
|
|
9338
|
+
tui.print(`${c4.gray}Signed out \u2014 removed the saved token for ${c4.teal}${instanceHost}${c4.reset}${c4.gray}. Run standardcode to sign in again.${c4.reset}`);
|
|
8790
9339
|
await quit();
|
|
8791
9340
|
};
|
|
8792
9341
|
const bgMgr = {
|
|
@@ -8794,7 +9343,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8794
9343
|
stop: async (id) => {
|
|
8795
9344
|
if (!host) {
|
|
8796
9345
|
tui.print(
|
|
8797
|
-
`${
|
|
9346
|
+
`${c4.gray}That process runs on ${runnerName} \u2014 ask the agent to stop it (it manages processes there).${c4.reset}`
|
|
8798
9347
|
);
|
|
8799
9348
|
return;
|
|
8800
9349
|
}
|
|
@@ -8875,7 +9424,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8875
9424
|
} catch (error) {
|
|
8876
9425
|
if (!sharedMessagingReady && !sharedMessagingUnavailableShown) {
|
|
8877
9426
|
sharedMessagingUnavailableShown = true;
|
|
8878
|
-
tui.print(`${
|
|
9427
|
+
tui.print(`${c4.dim}shared messaging unavailable: ${error instanceof Error ? error.message : String(error)}${c4.reset}`);
|
|
8879
9428
|
}
|
|
8880
9429
|
}
|
|
8881
9430
|
};
|
|
@@ -8885,7 +9434,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8885
9434
|
applySharedMessaging(snapshot, false);
|
|
8886
9435
|
return true;
|
|
8887
9436
|
}).catch((error) => {
|
|
8888
|
-
tui.print(`${
|
|
9437
|
+
tui.print(`${c4.dim}shared messaging failed: ${error instanceof Error ? error.message : String(error)}${c4.reset}`);
|
|
8889
9438
|
return false;
|
|
8890
9439
|
});
|
|
8891
9440
|
const appendSharedPending = (text, images, refs = []) => applySharedMutation(api.appendPendingInput(threadId, {
|
|
@@ -8913,28 +9462,32 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8913
9462
|
tui.printUserMessage(text || `\u{1F4CE} ${refs.length + images.length} attachment(s)`);
|
|
8914
9463
|
const key = text.trim();
|
|
8915
9464
|
pendingSent.set(key, (pendingSent.get(key) ?? 0) + 1);
|
|
9465
|
+
busy = true;
|
|
9466
|
+
optimisticBusyUntil = Date.now() + 8e3;
|
|
9467
|
+
tui.setWorking(true);
|
|
8916
9468
|
try {
|
|
8917
9469
|
await api.sendMessage(threadId, text, [...refs, ...toAttachments(images)]);
|
|
8918
9470
|
} catch (e) {
|
|
8919
9471
|
const n = (pendingSent.get(key) ?? 1) - 1;
|
|
8920
9472
|
if (n > 0) pendingSent.set(key, n);
|
|
8921
9473
|
else pendingSent.delete(key);
|
|
8922
|
-
|
|
9474
|
+
busy = false;
|
|
9475
|
+
optimisticBusyUntil = 0;
|
|
9476
|
+
tui.setWorking(false);
|
|
9477
|
+
tui.print(`${c4.dim}failed to send: ${e instanceof Error ? e.message : String(e)}${c4.reset}`);
|
|
8923
9478
|
return false;
|
|
8924
9479
|
}
|
|
8925
|
-
busy = true;
|
|
8926
|
-
tui.setWorking(true);
|
|
8927
9480
|
return true;
|
|
8928
9481
|
};
|
|
8929
9482
|
const whereLabel = remote ? runnerName : "this machine";
|
|
8930
9483
|
let bangRunning = false;
|
|
8931
9484
|
const runBangCommand = async (command) => {
|
|
8932
9485
|
if (bangRunning) {
|
|
8933
|
-
tui.print(`${
|
|
9486
|
+
tui.print(`${c4.dim}a command is already running \u2014 one at a time.${c4.reset}`);
|
|
8934
9487
|
return;
|
|
8935
9488
|
}
|
|
8936
9489
|
bangRunning = true;
|
|
8937
|
-
tui.print(`${
|
|
9490
|
+
tui.print(`${c4.magenta}!${c4.reset} ${c4.dim}running on ${whereLabel}\u2026${c4.reset}`);
|
|
8938
9491
|
try {
|
|
8939
9492
|
const res = await api.runCommand(threadId, command);
|
|
8940
9493
|
if (res.messageId) shownIds.add(res.messageId);
|
|
@@ -8948,7 +9501,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8948
9501
|
const openPendingMenu = async () => {
|
|
8949
9502
|
const items = sharedMessaging.pending.items;
|
|
8950
9503
|
if (!items.length) {
|
|
8951
|
-
tui.print(`${
|
|
9504
|
+
tui.print(`${c4.dim}No pending messages.${c4.reset}`);
|
|
8952
9505
|
return;
|
|
8953
9506
|
}
|
|
8954
9507
|
const picked = await tui.select("Pending messages", items.map((item, index) => ({
|
|
@@ -8978,16 +9531,16 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8978
9531
|
try {
|
|
8979
9532
|
await api.compact(threadId);
|
|
8980
9533
|
} catch (err) {
|
|
8981
|
-
tui.print(`${
|
|
9534
|
+
tui.print(`${c4.red}\u2717${c4.reset} couldn't start compaction: ${err.message}`);
|
|
8982
9535
|
}
|
|
8983
9536
|
};
|
|
8984
9537
|
const runAccountCommand = async () => {
|
|
8985
|
-
tui.print(`${
|
|
9538
|
+
tui.print(`${c4.gray}Opening your account\u2026${c4.reset}`);
|
|
8986
9539
|
const link = await api.accountLink(threadId).catch(() => null);
|
|
8987
9540
|
const target = link?.url ?? "https://standardcode.ai/account";
|
|
8988
9541
|
openUrl(target);
|
|
8989
9542
|
tui.print(
|
|
8990
|
-
link?.preauthed ? `${
|
|
9543
|
+
link?.preauthed ? `${c4.gray}\u2192 account dashboard opened in your browser (signed in)${c4.reset}` : `${c4.gray}\u2192 opened ${target} \u2014 sign in with your account email${c4.reset}`
|
|
8991
9544
|
);
|
|
8992
9545
|
};
|
|
8993
9546
|
const ordinal = (n) => {
|
|
@@ -8998,24 +9551,24 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8998
9551
|
const renderUpgradePanel = (q) => {
|
|
8999
9552
|
const dots = [];
|
|
9000
9553
|
for (let i = 0; i < q.max; i++) {
|
|
9001
|
-
if (i < q.current) dots.push(`${
|
|
9002
|
-
else if (i === q.current) dots.push(`${
|
|
9003
|
-
else dots.push(`${
|
|
9554
|
+
if (i < q.current) dots.push(`${c4.teal}\u25CF${c4.reset}`);
|
|
9555
|
+
else if (i === q.current) dots.push(`${c4.bold}${gradientText("\uFF0B")}${c4.reset}`);
|
|
9556
|
+
else dots.push(`${c4.dim}\xB7${c4.reset}`);
|
|
9004
9557
|
}
|
|
9005
9558
|
const cost = fmtCost(q);
|
|
9006
9559
|
const lines = [
|
|
9007
9560
|
"",
|
|
9008
|
-
`${
|
|
9561
|
+
`${c4.bold}${gradientText("\u2726 Add a parallel session")}${c4.reset}`,
|
|
9009
9562
|
"",
|
|
9010
|
-
`${dots.join(" ")} ${
|
|
9563
|
+
`${dots.join(" ")} ${c4.dim}${q.current} of ${q.current} session${q.current === 1 ? "" : "s"} in use${c4.reset}`
|
|
9011
9564
|
];
|
|
9012
9565
|
if (q.ends_trial) {
|
|
9013
9566
|
lines.push(
|
|
9014
|
-
`${
|
|
9015
|
-
`${
|
|
9567
|
+
`${c4.yellow}Your $5 trial covers 1 session. Adding a ${ordinal(q.sessions)} ends the trial${c4.reset}`,
|
|
9568
|
+
`${c4.yellow}and starts your $49/mo plan now${cost ? ` \u2014 ${c4.bold}${cost} charged today${c4.reset}${c4.yellow}` : ""}.${c4.reset}`
|
|
9016
9569
|
);
|
|
9017
9570
|
} else if (cost) {
|
|
9018
|
-
lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 ${
|
|
9571
|
+
lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 ${c4.bold}${cost} charged now${c4.reset}.`);
|
|
9019
9572
|
} else {
|
|
9020
9573
|
lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 prorated on your next invoice.`);
|
|
9021
9574
|
}
|
|
@@ -9028,7 +9581,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
9028
9581
|
try {
|
|
9029
9582
|
if (opts.auto) {
|
|
9030
9583
|
tui.print(
|
|
9031
|
-
`${
|
|
9584
|
+
`${c4.yellow}You're out of simultaneous sessions \u2014 another Standard Code session is using your slot.${c4.reset}`
|
|
9032
9585
|
);
|
|
9033
9586
|
}
|
|
9034
9587
|
const quote = await api.sessionsQuote(threadId);
|
|
@@ -9036,16 +9589,16 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
9036
9589
|
const link = await api.accountLink(threadId).catch(() => null);
|
|
9037
9590
|
const target = link?.url ?? "https://standardcode.ai/account";
|
|
9038
9591
|
tui.print(
|
|
9039
|
-
`${
|
|
9592
|
+
`${c4.gray}Close the other session (its slot frees within ~90s) \u2014 or add another simultaneous session to your plan, then resend your message.${c4.reset}`
|
|
9040
9593
|
);
|
|
9041
9594
|
openUrl(target);
|
|
9042
|
-
tui.print(`${
|
|
9595
|
+
tui.print(`${c4.gray}\u2192 opened ${target} to manage your plan${c4.reset}`);
|
|
9043
9596
|
return;
|
|
9044
9597
|
}
|
|
9045
9598
|
if (quote.current >= quote.max) {
|
|
9046
9599
|
tui.print(
|
|
9047
|
-
`${
|
|
9048
|
-
${
|
|
9600
|
+
`${c4.yellow}You're at the maximum of ${quote.max} parallel session${quote.max === 1 ? "" : "s"}.${c4.reset}
|
|
9601
|
+
${c4.gray}Close another session (its slot frees within ~90s), then resend your message.${c4.reset}`
|
|
9049
9602
|
);
|
|
9050
9603
|
return;
|
|
9051
9604
|
}
|
|
@@ -9057,25 +9610,25 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9057
9610
|
{ label: "Not now", value: "no" }
|
|
9058
9611
|
]);
|
|
9059
9612
|
if (choice !== "go") {
|
|
9060
|
-
tui.print(`${
|
|
9613
|
+
tui.print(`${c4.gray}No change made \u2014 you can upgrade anytime with /upgrade.${c4.reset}`);
|
|
9061
9614
|
return;
|
|
9062
9615
|
}
|
|
9063
|
-
tui.print(`${
|
|
9616
|
+
tui.print(`${c4.gray}Applying\u2026${c4.reset}`);
|
|
9064
9617
|
let applied;
|
|
9065
9618
|
try {
|
|
9066
9619
|
applied = await api.sessionsUpgrade(threadId, quote.sessions);
|
|
9067
9620
|
} catch (e) {
|
|
9068
|
-
tui.print(`${
|
|
9621
|
+
tui.print(`${c4.red}\u2717${c4.reset} Upgrade failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
9069
9622
|
return;
|
|
9070
9623
|
}
|
|
9071
9624
|
if (!applied?.ok) {
|
|
9072
|
-
tui.print(`${
|
|
9625
|
+
tui.print(`${c4.red}\u2717${c4.reset} Upgrade failed: ${applied?.error ?? "unknown error"}`);
|
|
9073
9626
|
return;
|
|
9074
9627
|
}
|
|
9075
9628
|
const n = applied.sessions ?? quote.sessions;
|
|
9076
|
-
tui.print(`${
|
|
9629
|
+
tui.print(`${c4.green}\u2713${c4.reset} ${c4.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c4.reset}`);
|
|
9077
9630
|
if (opts.auto && lastSent) {
|
|
9078
|
-
tui.print(`${
|
|
9631
|
+
tui.print(`${c4.gray}Continuing\u2026${c4.reset}`);
|
|
9079
9632
|
await sendNow(lastSent.text, lastSent.images, lastSent.refs);
|
|
9080
9633
|
}
|
|
9081
9634
|
} finally {
|
|
@@ -9217,7 +9770,7 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9217
9770
|
const submitComposer = async (text, images, steer) => {
|
|
9218
9771
|
const draftRefs = mirroredDraftRefs;
|
|
9219
9772
|
if (!sharedMessagingReady && (busy || steer || editingPendingId !== null)) {
|
|
9220
|
-
tui.print(`${
|
|
9773
|
+
tui.print(`${c4.dim}Restoring shared message state \u2014 try again in a moment.${c4.reset}`);
|
|
9221
9774
|
tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
|
|
9222
9775
|
tui.setInput(text, images);
|
|
9223
9776
|
return;
|
|
@@ -9240,7 +9793,7 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9240
9793
|
const item = sharedMessaging.pending.items.find((candidate) => candidate.id === pendingId);
|
|
9241
9794
|
editingPendingId = null;
|
|
9242
9795
|
if (!item) {
|
|
9243
|
-
tui.print(`${
|
|
9796
|
+
tui.print(`${c4.dim}That pending message was already dispatched or dismissed.${c4.reset}`);
|
|
9244
9797
|
return;
|
|
9245
9798
|
}
|
|
9246
9799
|
const updated = await editSharedPending(item, text, images, draftRefs);
|
|
@@ -9253,7 +9806,7 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9253
9806
|
return;
|
|
9254
9807
|
}
|
|
9255
9808
|
if (steer) {
|
|
9256
|
-
tui.print(`${
|
|
9809
|
+
tui.print(`${c4.yellow}\u21AA steering at the next safe model boundary:${c4.reset} ${text}`);
|
|
9257
9810
|
if (!await steerSharedInput(text, images, draftRefs)) {
|
|
9258
9811
|
mirroredDraftRefs = draftRefs;
|
|
9259
9812
|
tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
|
|
@@ -9263,7 +9816,7 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9263
9816
|
}
|
|
9264
9817
|
if (busy) {
|
|
9265
9818
|
if (await appendSharedPending(text, images, draftRefs)) {
|
|
9266
|
-
tui.print(`${
|
|
9819
|
+
tui.print(`${c4.gray}\u23F3 pending:${c4.reset} ${text} ${c4.dim}(/queue to edit, steer, or dismiss)${c4.reset}`);
|
|
9267
9820
|
} else {
|
|
9268
9821
|
mirroredDraftRefs = draftRefs;
|
|
9269
9822
|
tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
|
|
@@ -9286,17 +9839,17 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9286
9839
|
tui.onInterrupt = () => {
|
|
9287
9840
|
const firstPending = sharedMessaging.pending.items[0];
|
|
9288
9841
|
if (!busy && firstPending) {
|
|
9289
|
-
tui.print(`${
|
|
9842
|
+
tui.print(`${c4.yellow}\u21AA steering the first pending message\u2026${c4.reset}`);
|
|
9290
9843
|
void promoteSharedPending(firstPending);
|
|
9291
9844
|
return;
|
|
9292
9845
|
}
|
|
9293
9846
|
if (busy) {
|
|
9294
9847
|
if (!sharedMessagingReady) {
|
|
9295
|
-
tui.print(`${
|
|
9848
|
+
tui.print(`${c4.dim}Shared messaging is not connected; the session was not stopped.${c4.reset}`);
|
|
9296
9849
|
return;
|
|
9297
9850
|
}
|
|
9298
9851
|
const advancing = sharedMessaging.pending.items.length > 0;
|
|
9299
|
-
tui.print(`${
|
|
9852
|
+
tui.print(`${c4.yellow}${advancing ? "[stopping; next pending message will run]" : "[stopping at the next safe boundary]"}${c4.reset}`);
|
|
9300
9853
|
void applySharedMutation(api.requestSharedStop(threadId, messagingOrigin));
|
|
9301
9854
|
}
|
|
9302
9855
|
};
|
|
@@ -9320,20 +9873,32 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9320
9873
|
void api.getGoal(threadId).then((g) => tui.setGoal(g)).catch(() => {
|
|
9321
9874
|
});
|
|
9322
9875
|
attaching.stop();
|
|
9876
|
+
const header = `${c4.bold}${c4.magenta}Standard Code${c4.reset} ${c4.dim}\u2014 ${agentTitle}${c4.reset}`;
|
|
9877
|
+
const remoteDaemonV = session.runner?.daemon?.version;
|
|
9878
|
+
const owner = currentOwner;
|
|
9879
|
+
let localOwnerDesc = ownsExecution ? "this terminal" : describeExecOwner(owner);
|
|
9880
|
+
if (!remote && !ownsExecution && owner?.client_kind === "daemon") {
|
|
9881
|
+
const selfRec = await loadMachine(api, session.identity.machine_id).catch(() => null);
|
|
9882
|
+
if (selfRec?.daemon?.version) localOwnerDesc = `the daemon on this machine (v${selfRec.daemon.version})`;
|
|
9883
|
+
}
|
|
9884
|
+
const execLine = remote ? `${c4.gray}tool execution:${c4.reset} daemon${remoteDaemonV ? ` v${remoteDaemonV}` : ""} on ${runnerName}` : `${c4.gray}tool execution:${c4.reset} ${localOwnerDesc}`;
|
|
9885
|
+
tui.setAgentLabel(agentTitle);
|
|
9323
9886
|
tui.banner(
|
|
9324
9887
|
remote ? [
|
|
9325
|
-
|
|
9326
|
-
`${
|
|
9327
|
-
`${
|
|
9888
|
+
header,
|
|
9889
|
+
`${c4.gray}project:${c4.reset} ${session.remotePath ?? "?"} ${c4.teal}on ${runnerName}${c4.reset}`,
|
|
9890
|
+
`${c4.gray}machine:${c4.reset} ${runnerName} ${c4.gray}thread:${c4.reset} ${threadId.slice(0, 8)}`,
|
|
9891
|
+
execLine
|
|
9328
9892
|
] : [
|
|
9329
|
-
|
|
9330
|
-
`${
|
|
9331
|
-
`${
|
|
9893
|
+
header,
|
|
9894
|
+
`${c4.gray}project:${c4.reset} ${projectDir}`,
|
|
9895
|
+
`${c4.gray}machine:${c4.reset} ${machine} ${c4.gray}thread:${c4.reset} ${threadId.slice(0, 8)}`,
|
|
9896
|
+
execLine
|
|
9332
9897
|
]
|
|
9333
9898
|
);
|
|
9334
9899
|
if (!remote && session.suggestDaemonInstall && process.platform !== "win32" && !serviceStatus().installed) {
|
|
9335
9900
|
tui.print(
|
|
9336
|
-
`${
|
|
9901
|
+
`${c4.dim}Tip: install the always-on daemon (${c4.reset}standardcode daemon install${c4.dim}) to start sessions on this machine from anywhere.${c4.reset}`
|
|
9337
9902
|
);
|
|
9338
9903
|
}
|
|
9339
9904
|
if (resumed) await printHistory(api, threadId, tui);
|
|
@@ -9344,17 +9909,17 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9344
9909
|
const runningProcs = (await registry.list()).filter((p) => p.status === "running");
|
|
9345
9910
|
if (runningProcs.length) {
|
|
9346
9911
|
tui.print(
|
|
9347
|
-
`${
|
|
9912
|
+
`${c4.cyan}\u2699 ${runningProcs.length} background process${runningProcs.length === 1 ? "" : "es"} running:${c4.reset}`
|
|
9348
9913
|
);
|
|
9349
|
-
for (const p of runningProcs) tui.print(`${
|
|
9914
|
+
for (const p of runningProcs) tui.print(`${c4.gray} ${p.id} ${p.description || p.command}${c4.reset}`);
|
|
9350
9915
|
}
|
|
9351
9916
|
refreshBgCount();
|
|
9352
9917
|
if (exec && ownsExecution) {
|
|
9353
9918
|
for (const res of await exec.connectEnabledMcpServers()) {
|
|
9354
9919
|
if (res.ok) {
|
|
9355
|
-
tui.print(`${
|
|
9920
|
+
tui.print(`${c4.cyan}\u26A1 MCP "${res.name}" connected${c4.reset} ${c4.gray}(${res.tools} tool${res.tools === 1 ? "" : "s"})${c4.reset}`);
|
|
9356
9921
|
} else {
|
|
9357
|
-
tui.print(`${
|
|
9922
|
+
tui.print(`${c4.red}\u26A0 MCP "${res.name}" failed:${c4.reset} ${c4.gray}${res.error}${c4.reset}`);
|
|
9358
9923
|
}
|
|
9359
9924
|
}
|
|
9360
9925
|
}
|
|
@@ -9370,8 +9935,8 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9370
9935
|
try {
|
|
9371
9936
|
const { choice, reason } = await tui.approval(
|
|
9372
9937
|
`${request.summary}${request.permission ? `
|
|
9373
|
-
${
|
|
9374
|
-
${
|
|
9938
|
+
${c4.bold}why: ${request.permission}${c4.reset}` : ""}
|
|
9939
|
+
${c4.dim}runs on ${request.machine || runnerName}${c4.reset}`,
|
|
9375
9940
|
request.risk
|
|
9376
9941
|
);
|
|
9377
9942
|
answeredApprovals.add(request.tool_call_id);
|
|
@@ -9422,7 +9987,7 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
9422
9987
|
continue;
|
|
9423
9988
|
}
|
|
9424
9989
|
if (m.role === "assistant" && text) printAssistant(tui, text);
|
|
9425
|
-
else if (m.role === "system" && text) tui.print(`${
|
|
9990
|
+
else if (m.role === "system" && text) tui.print(`${c4.dim}${text}${c4.reset}`);
|
|
9426
9991
|
else if (m.role === "user" && text) {
|
|
9427
9992
|
const pending = pendingSent.get(text) ?? 0;
|
|
9428
9993
|
if (pending > 0) {
|
|
@@ -9444,7 +10009,8 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
9444
10009
|
activeSteps.clear();
|
|
9445
10010
|
refreshStatus();
|
|
9446
10011
|
}
|
|
9447
|
-
|
|
10012
|
+
if (polledBusy) optimisticBusyUntil = 0;
|
|
10013
|
+
busy = polledBusy || Date.now() < optimisticBusyUntil;
|
|
9448
10014
|
tui.setWorking(busy);
|
|
9449
10015
|
if (!busy) {
|
|
9450
10016
|
if (activeSteps.size) activeSteps.clear();
|
|
@@ -9515,16 +10081,16 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
9515
10081
|
tui.setStep(null, 0);
|
|
9516
10082
|
tui.setBackgroundCount(0);
|
|
9517
10083
|
if (killed > 0) {
|
|
9518
|
-
tui.print(`${
|
|
10084
|
+
tui.print(`${c4.cyan}\u2699${c4.reset} Stopped ${killed} background process${killed === 1 ? "" : "es"}.`);
|
|
9519
10085
|
}
|
|
9520
|
-
tui.print(`${
|
|
10086
|
+
tui.print(`${c4.dim}\u2500\u2500 conversation cleared \u2014 starting a fresh session \u2500\u2500${c4.reset}`);
|
|
9521
10087
|
}
|
|
9522
10088
|
async function runSkillsMenu(tui, skills) {
|
|
9523
10089
|
let list;
|
|
9524
10090
|
try {
|
|
9525
10091
|
list = await skills.list();
|
|
9526
10092
|
} catch (e) {
|
|
9527
|
-
tui.print(`${
|
|
10093
|
+
tui.print(`${c4.red}\u2717 couldn't load skills:${c4.reset} ${c4.gray}${e instanceof Error ? e.message : String(e)}${c4.reset}`);
|
|
9528
10094
|
return;
|
|
9529
10095
|
}
|
|
9530
10096
|
const INSTALL = "__install__";
|
|
@@ -9535,7 +10101,7 @@ async function runSkillsMenu(tui, skills) {
|
|
|
9535
10101
|
}));
|
|
9536
10102
|
items.push({ label: "\uFF0B Install a skill\u2026", hint: "find & install", value: INSTALL });
|
|
9537
10103
|
const picked = await tui.select(
|
|
9538
|
-
`${
|
|
10104
|
+
`${c4.bold}Agent skills${c4.reset} ${c4.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c4.reset}`,
|
|
9539
10105
|
items
|
|
9540
10106
|
);
|
|
9541
10107
|
if (!picked) return;
|
|
@@ -9548,8 +10114,8 @@ async function runSkillsMenu(tui, skills) {
|
|
|
9548
10114
|
return;
|
|
9549
10115
|
}
|
|
9550
10116
|
const skill = list.find((s) => s.name === picked);
|
|
9551
|
-
tui.print(`${
|
|
9552
|
-
const action = await tui.select(`${
|
|
10117
|
+
tui.print(`${c4.cyan}${skill.name}${c4.reset}${skill.version ? ` ${c4.dim}v${skill.version}${c4.reset}` : ""} ${c4.gray}\u2014 ${skill.description}${c4.reset}`);
|
|
10118
|
+
const action = await tui.select(`${c4.bold}${picked}${c4.reset}`, [
|
|
9553
10119
|
skill.enabled ? { label: "Disable (hide from the agent)", value: "disable" } : { label: "Enable", value: "enable" },
|
|
9554
10120
|
{ label: "View files", value: "files" },
|
|
9555
10121
|
{ label: "Remove this skill", value: "remove" },
|
|
@@ -9558,20 +10124,20 @@ async function runSkillsMenu(tui, skills) {
|
|
|
9558
10124
|
try {
|
|
9559
10125
|
if (action === "enable" || action === "disable") {
|
|
9560
10126
|
await skills.setEnabled(picked, action === "enable");
|
|
9561
|
-
tui.print(`${
|
|
10127
|
+
tui.print(`${c4.gray}${action}d ${picked}${c4.reset}`);
|
|
9562
10128
|
} else if (action === "files") {
|
|
9563
|
-
for (const f of skill.files) tui.print(` ${
|
|
10129
|
+
for (const f of skill.files) tui.print(` ${c4.gray}${f}${c4.reset}`);
|
|
9564
10130
|
} else if (action === "remove") {
|
|
9565
10131
|
await skills.remove(picked);
|
|
9566
|
-
tui.print(`${
|
|
10132
|
+
tui.print(`${c4.gray}removed ${picked}${c4.reset}`);
|
|
9567
10133
|
}
|
|
9568
10134
|
} catch (e) {
|
|
9569
|
-
tui.print(`${
|
|
10135
|
+
tui.print(`${c4.red}\u2717 ${e instanceof Error ? e.message : String(e)}${c4.reset}`);
|
|
9570
10136
|
}
|
|
9571
10137
|
}
|
|
9572
10138
|
async function runLevelMenu(tui, perm) {
|
|
9573
10139
|
const picked = await tui.select(
|
|
9574
|
-
`${
|
|
10140
|
+
`${c4.bold}Auto-accept level${c4.reset} ${c4.dim}(\u2191/\u2193 \xB7 enter \xB7 shift-tab cycles)${c4.reset}`,
|
|
9575
10141
|
LEVELS.map((l) => ({
|
|
9576
10142
|
label: levelLabel(l),
|
|
9577
10143
|
hint: l === tui.level ? "current" : "",
|
|
@@ -9588,16 +10154,16 @@ async function runMachinesMenu(tui, api, self) {
|
|
|
9588
10154
|
try {
|
|
9589
10155
|
machines = await loadMachines(api);
|
|
9590
10156
|
} catch (e) {
|
|
9591
|
-
tui.print(`${
|
|
10157
|
+
tui.print(`${c4.red}\u2717 couldn't load machines:${c4.reset} ${c4.gray}${e instanceof Error ? e.message : String(e)}${c4.reset}`);
|
|
9592
10158
|
return;
|
|
9593
10159
|
}
|
|
9594
10160
|
if (!machines.length) {
|
|
9595
|
-
tui.print(`${
|
|
10161
|
+
tui.print(`${c4.gray}No machines registered yet. Run standardcode on a machine (or install its daemon) to register it.${c4.reset}`);
|
|
9596
10162
|
return;
|
|
9597
10163
|
}
|
|
9598
10164
|
machines.sort((a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0));
|
|
9599
10165
|
const picked = await tui.select(
|
|
9600
|
-
`${
|
|
10166
|
+
`${c4.bold}Your machines${c4.reset} ${c4.dim}(\u2191/\u2193 \xB7 enter \xB7 esc)${c4.reset}`,
|
|
9601
10167
|
machines.map((m) => {
|
|
9602
10168
|
const isSelf = m.id === self.machine_id;
|
|
9603
10169
|
const online = daemonOnline(m);
|
|
@@ -9631,26 +10197,26 @@ async function manageMachine(tui, api, self, machine) {
|
|
|
9631
10197
|
options.push({ label: "Back", value: "back" });
|
|
9632
10198
|
if (!isSelf && !machine.daemon) {
|
|
9633
10199
|
tui.print(
|
|
9634
|
-
`${
|
|
10200
|
+
`${c4.dim}${machine.name} has no daemon \u2014 you can rename it here; update and project changes need its daemon installed.${c4.reset}`
|
|
9635
10201
|
);
|
|
9636
10202
|
} else if (!isSelf && machine.daemon && !online) {
|
|
9637
10203
|
tui.print(
|
|
9638
|
-
`${
|
|
10204
|
+
`${c4.dim}${machine.name}'s daemon is offline \u2014 queued changes apply when it next comes online.${c4.reset}`
|
|
9639
10205
|
);
|
|
9640
10206
|
}
|
|
9641
|
-
const action = await tui.select(`${
|
|
10207
|
+
const action = await tui.select(`${c4.bold}${machineIcon(machine)} ${machine.name}${c4.reset}`, options);
|
|
9642
10208
|
if (!action || action === "back") return;
|
|
9643
10209
|
if (action === "icon") {
|
|
9644
10210
|
const current = machine.icon ?? "";
|
|
9645
10211
|
const emoji = await tui.prompt(
|
|
9646
|
-
`${
|
|
10212
|
+
`${c4.bold}Icon for ${machine.name}${c4.reset} ${c4.dim}(paste an emoji, blank to reset)${c4.reset}`,
|
|
9647
10213
|
current
|
|
9648
10214
|
);
|
|
9649
10215
|
if (emoji !== null) {
|
|
9650
10216
|
const trimmed = emoji.trim();
|
|
9651
10217
|
await setMachineIcon(api, machine.id, trimmed);
|
|
9652
10218
|
machine.icon = trimmed || void 0;
|
|
9653
|
-
tui.print(`${
|
|
10219
|
+
tui.print(`${c4.green}\u2713${c4.reset} icon ${trimmed ? `set to ${trimmed}` : "reset"} for ${machine.name}`);
|
|
9654
10220
|
}
|
|
9655
10221
|
return manageMachine(tui, api, self, machine);
|
|
9656
10222
|
}
|
|
@@ -9666,7 +10232,7 @@ async function manageMachine(tui, api, self, machine) {
|
|
|
9666
10232
|
const name = await tui.prompt(`New name for ${machine.name}`, machine.name);
|
|
9667
10233
|
if (name === null || !name.trim()) return;
|
|
9668
10234
|
await setMachineName(api, machine.id, name.trim());
|
|
9669
|
-
tui.print(`${
|
|
10235
|
+
tui.print(`${c4.green}\u2713${c4.reset} Renamed ${c4.bold}${machine.name}${c4.reset} \u2192 ${c4.bold}${name.trim()}${c4.reset}.`);
|
|
9670
10236
|
} else if (action === "update") {
|
|
9671
10237
|
if (isSelf) {
|
|
9672
10238
|
await runUpdateCommand(tui);
|
|
@@ -9677,7 +10243,7 @@ async function manageMachine(tui, api, self, machine) {
|
|
|
9677
10243
|
]);
|
|
9678
10244
|
if (go !== "yes") return;
|
|
9679
10245
|
await dispatch("update");
|
|
9680
|
-
tui.print(`${
|
|
10246
|
+
tui.print(`${c4.green}\u2713${c4.reset} Update ${c4.gray}${applyNote} (its daemon updates and restarts on the new version).${c4.reset}`);
|
|
9681
10247
|
}
|
|
9682
10248
|
} else if (action === "projects") {
|
|
9683
10249
|
await manageMachineProjects(tui, api, self, machine, dispatch, applyNote);
|
|
@@ -9686,78 +10252,103 @@ async function manageMachine(tui, api, self, machine) {
|
|
|
9686
10252
|
async function manageMachineProjects(tui, api, self, machine, dispatch, applyNote) {
|
|
9687
10253
|
const ADD = "__add__";
|
|
9688
10254
|
const paths = Object.keys(machine.projects).sort();
|
|
10255
|
+
const projLabels = projectDisplayLabels(machine.projects);
|
|
9689
10256
|
const picked = await tui.select(
|
|
9690
|
-
`${
|
|
10257
|
+
`${c4.bold}Projects on ${machine.name}${c4.reset} ${c4.dim}(\u2191\u2193 \xB7 enter \xB7 esc)${c4.reset}`,
|
|
9691
10258
|
[
|
|
9692
|
-
...paths.map((p) => ({
|
|
10259
|
+
...paths.map((p) => ({
|
|
10260
|
+
label: projLabels.get(p) ?? projectDisplayName(p, machine.projects[p]),
|
|
10261
|
+
detail: p,
|
|
10262
|
+
value: p
|
|
10263
|
+
})),
|
|
9693
10264
|
{ label: "\uFF0B Add a project directory\u2026", hint: "absolute path", value: ADD }
|
|
9694
|
-
]
|
|
10265
|
+
],
|
|
10266
|
+
{ spaced: true }
|
|
9695
10267
|
);
|
|
9696
10268
|
if (!picked) return;
|
|
9697
10269
|
if (picked === ADD) {
|
|
9698
|
-
const
|
|
9699
|
-
|
|
9700
|
-
|
|
10270
|
+
const isSelf = machine.id === self.machine_id;
|
|
10271
|
+
const backend = isSelf ? localBrowseBackend(machine.name) : remoteBrowseBackend(api, machine.id, machine.name);
|
|
10272
|
+
const chosen = await pickDirectory(tui, backend, {
|
|
10273
|
+
startPath: isSelf ? process.cwd() : null,
|
|
10274
|
+
loader: startLoader
|
|
10275
|
+
});
|
|
10276
|
+
if (!chosen) return;
|
|
10277
|
+
await dispatch("add_project", { path: chosen });
|
|
10278
|
+
tui.print(`${c4.green}\u2713${c4.reset} Add ${chosen} ${c4.gray}${applyNote}.${c4.reset}`);
|
|
10279
|
+
return;
|
|
10280
|
+
}
|
|
10281
|
+
const project = machine.projects[picked];
|
|
10282
|
+
const displayName = projectDisplayName(picked, project);
|
|
10283
|
+
const action = await tui.select(`${c4.bold}${displayName}${c4.reset} ${c4.dim}${shortenPath(picked, 48)}${c4.reset}`, [
|
|
10284
|
+
{ label: "Rename", hint: "display name only \u2014 the directory is untouched", value: "rename" },
|
|
10285
|
+
{ label: "Remove from this machine's projects", hint: "doesn't delete the directory", value: "remove" },
|
|
10286
|
+
{ label: "Back", value: "back" }
|
|
10287
|
+
]);
|
|
10288
|
+
if (!action || action === "back") return;
|
|
10289
|
+
if (action === "rename") {
|
|
10290
|
+
const name = await tui.prompt(
|
|
10291
|
+
`New name for ${displayName} (blank resets to the directory name)`,
|
|
10292
|
+
displayName
|
|
9701
10293
|
);
|
|
9702
|
-
if (
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
}
|
|
9708
|
-
await dispatch("add_project", { path: trimmed });
|
|
9709
|
-
tui.print(`${c3.green}\u2713${c3.reset} Add ${trimmed} ${c3.gray}${applyNote}.${c3.reset}`);
|
|
10294
|
+
if (name === null) return;
|
|
10295
|
+
await setProjectName(api, machine.id, picked, name);
|
|
10296
|
+
const now = name.trim() || projectDisplayName(picked, null);
|
|
10297
|
+
if (project) project.name = now;
|
|
10298
|
+
tui.print(`${c4.green}\u2713${c4.reset} Renamed ${c4.bold}${displayName}${c4.reset} \u2192 ${c4.bold}${now}${c4.reset}.`);
|
|
9710
10299
|
} else {
|
|
9711
10300
|
await dispatch("remove_project", { path: picked });
|
|
9712
|
-
tui.print(`${
|
|
10301
|
+
tui.print(`${c4.green}\u2713${c4.reset} Remove ${picked} ${c4.gray}${applyNote}.${c4.reset}`);
|
|
9713
10302
|
}
|
|
9714
10303
|
}
|
|
9715
10304
|
function showDaemonInfo(tui, session) {
|
|
9716
10305
|
if (session.mode === "remote" && session.runner) {
|
|
9717
10306
|
tui.print(
|
|
9718
|
-
`${
|
|
10307
|
+
`${c4.gray}This session runs on${c4.reset} ${c4.bold}${session.runner.name}${c4.reset} ${c4.gray}(${session.runner.hostname}) \u2014 its daemon executes the tools.${c4.reset}`
|
|
9719
10308
|
);
|
|
9720
10309
|
} else {
|
|
9721
|
-
tui.print(`${
|
|
10310
|
+
tui.print(`${c4.gray}This session runs on this machine.${c4.reset}`);
|
|
9722
10311
|
}
|
|
9723
10312
|
const status = serviceStatus();
|
|
9724
10313
|
tui.print(
|
|
9725
|
-
`${
|
|
10314
|
+
`${c4.gray}Daemon on this machine:${c4.reset} ${status.installed ? status.detail : "not installed"}`
|
|
9726
10315
|
);
|
|
9727
10316
|
if (!status.installed) {
|
|
9728
10317
|
tui.print(
|
|
9729
|
-
`${
|
|
10318
|
+
`${c4.gray}Install it to start sessions on this machine from anywhere:${c4.reset} ${c4.bold}standardcode daemon install${c4.reset}`
|
|
9730
10319
|
);
|
|
9731
10320
|
tui.print(
|
|
9732
|
-
`${
|
|
10321
|
+
`${c4.dim}The daemon keeps running after you close the terminal \u2014 it self-restarts, self-updates, and executes sessions you start from other machines.${c4.reset}`
|
|
9733
10322
|
);
|
|
9734
10323
|
} else {
|
|
9735
|
-
tui.print(`${
|
|
10324
|
+
tui.print(`${c4.dim}Manage it with standardcode daemon status | uninstall | add-project <path>.${c4.reset}`);
|
|
9736
10325
|
}
|
|
9737
10326
|
}
|
|
9738
10327
|
function showKeybindings(tui) {
|
|
9739
|
-
tui.print(`${
|
|
9740
|
-
tui.print(`${
|
|
9741
|
-
tui.print(`${
|
|
9742
|
-
tui.print(`${
|
|
9743
|
-
tui.print(`${
|
|
9744
|
-
tui.print(`${
|
|
9745
|
-
tui.print(`${
|
|
9746
|
-
tui.print(`${
|
|
10328
|
+
tui.print(`${c4.gray}shortcuts:${c4.reset}`);
|
|
10329
|
+
tui.print(`${c4.gray} shift-tab${c4.reset} cycle auto-accept level (1\u20135)`);
|
|
10330
|
+
tui.print(`${c4.gray} shift-\u23CE${c4.reset} insert a newline (multiline input)`);
|
|
10331
|
+
tui.print(`${c4.gray} option-\u23CE${c4.reset} steer the working agent at the next safe boundary`);
|
|
10332
|
+
tui.print(`${c4.gray} /${c4.reset} open the command palette (type to filter)`);
|
|
10333
|
+
tui.print(`${c4.gray} !cmd${c4.reset} run a shell command on the session's machine (e.g. !ls); !! to send a literal !`);
|
|
10334
|
+
tui.print(`${c4.gray} ctrl-v${c4.reset} paste an image from the clipboard ([#Image 1])`);
|
|
10335
|
+
tui.print(`${c4.gray} \u2191 / \u2193${c4.reset} cycle past messages (on the input's top line)`);
|
|
10336
|
+
tui.print(`${c4.gray} \u2190${c4.reset} from the start of the input: select the [\u2699 n bg] badge (enter opens it)`);
|
|
10337
|
+
tui.print(`${c4.gray} ctrl-c${c4.reset} quit`);
|
|
9747
10338
|
}
|
|
9748
10339
|
async function runUpdateCommand(tui) {
|
|
9749
10340
|
const version = readVersion();
|
|
9750
10341
|
const result = await forceCheckForUpdate(version);
|
|
9751
10342
|
if (!result) {
|
|
9752
|
-
tui.print(`${
|
|
10343
|
+
tui.print(`${c4.green}\u2713${c4.reset} ${c4.gray}@standardagents/code${c4.reset} is up to date (v${version})`);
|
|
9753
10344
|
return;
|
|
9754
10345
|
}
|
|
9755
10346
|
const { latest } = result;
|
|
9756
10347
|
tui.print(`
|
|
9757
|
-
${
|
|
10348
|
+
${c4.yellow}\u27F3${c4.reset} Update available: ${c4.gray}v${version}${c4.reset} \u2192 ${c4.green}v${latest}${c4.reset}`);
|
|
9758
10349
|
const pm = detectPackageManager();
|
|
9759
10350
|
if (!pm) {
|
|
9760
|
-
tui.print(` ${
|
|
10351
|
+
tui.print(` ${c4.gray}This is a source checkout \u2014 pull the repo to update.${c4.reset}`);
|
|
9761
10352
|
return;
|
|
9762
10353
|
}
|
|
9763
10354
|
const { display } = updateCommand(pm);
|
|
@@ -9766,28 +10357,28 @@ async function runUpdateCommand(tui) {
|
|
|
9766
10357
|
{ label: "No, skip", value: "no" }
|
|
9767
10358
|
]);
|
|
9768
10359
|
if (choice === "yes") {
|
|
9769
|
-
tui.print(` ${
|
|
10360
|
+
tui.print(` ${c4.gray}Running ${display}\u2026${c4.reset}`);
|
|
9770
10361
|
const { ok, output: pmOutput } = await runUpdate(pm);
|
|
9771
10362
|
if (ok) {
|
|
9772
|
-
tui.print(` ${
|
|
10363
|
+
tui.print(` ${c4.green}\u2713${c4.reset} Updated to v${latest}. Restart to use the new version.`);
|
|
9773
10364
|
} else {
|
|
9774
|
-
tui.print(` ${
|
|
10365
|
+
tui.print(` ${c4.red}\u2717${c4.reset} Update failed:`);
|
|
9775
10366
|
for (const line of pmOutput.trim().split("\n").slice(-6)) {
|
|
9776
|
-
tui.print(` ${
|
|
10367
|
+
tui.print(` ${c4.dim}${line}${c4.reset}`);
|
|
9777
10368
|
}
|
|
9778
10369
|
}
|
|
9779
10370
|
} else {
|
|
9780
|
-
tui.print(` ${
|
|
10371
|
+
tui.print(` ${c4.gray}Skipped. Run /update later.${c4.reset}`);
|
|
9781
10372
|
}
|
|
9782
10373
|
}
|
|
9783
10374
|
async function runProcessMenu(tui, bg) {
|
|
9784
10375
|
const procs = await bg.list();
|
|
9785
10376
|
if (!procs.length) {
|
|
9786
|
-
tui.print(`${
|
|
10377
|
+
tui.print(`${c4.gray}No background processes for this session.${c4.reset}`);
|
|
9787
10378
|
return;
|
|
9788
10379
|
}
|
|
9789
10380
|
const items = procs.map((p) => {
|
|
9790
|
-
const status = p.status === "running" ? `${
|
|
10381
|
+
const status = p.status === "running" ? `${c4.green}running${c4.reset}` : `${c4.gray}${p.status}${typeof p.exitCode === "number" ? ` (exit ${p.exitCode})` : ""}${c4.reset}`;
|
|
9791
10382
|
return {
|
|
9792
10383
|
label: `${p.description || p.command}`,
|
|
9793
10384
|
hint: `${p.id} \xB7 ${status}`,
|
|
@@ -9795,22 +10386,22 @@ async function runProcessMenu(tui, bg) {
|
|
|
9795
10386
|
};
|
|
9796
10387
|
});
|
|
9797
10388
|
const picked = await tui.select(
|
|
9798
|
-
`${
|
|
10389
|
+
`${c4.bold}Background processes${c4.reset} ${c4.dim}(\u2191/\u2193 \xB7 enter to manage \xB7 esc to close)${c4.reset}`,
|
|
9799
10390
|
items
|
|
9800
10391
|
);
|
|
9801
10392
|
if (!picked) return;
|
|
9802
10393
|
const proc = procs.find((p) => p.id === picked);
|
|
9803
10394
|
if (!proc || proc.status !== "running") {
|
|
9804
|
-
tui.print(`${
|
|
10395
|
+
tui.print(`${c4.gray}${picked} is not running.${c4.reset}`);
|
|
9805
10396
|
return;
|
|
9806
10397
|
}
|
|
9807
|
-
const action = await tui.select(`${
|
|
10398
|
+
const action = await tui.select(`${c4.bold}${proc.description || proc.command}${c4.reset}`, [
|
|
9808
10399
|
{ label: "Stop this process", value: "stop" },
|
|
9809
10400
|
{ label: "Leave it running", value: "leave" }
|
|
9810
10401
|
]);
|
|
9811
10402
|
if (action === "stop") {
|
|
9812
10403
|
await bg.stop(picked);
|
|
9813
|
-
tui.print(`${
|
|
10404
|
+
tui.print(`${c4.gray}stopped ${picked}${c4.reset}`);
|
|
9814
10405
|
}
|
|
9815
10406
|
}
|
|
9816
10407
|
async function runApprovalsMenu(tui, perm, save) {
|
|
@@ -9818,7 +10409,7 @@ async function runApprovalsMenu(tui, perm, save) {
|
|
|
9818
10409
|
const risks = Array.from(perm.allowRisk).sort((a, b) => a - b);
|
|
9819
10410
|
if (!tools.length && !risks.length) {
|
|
9820
10411
|
tui.print(
|
|
9821
|
-
`${
|
|
10412
|
+
`${c4.gray}No pre-approved commands. At a permission prompt, choose "Always allow this tool" or "Allow all level N" to add some.${c4.reset}`
|
|
9822
10413
|
);
|
|
9823
10414
|
return;
|
|
9824
10415
|
}
|
|
@@ -9828,22 +10419,22 @@ async function runApprovalsMenu(tui, perm, save) {
|
|
|
9828
10419
|
{ label: "Clear all approvals", hint: "", value: "clear" }
|
|
9829
10420
|
];
|
|
9830
10421
|
const picked = await tui.select(
|
|
9831
|
-
`${
|
|
10422
|
+
`${c4.bold}Approved commands${c4.reset} ${c4.dim}(enter to revoke \xB7 esc to close)${c4.reset}`,
|
|
9832
10423
|
items
|
|
9833
10424
|
);
|
|
9834
10425
|
if (!picked) return;
|
|
9835
10426
|
if (picked === "clear") {
|
|
9836
10427
|
perm.alwaysAllow.clear();
|
|
9837
10428
|
perm.allowRisk.clear();
|
|
9838
|
-
tui.print(`${
|
|
10429
|
+
tui.print(`${c4.gray}cleared all approvals${c4.reset}`);
|
|
9839
10430
|
} else if (picked.startsWith("tool:")) {
|
|
9840
10431
|
const t = picked.slice(5);
|
|
9841
10432
|
perm.alwaysAllow.delete(t);
|
|
9842
|
-
tui.print(`${
|
|
10433
|
+
tui.print(`${c4.gray}revoked tool ${t}${c4.reset}`);
|
|
9843
10434
|
} else if (picked.startsWith("risk:")) {
|
|
9844
10435
|
const r = Number(picked.slice(5));
|
|
9845
10436
|
perm.allowRisk.delete(r);
|
|
9846
|
-
tui.print(`${
|
|
10437
|
+
tui.print(`${c4.gray}revoked level ${r}${c4.reset}`);
|
|
9847
10438
|
}
|
|
9848
10439
|
save();
|
|
9849
10440
|
}
|
|
@@ -9861,7 +10452,7 @@ async function runMcpMenu(tui, mcp) {
|
|
|
9861
10452
|
items.push({ label: "\uFF0B Install a new MCP server\u2026", hint: "find & install", value: INSTALL });
|
|
9862
10453
|
items.push({ label: "Add manually (name: command)\u2026", hint: "advanced", value: ADD_MANUAL });
|
|
9863
10454
|
const picked = await tui.select(
|
|
9864
|
-
`${
|
|
10455
|
+
`${c4.bold}MCP servers${c4.reset} ${c4.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c4.reset}`,
|
|
9865
10456
|
items
|
|
9866
10457
|
);
|
|
9867
10458
|
if (!picked) return;
|
|
@@ -9875,7 +10466,7 @@ async function runMcpMenu(tui, mcp) {
|
|
|
9875
10466
|
}
|
|
9876
10467
|
const server = configured.find((s) => s.name === picked);
|
|
9877
10468
|
const isConnected = connected.has(picked);
|
|
9878
|
-
const action = await tui.select(`${
|
|
10469
|
+
const action = await tui.select(`${c4.bold}${picked}${c4.reset}`, [
|
|
9879
10470
|
{ label: "View tools", value: "tools" },
|
|
9880
10471
|
isConnected ? { label: "Disconnect", value: "disconnect" } : { label: "Connect", value: "connect" },
|
|
9881
10472
|
server.enabled ? { label: "Disable (don't auto-connect)", value: "disable" } : { label: "Enable (auto-connect on start)", value: "enable" },
|
|
@@ -9885,29 +10476,29 @@ async function runMcpMenu(tui, mcp) {
|
|
|
9885
10476
|
if (action === "tools") {
|
|
9886
10477
|
const entry = mcp.catalog().servers.find((e) => e.name === picked);
|
|
9887
10478
|
if (!entry || entry.status !== "connected") {
|
|
9888
|
-
tui.print(`${
|
|
10479
|
+
tui.print(`${c4.gray}${picked} is not connected \u2014 connect it to list tools.${c4.reset}`);
|
|
9889
10480
|
return;
|
|
9890
10481
|
}
|
|
9891
|
-
if (!entry.tools.length) tui.print(`${
|
|
9892
|
-
for (const t of entry.tools) tui.print(` ${
|
|
9893
|
-
if (entry.resources.length) tui.print(` ${
|
|
10482
|
+
if (!entry.tools.length) tui.print(`${c4.gray}${picked} exposes no tools.${c4.reset}`);
|
|
10483
|
+
for (const t of entry.tools) tui.print(` ${c4.cyan}${t.name}${c4.reset}${t.description ? ` ${c4.gray}\u2014 ${t.description}${c4.reset}` : ""}`);
|
|
10484
|
+
if (entry.resources.length) tui.print(` ${c4.gray}${entry.resources.length} resource(s)${c4.reset}`);
|
|
9894
10485
|
} else if (action === "connect") {
|
|
9895
10486
|
const res = await mcp.connect(server);
|
|
9896
|
-
tui.print(res.ok ? `${
|
|
10487
|
+
tui.print(res.ok ? `${c4.cyan}\u26A1 connected (${res.tools} tools)${c4.reset}` : `${c4.red}\u26A0 ${res.error}${c4.reset}`);
|
|
9897
10488
|
} else if (action === "disconnect") {
|
|
9898
10489
|
mcp.disconnect(picked);
|
|
9899
|
-
tui.print(`${
|
|
10490
|
+
tui.print(`${c4.gray}disconnected ${picked}${c4.reset}`);
|
|
9900
10491
|
} else if (action === "enable") {
|
|
9901
10492
|
mcp.setEnabled(picked, true);
|
|
9902
10493
|
const res = await mcp.connect(server);
|
|
9903
|
-
tui.print(res.ok ? `${
|
|
10494
|
+
tui.print(res.ok ? `${c4.cyan}\u26A1 enabled + connected (${res.tools} tools)${c4.reset}` : `${c4.red}\u26A0 enabled but failed: ${res.error}${c4.reset}`);
|
|
9904
10495
|
} else if (action === "disable") {
|
|
9905
10496
|
mcp.setEnabled(picked, false);
|
|
9906
10497
|
mcp.disconnect(picked);
|
|
9907
|
-
tui.print(`${
|
|
10498
|
+
tui.print(`${c4.gray}disabled + disconnected ${picked}${c4.reset}`);
|
|
9908
10499
|
} else if (action === "remove") {
|
|
9909
10500
|
mcp.remove(picked);
|
|
9910
|
-
tui.print(`${
|
|
10501
|
+
tui.print(`${c4.gray}removed ${picked}${c4.reset}`);
|
|
9911
10502
|
}
|
|
9912
10503
|
}
|
|
9913
10504
|
async function addMcpServer(tui, mcp) {
|
|
@@ -9918,13 +10509,13 @@ async function addMcpServer(tui, mcp) {
|
|
|
9918
10509
|
if (!spec) return;
|
|
9919
10510
|
const cfg = parseServerSpec(spec);
|
|
9920
10511
|
if (!cfg) {
|
|
9921
|
-
tui.print(`${
|
|
10512
|
+
tui.print(`${c4.yellow}couldn't parse that. Use name: command [args]${c4.reset}`);
|
|
9922
10513
|
return;
|
|
9923
10514
|
}
|
|
9924
|
-
tui.print(`${
|
|
10515
|
+
tui.print(`${c4.gray}connecting MCP "${cfg.name}" (${cfg.command} ${cfg.args.join(" ")})\u2026${c4.reset}`);
|
|
9925
10516
|
const res = await mcp.add(cfg);
|
|
9926
|
-
if (res.ok) tui.print(`${
|
|
9927
|
-
else tui.print(`${
|
|
10517
|
+
if (res.ok) tui.print(`${c4.cyan}\u26A1 MCP "${cfg.name}" connected${c4.reset} ${c4.gray}(${res.tools} tool${res.tools === 1 ? "" : "s"})${c4.reset}`);
|
|
10518
|
+
else tui.print(`${c4.red}\u26A0 MCP "${cfg.name}" failed:${c4.reset} ${c4.gray}${res.error}${c4.reset} ${c4.dim}(saved; retry from the MCP menu)${c4.reset}`);
|
|
9928
10519
|
}
|
|
9929
10520
|
async function installMcpServerFlow(tui, mcp) {
|
|
9930
10521
|
const query = await tui.prompt(
|