@standardagents/code 0.7.2 → 0.9.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 +2021 -402
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import fs4 from 'fs';
|
|
2
|
+
import os9, { homedir } from 'os';
|
|
4
3
|
import path3 from 'path';
|
|
5
4
|
import readline2 from 'readline/promises';
|
|
6
|
-
import { spawn, execFile } from 'child_process';
|
|
7
5
|
import { stdout, stdin } from 'process';
|
|
6
|
+
import fs4 from 'fs';
|
|
8
7
|
import fsp from 'fs/promises';
|
|
9
8
|
import crypto from 'crypto';
|
|
9
|
+
import { spawn, execFileSync, spawnSync, execFile } from 'child_process';
|
|
10
10
|
import readline from 'readline';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
|
|
@@ -251,8 +251,16 @@ var ApiClient = class {
|
|
|
251
251
|
* Retries with backoff — this is the durable delivery path, so it must land
|
|
252
252
|
* even if the connection is briefly flaky after a permission wait.
|
|
253
253
|
*/
|
|
254
|
-
async postToolResult(threadId, toolCallId, ok, result, error) {
|
|
255
|
-
const body = JSON.stringify({
|
|
254
|
+
async postToolResult(threadId, toolCallId, ok, result, error, executor) {
|
|
255
|
+
const body = JSON.stringify({
|
|
256
|
+
tool_call_id: toolCallId,
|
|
257
|
+
ok,
|
|
258
|
+
result,
|
|
259
|
+
error,
|
|
260
|
+
// Ownership fence: calls dispatched to a specific execution owner are
|
|
261
|
+
// only accepted back from that owner at that generation.
|
|
262
|
+
...executor ? { client_id: executor.clientId, generation: executor.generation ?? 0 } : {}
|
|
263
|
+
});
|
|
256
264
|
for (let attempt = 0; attempt < 6; attempt++) {
|
|
257
265
|
try {
|
|
258
266
|
await this.json(`/api/threads/${threadId}/tool-result`, {
|
|
@@ -283,6 +291,39 @@ var ApiClient = class {
|
|
|
283
291
|
return null;
|
|
284
292
|
}
|
|
285
293
|
}
|
|
294
|
+
// ── account-wide user KV (per-user, spans every thread) ───────────────────
|
|
295
|
+
// Backed by the instance's /api/users/me/kv store. This is where the machine
|
|
296
|
+
// registry lives: which machines (laptops / VPS daemons) belong to this
|
|
297
|
+
// account, and which projects each machine has.
|
|
298
|
+
/** Read one value from the signed-in user's account-wide KV (null if absent). */
|
|
299
|
+
async userKvGet(key) {
|
|
300
|
+
try {
|
|
301
|
+
const res = await this.json(
|
|
302
|
+
`/api/users/me/kv?key=${encodeURIComponent(key)}`
|
|
303
|
+
);
|
|
304
|
+
return res?.value ?? null;
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/** Write one value to the signed-in user's account-wide KV (null deletes). */
|
|
310
|
+
async userKvSet(key, value) {
|
|
311
|
+
await this.json(`/api/users/me/kv`, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
body: JSON.stringify({ key, value })
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/** List the signed-in user's account-wide KV entries under a key prefix. */
|
|
317
|
+
async userKvList(prefix) {
|
|
318
|
+
try {
|
|
319
|
+
const res = await this.json(
|
|
320
|
+
`/api/users/me/kv?prefix=${encodeURIComponent(prefix)}&limit=200`
|
|
321
|
+
);
|
|
322
|
+
return Array.isArray(res?.entries) ? res.entries : [];
|
|
323
|
+
} catch {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
286
327
|
/** Read a value from the thread's durable KV store (null if absent). */
|
|
287
328
|
async kvGet(threadId, key) {
|
|
288
329
|
try {
|
|
@@ -654,18 +695,20 @@ function gradientText(text, phase = 0) {
|
|
|
654
695
|
// src/bridge.ts
|
|
655
696
|
var PATH_ARG_TOOLS = /* @__PURE__ */ new Set(["read_file", "grep", "glob", "write_file", "edit_file"]);
|
|
656
697
|
var Bridge = class {
|
|
657
|
-
constructor(api, threadId, host, perm, hooks) {
|
|
698
|
+
constructor(api, threadId, host, perm, hooks, identity) {
|
|
658
699
|
this.api = api;
|
|
659
700
|
this.threadId = threadId;
|
|
660
701
|
this.host = host;
|
|
661
702
|
this.perm = perm;
|
|
662
703
|
this.hooks = hooks;
|
|
704
|
+
this.identity = identity;
|
|
663
705
|
}
|
|
664
706
|
api;
|
|
665
707
|
threadId;
|
|
666
708
|
host;
|
|
667
709
|
perm;
|
|
668
710
|
hooks;
|
|
711
|
+
identity;
|
|
669
712
|
ws = null;
|
|
670
713
|
closed = false;
|
|
671
714
|
heartbeat = null;
|
|
@@ -675,6 +718,43 @@ var Bridge = class {
|
|
|
675
718
|
// Durable forwarded calls we've started handling, so a server re-send (after a
|
|
676
719
|
// reconnect) doesn't prompt or run them twice.
|
|
677
720
|
handledDurable = /* @__PURE__ */ new Set();
|
|
721
|
+
// Whether this client currently holds execution ownership. Starts true so an
|
|
722
|
+
// instance that predates the owner concept (no owner field in bridge_ready)
|
|
723
|
+
// behaves exactly as before; an owner-aware instance sets it on every connect.
|
|
724
|
+
owner = true;
|
|
725
|
+
// Ownership generation from the last bridge_ready/owner_changed (echoed in
|
|
726
|
+
// durable deliveries so the server's fence can match).
|
|
727
|
+
ownerGeneration = 0;
|
|
728
|
+
// Resolved once the first owner-aware bridge_ready arrives (or never, on a
|
|
729
|
+
// legacy instance — callers pair this with a timeout).
|
|
730
|
+
ownershipKnownResolve = null;
|
|
731
|
+
ownershipKnownPromise = new Promise((resolve) => {
|
|
732
|
+
this.ownershipKnownResolve = resolve;
|
|
733
|
+
});
|
|
734
|
+
/** Whether this client currently executes forwarded tool calls. */
|
|
735
|
+
get isOwner() {
|
|
736
|
+
return this.owner;
|
|
737
|
+
}
|
|
738
|
+
/** Adjust the ownership claim used on the NEXT (re)connect — e.g. an
|
|
739
|
+
* interactive session that became owner reasserts with `takeover` after a
|
|
740
|
+
* brief drop, instead of silently losing execution to a daemon's
|
|
741
|
+
* stale-claim probe. */
|
|
742
|
+
setClaim(claim) {
|
|
743
|
+
if (this.identity) this.identity.claim = claim;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Resolve once the instance has said whether this client owns execution
|
|
747
|
+
* (the first owner-aware bridge_ready). Legacy instances never say — the
|
|
748
|
+
* timeout resolves to the current (assumed-owner) state so callers can gate
|
|
749
|
+
* owner-only side effects like session_info/mcp_catalog writes.
|
|
750
|
+
*/
|
|
751
|
+
async whenOwnershipKnown(timeoutMs = 3e3) {
|
|
752
|
+
await Promise.race([
|
|
753
|
+
this.ownershipKnownPromise,
|
|
754
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
755
|
+
]);
|
|
756
|
+
return this.owner;
|
|
757
|
+
}
|
|
678
758
|
/**
|
|
679
759
|
* Connect and keep the bridge connected. Resolves on the first successful
|
|
680
760
|
* open; thereafter any drop is reconnected automatically with exponential
|
|
@@ -697,7 +777,13 @@ var Bridge = class {
|
|
|
697
777
|
}
|
|
698
778
|
openSocket() {
|
|
699
779
|
if (this.closed) return;
|
|
700
|
-
|
|
780
|
+
let url = `${this.api.wsEndpoint}/api/threads/${this.threadId}/bridge?token=${encodeURIComponent(this.api.bearer)}`;
|
|
781
|
+
if (this.identity) {
|
|
782
|
+
url += `&client_id=${encodeURIComponent(this.identity.clientId)}&client_kind=${encodeURIComponent(this.identity.clientKind)}&claim=${encodeURIComponent(this.identity.claim)}`;
|
|
783
|
+
if (this.identity.clientName) {
|
|
784
|
+
url += `&client_name=${encodeURIComponent(this.identity.clientName)}`;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
701
787
|
let ws;
|
|
702
788
|
try {
|
|
703
789
|
ws = new WebSocket(url);
|
|
@@ -718,7 +804,17 @@ var Bridge = class {
|
|
|
718
804
|
this.onMessage(String(ev.data));
|
|
719
805
|
});
|
|
720
806
|
ws.addEventListener("error", () => this.handleDrop(ws));
|
|
721
|
-
ws.addEventListener("close", () =>
|
|
807
|
+
ws.addEventListener("close", (ev) => {
|
|
808
|
+
if (ev.code === 4001 && this.ws === ws) {
|
|
809
|
+
this.closed = true;
|
|
810
|
+
this.owner = false;
|
|
811
|
+
this.ws = null;
|
|
812
|
+
this.stopHeartbeat();
|
|
813
|
+
this.hooks.onSuperseded?.();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
this.handleDrop(ws);
|
|
817
|
+
});
|
|
722
818
|
}
|
|
723
819
|
handleDrop(ws) {
|
|
724
820
|
if (this.ws !== ws) return;
|
|
@@ -757,6 +853,24 @@ var Bridge = class {
|
|
|
757
853
|
}
|
|
758
854
|
this.ws?.close();
|
|
759
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* Force a reconnect now. Ownership claims are evaluated at bridge connect,
|
|
858
|
+
* so a non-owner that wants to re-check (e.g. the daemon probing whether a
|
|
859
|
+
* disconnected interactive owner has gone stale) cycles its socket.
|
|
860
|
+
*/
|
|
861
|
+
refresh() {
|
|
862
|
+
if (this.closed) return;
|
|
863
|
+
const ws = this.ws;
|
|
864
|
+
if (ws) {
|
|
865
|
+
try {
|
|
866
|
+
ws.close();
|
|
867
|
+
} catch {
|
|
868
|
+
}
|
|
869
|
+
this.handleDrop(ws);
|
|
870
|
+
} else {
|
|
871
|
+
this.scheduleReconnect();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
760
874
|
send(payload) {
|
|
761
875
|
try {
|
|
762
876
|
this.ws?.send(JSON.stringify(payload));
|
|
@@ -770,7 +884,44 @@ var Bridge = class {
|
|
|
770
884
|
} catch {
|
|
771
885
|
return;
|
|
772
886
|
}
|
|
887
|
+
if (msg.type === "bridge_ready") {
|
|
888
|
+
if (typeof msg.owner === "boolean") {
|
|
889
|
+
this.owner = msg.owner;
|
|
890
|
+
const owner = parseOwnerInfo(msg.current_owner);
|
|
891
|
+
this.ownerGeneration = owner?.generation ?? 0;
|
|
892
|
+
if (typeof msg.claim_refused === "string") {
|
|
893
|
+
this.hooks.onClaimRefused?.(msg.claim_refused);
|
|
894
|
+
}
|
|
895
|
+
this.hooks.onOwnership?.(msg.owner, owner);
|
|
896
|
+
this.ownershipKnownResolve?.();
|
|
897
|
+
}
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (msg.type === "owner_changed") {
|
|
901
|
+
const owner = parseOwnerInfo(msg.owner);
|
|
902
|
+
this.owner = !!owner && !!this.identity && owner.client_id === this.identity.clientId;
|
|
903
|
+
this.ownerGeneration = owner?.generation ?? 0;
|
|
904
|
+
this.hooks.onOwnership?.(this.owner, owner);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (msg.type === "superseded") {
|
|
908
|
+
this.closed = true;
|
|
909
|
+
this.owner = false;
|
|
910
|
+
this.stopHeartbeat();
|
|
911
|
+
if (this.reconnectTimer) {
|
|
912
|
+
clearTimeout(this.reconnectTimer);
|
|
913
|
+
this.reconnectTimer = null;
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
this.ws?.close();
|
|
917
|
+
} catch {
|
|
918
|
+
}
|
|
919
|
+
this.ws = null;
|
|
920
|
+
this.hooks.onSuperseded?.();
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
773
923
|
if (msg.type !== "tool_request") return;
|
|
924
|
+
if (!this.owner) return;
|
|
774
925
|
const req = msg;
|
|
775
926
|
await this.handleToolRequest(req);
|
|
776
927
|
}
|
|
@@ -781,7 +932,11 @@ var Bridge = class {
|
|
|
781
932
|
*/
|
|
782
933
|
respond(req, ok, result, error) {
|
|
783
934
|
if (req.durable && req.toolCallId) {
|
|
784
|
-
|
|
935
|
+
const executor = this.identity ? {
|
|
936
|
+
clientId: this.identity.clientId,
|
|
937
|
+
generation: typeof req.generation === "number" ? req.generation : this.ownerGeneration
|
|
938
|
+
} : void 0;
|
|
939
|
+
void this.api.postToolResult(this.threadId, req.toolCallId, ok, result, error, executor);
|
|
785
940
|
} else {
|
|
786
941
|
this.send({ type: "tool_response", id: req.id, ok, result, error });
|
|
787
942
|
}
|
|
@@ -801,6 +956,8 @@ var Bridge = class {
|
|
|
801
956
|
this.respond(req, false, void 0, "Blocked: this command is considered catastrophic and was refused by the client safety guard.");
|
|
802
957
|
return;
|
|
803
958
|
}
|
|
959
|
+
await this.hooks.refreshPermissions?.().catch(() => {
|
|
960
|
+
});
|
|
804
961
|
const permKey = permissionKey(req);
|
|
805
962
|
const decision = decide(this.perm, permKey, effectiveRisk, !!req.requestPermission);
|
|
806
963
|
if (decision === "deny") {
|
|
@@ -845,6 +1002,17 @@ var Bridge = class {
|
|
|
845
1002
|
}
|
|
846
1003
|
}
|
|
847
1004
|
};
|
|
1005
|
+
function parseOwnerInfo(value) {
|
|
1006
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1007
|
+
const r = value;
|
|
1008
|
+
if (typeof r.client_id !== "string" || r.client_id.length === 0) return null;
|
|
1009
|
+
return {
|
|
1010
|
+
client_id: r.client_id,
|
|
1011
|
+
client_name: typeof r.client_name === "string" ? r.client_name : null,
|
|
1012
|
+
client_kind: typeof r.client_kind === "string" ? r.client_kind : null,
|
|
1013
|
+
generation: typeof r.generation === "number" ? r.generation : void 0
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
848
1016
|
function permissionKey(req) {
|
|
849
1017
|
if (req.tool !== "mcp") return req.tool;
|
|
850
1018
|
const a = req.args;
|
|
@@ -929,7 +1097,7 @@ function detailSuffix(tool, result) {
|
|
|
929
1097
|
const lines = result.split("\n").length;
|
|
930
1098
|
return ` (${lines} line${lines === 1 ? "" : "s"})`;
|
|
931
1099
|
}
|
|
932
|
-
var LOG_DIR = path3.join(
|
|
1100
|
+
var LOG_DIR = path3.join(os9.homedir(), ".standardagents", "process-logs");
|
|
933
1101
|
var KEY2 = "bg_processes";
|
|
934
1102
|
function isAlive(pid) {
|
|
935
1103
|
try {
|
|
@@ -1006,7 +1174,7 @@ var ProcessRegistry = class {
|
|
|
1006
1174
|
}
|
|
1007
1175
|
};
|
|
1008
1176
|
function configFile() {
|
|
1009
|
-
return process.env.STANDARDAGENTS_MCP_CONFIG || path3.join(
|
|
1177
|
+
return process.env.STANDARDAGENTS_MCP_CONFIG || path3.join(os9.homedir(), ".standardagents", "mcp.json");
|
|
1010
1178
|
}
|
|
1011
1179
|
function loadMcpConfig() {
|
|
1012
1180
|
try {
|
|
@@ -1063,12 +1231,12 @@ function serverFromCommand(name, commandLine, env) {
|
|
|
1063
1231
|
const [command, ...args] = parts;
|
|
1064
1232
|
return { name: cleanName, command, args, env, enabled: true };
|
|
1065
1233
|
}
|
|
1066
|
-
function tokenize(
|
|
1234
|
+
function tokenize(input3) {
|
|
1067
1235
|
const out = [];
|
|
1068
1236
|
let cur = "";
|
|
1069
1237
|
let quote = null;
|
|
1070
|
-
for (let i = 0; i <
|
|
1071
|
-
const ch =
|
|
1238
|
+
for (let i = 0; i < input3.length; i++) {
|
|
1239
|
+
const ch = input3[i];
|
|
1072
1240
|
if (quote) {
|
|
1073
1241
|
if (ch === quote) quote = null;
|
|
1074
1242
|
else cur += ch;
|
|
@@ -1308,7 +1476,7 @@ var HostTools = class {
|
|
|
1308
1476
|
}
|
|
1309
1477
|
}
|
|
1310
1478
|
const hash = crypto.createHash("sha256").update(JSON.stringify(files)).digest("hex").slice(0, 12);
|
|
1311
|
-
const skillDir = path3.join(
|
|
1479
|
+
const skillDir = path3.join(os9.tmpdir(), "standardcode-skills", `${skill}-${hash}`);
|
|
1312
1480
|
for (const f of files) {
|
|
1313
1481
|
const dest = path3.resolve(skillDir, f.path);
|
|
1314
1482
|
if (path3.relative(skillDir, dest).startsWith("..")) {
|
|
@@ -2163,7 +2331,7 @@ function fromFile(filePath) {
|
|
|
2163
2331
|
}
|
|
2164
2332
|
}
|
|
2165
2333
|
async function readDarwin() {
|
|
2166
|
-
const tmp = path3.join(
|
|
2334
|
+
const tmp = path3.join(os9.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
|
|
2167
2335
|
const script = [
|
|
2168
2336
|
`set d to the clipboard as \xABclass PNGf\xBB`,
|
|
2169
2337
|
`set f to open for access POSIX file "${tmp}" with write permission`,
|
|
@@ -2200,7 +2368,7 @@ async function readLinux() {
|
|
|
2200
2368
|
return null;
|
|
2201
2369
|
}
|
|
2202
2370
|
async function readWindows() {
|
|
2203
|
-
const tmp = path3.join(
|
|
2371
|
+
const tmp = path3.join(os9.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
|
|
2204
2372
|
const ps = [
|
|
2205
2373
|
"Add-Type -AssemblyName System.Windows.Forms;",
|
|
2206
2374
|
"$img = [System.Windows.Forms.Clipboard]::GetImage();",
|
|
@@ -2274,6 +2442,13 @@ function buildBoxBottom(boxW, borderColor, reset = "\x1B[0m") {
|
|
|
2274
2442
|
function buildBoxTopPlain(boxW, borderColor, reset = "\x1B[0m") {
|
|
2275
2443
|
return `${borderColor}\u256D${"\u2500".repeat(Math.max(1, boxW - 2))}\u256E${reset}`;
|
|
2276
2444
|
}
|
|
2445
|
+
function buildBoxTopLeftLabel(boxW, label, borderColor, labelColor = "\x1B[90m", reset = "\x1B[0m") {
|
|
2446
|
+
const plain = ` ${label} `;
|
|
2447
|
+
const budget = Math.max(1, boxW - 3);
|
|
2448
|
+
const lab = plain.length > budget ? plain.slice(0, Math.max(1, budget)) : plain;
|
|
2449
|
+
const fill = Math.max(1, boxW - 2 - lab.length);
|
|
2450
|
+
return `${borderColor}\u256D${reset}${labelColor}${lab}${reset}${borderColor}${"\u2500".repeat(fill)}\u256E${reset}`;
|
|
2451
|
+
}
|
|
2277
2452
|
function buildBoxBody(content, contentW, borderColor, reset = "\x1B[0m") {
|
|
2278
2453
|
const vw = boxVisibleWidth(content);
|
|
2279
2454
|
const pad = Math.max(0, contentW - vw);
|
|
@@ -2302,17 +2477,104 @@ function wrapInputBodyLines(inputBuffer, prefix, prefixWidth, contentW) {
|
|
|
2302
2477
|
if (out.length === 0) out.push(prefix);
|
|
2303
2478
|
return out;
|
|
2304
2479
|
}
|
|
2480
|
+
function borderHighlightGrey(intensity) {
|
|
2481
|
+
const t = Math.max(0, Math.min(1, intensity));
|
|
2482
|
+
const e = t * t;
|
|
2483
|
+
const lo = 88;
|
|
2484
|
+
const hi = 236;
|
|
2485
|
+
const v = Math.round(lo + (hi - lo) * e);
|
|
2486
|
+
const ct = process.env.COLORTERM ?? "";
|
|
2487
|
+
const tc = /truecolor|24bit/i.test(ct) || /iterm|kitty|wezterm|ghostty|alacritty/i.test(process.env.TERM_PROGRAM ?? "");
|
|
2488
|
+
if (tc) return `\x1B[38;2;${v};${v};${v}m`;
|
|
2489
|
+
return `\x1B[38;5;${232 + Math.round(23 * e)}m`;
|
|
2490
|
+
}
|
|
2491
|
+
function rotatingBorderCellColor(index, perimeter, phase, trailFrac = 0.2) {
|
|
2492
|
+
const P = Math.max(1, perimeter);
|
|
2493
|
+
const pos = index / P;
|
|
2494
|
+
const dBehind = ((phase - pos) % 1 + 1) % 1;
|
|
2495
|
+
if (dBehind <= trailFrac) {
|
|
2496
|
+
return borderHighlightGrey(1 - dBehind / trailFrac);
|
|
2497
|
+
}
|
|
2498
|
+
return inputBoxBorderColor();
|
|
2499
|
+
}
|
|
2500
|
+
function perimeterIndex(edge, offset, boxW, bodyH) {
|
|
2501
|
+
const W = boxW;
|
|
2502
|
+
const H = Math.max(1, bodyH);
|
|
2503
|
+
if (edge === "top") return Math.max(0, Math.min(W - 1, offset));
|
|
2504
|
+
if (edge === "right") return W + Math.max(0, Math.min(H - 1, offset));
|
|
2505
|
+
if (edge === "bottom") return W + H + Math.max(0, Math.min(W - 1, offset));
|
|
2506
|
+
return 2 * W + H + Math.max(0, Math.min(H - 1, offset));
|
|
2507
|
+
}
|
|
2305
2508
|
function buildInputBoxRows(opts) {
|
|
2306
2509
|
const { cols: cols2, inputBuffer, prefix, prefixWidth, level, levelColor } = opts;
|
|
2307
2510
|
const borderColor = opts.borderColor ?? inputBoxBorderColor();
|
|
2308
2511
|
const geo = inputBoxGeometry(cols2);
|
|
2309
2512
|
const body = wrapInputBodyLines(inputBuffer, prefix, prefixWidth, geo.contentW);
|
|
2310
|
-
const dots = borderLevelDots(level);
|
|
2311
|
-
const dotsStyled = borderLevelDotsStyled(level, levelColor);
|
|
2312
|
-
const top = buildBoxTop(geo.boxW, dots, borderColor, levelColor, "\x1B[0m", dotsStyled);
|
|
2313
|
-
const bottom = buildBoxBottom(geo.boxW, borderColor);
|
|
2314
2513
|
const pad = " ".repeat(geo.margin);
|
|
2315
|
-
|
|
2514
|
+
const phase = opts.borderPhase;
|
|
2515
|
+
if (phase == null || !Number.isFinite(phase)) {
|
|
2516
|
+
const dots2 = borderLevelDots(level);
|
|
2517
|
+
const dotsStyled2 = borderLevelDotsStyled(level, levelColor);
|
|
2518
|
+
const top2 = buildBoxTop(geo.boxW, dots2, borderColor, levelColor, "\x1B[0m", dotsStyled2);
|
|
2519
|
+
const bottom2 = buildBoxBottom(geo.boxW, borderColor);
|
|
2520
|
+
return [pad + top2, ...body.map((b) => pad + buildBoxBody(b, geo.contentW, borderColor)), pad + bottom2];
|
|
2521
|
+
}
|
|
2522
|
+
const W = geo.boxW;
|
|
2523
|
+
const bodyH = Math.max(1, body.length);
|
|
2524
|
+
const P = 2 * W + 2 * bodyH;
|
|
2525
|
+
const reset = "\x1B[0m";
|
|
2526
|
+
const colorAt = (i) => rotatingBorderCellColor(i, P, phase);
|
|
2527
|
+
const dots = borderLevelDots(level);
|
|
2528
|
+
borderLevelDotsStyled(level, levelColor);
|
|
2529
|
+
const labelPlain = ` ${dots} `;
|
|
2530
|
+
const rightFillN = 1;
|
|
2531
|
+
const budget = Math.max(1, W - 2 - rightFillN);
|
|
2532
|
+
const plain = labelPlain.length > budget ? labelPlain.slice(0, Math.max(1, budget)) : labelPlain;
|
|
2533
|
+
const leftFill = Math.max(1, W - 2 - plain.length - rightFillN);
|
|
2534
|
+
let top = "";
|
|
2535
|
+
let xi = 0;
|
|
2536
|
+
top += colorAt(perimeterIndex("top", xi++, W, bodyH)) + "\u256D" + reset;
|
|
2537
|
+
for (let k = 0; k < leftFill; k++) {
|
|
2538
|
+
top += colorAt(perimeterIndex("top", xi++, W, bodyH)) + "\u2500" + reset;
|
|
2539
|
+
}
|
|
2540
|
+
{
|
|
2541
|
+
const plainChars = [...plain];
|
|
2542
|
+
let di = 0;
|
|
2543
|
+
for (const ch of plainChars) {
|
|
2544
|
+
const c4 = colorAt(perimeterIndex("top", xi++, W, bodyH));
|
|
2545
|
+
if (ch === "\u25CF" || ch === "\u25CB") {
|
|
2546
|
+
const levelN = Math.max(1, Math.min(5, level));
|
|
2547
|
+
const filled = di < levelN;
|
|
2548
|
+
di++;
|
|
2549
|
+
top += (filled ? levelColor || c4 : "\x1B[38;5;240m") + (filled ? "\u25CF" : "\u25CB") + reset;
|
|
2550
|
+
} else {
|
|
2551
|
+
top += c4 + ch + reset;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
for (let k = 0; k < rightFillN; k++) {
|
|
2556
|
+
top += colorAt(perimeterIndex("top", xi++, W, bodyH)) + "\u2500" + reset;
|
|
2557
|
+
}
|
|
2558
|
+
top += colorAt(perimeterIndex("top", Math.min(xi, W - 1), W, bodyH)) + "\u256E" + reset;
|
|
2559
|
+
const bodyRows = [];
|
|
2560
|
+
for (let y = 0; y < bodyH; y++) {
|
|
2561
|
+
const leftC = colorAt(perimeterIndex("left", bodyH - 1 - y, W, bodyH));
|
|
2562
|
+
const rightC = colorAt(perimeterIndex("right", y, W, bodyH));
|
|
2563
|
+
const vw = boxVisibleWidth(body[y]);
|
|
2564
|
+
const sp = Math.max(0, geo.contentW - vw);
|
|
2565
|
+
bodyRows.push(
|
|
2566
|
+
`${pad}${leftC}\u2502${reset} ${body[y]}${" ".repeat(sp)} ${rightC}\u2502${reset}`
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
let bottom = "";
|
|
2570
|
+
for (let x = 0; x < W; x++) {
|
|
2571
|
+
const idx = perimeterIndex("bottom", W - 1 - x, W, bodyH);
|
|
2572
|
+
const c4 = colorAt(idx);
|
|
2573
|
+
if (x === 0) bottom += c4 + "\u2570" + reset;
|
|
2574
|
+
else if (x === W - 1) bottom += c4 + "\u256F" + reset;
|
|
2575
|
+
else bottom += c4 + "\u2500" + reset;
|
|
2576
|
+
}
|
|
2577
|
+
return [pad + top, ...bodyRows, pad + bottom];
|
|
2316
2578
|
}
|
|
2317
2579
|
var C = {
|
|
2318
2580
|
reset: "\x1B[0m",
|
|
@@ -2367,7 +2629,7 @@ var Tui = class _Tui {
|
|
|
2367
2629
|
process.stdin.resume();
|
|
2368
2630
|
process.stdout.write("\x1B[?2004h");
|
|
2369
2631
|
process.on("exit", () => process.stdout.write("\x1B[?2004l\x1B[?25h"));
|
|
2370
|
-
process.stdout.on("resize", () => this.
|
|
2632
|
+
process.stdout.on("resize", () => this.scheduleResizeRedraw());
|
|
2371
2633
|
}
|
|
2372
2634
|
level;
|
|
2373
2635
|
// input + indicators
|
|
@@ -2426,13 +2688,22 @@ var Tui = class _Tui {
|
|
|
2426
2688
|
connected = true;
|
|
2427
2689
|
bottomDrawn = false;
|
|
2428
2690
|
started = false;
|
|
2429
|
-
// Resize bookkeeping
|
|
2430
|
-
//
|
|
2431
|
-
//
|
|
2432
|
-
//
|
|
2433
|
-
//
|
|
2434
|
-
//
|
|
2691
|
+
// Resize bookkeeping. When the terminal width changes, previously drawn HUD
|
|
2692
|
+
// rows re-wrap (a full-width ruler becomes 2+ physical rows when narrowed),
|
|
2693
|
+
// so the caret-relative move-up from the last paint is stale. We store the
|
|
2694
|
+
// visible width of EVERY region row (not just above the body) plus the caret
|
|
2695
|
+
// row index so moveToRegionTop can recompute physical height under the new
|
|
2696
|
+
// wrap. Resize events are debounced — drag-resizing fires dozens of events
|
|
2697
|
+
// and redrawing each one desyncs and leaves ghost chrome.
|
|
2435
2698
|
lastDrawnCols = 0;
|
|
2699
|
+
/** Visible width of every HUD row from the last paint, top → bottom. */
|
|
2700
|
+
drawnRegionWidths = [];
|
|
2701
|
+
/** Index into drawnRegionWidths of the row the caret sat on last paint. */
|
|
2702
|
+
lastCaretRegionIndex = 0;
|
|
2703
|
+
resizeTimer = null;
|
|
2704
|
+
/** True between a resize event and the debounced repaint — spinner ticks no-op. */
|
|
2705
|
+
resizePending = false;
|
|
2706
|
+
/** @deprecated kept as alias during paint — prefer drawnRegionWidths */
|
|
2436
2707
|
drawnHudWidths = [];
|
|
2437
2708
|
// takeover (approval / menu) state
|
|
2438
2709
|
takeoverHandler = null;
|
|
@@ -2471,6 +2742,23 @@ var Tui = class _Tui {
|
|
|
2471
2742
|
};
|
|
2472
2743
|
onQuit = () => process.exit(0);
|
|
2473
2744
|
levelListeners = [];
|
|
2745
|
+
/**
|
|
2746
|
+
* Terminal resize fires continuously while the user drags. Redrawing on every
|
|
2747
|
+
* event desyncs the region-height math (each paint uses a half-rewrapped
|
|
2748
|
+
* intermediate width) and stamps ghost boxes into the scrollback. Wait for
|
|
2749
|
+
* the size to settle (~1–2 frames), then do one clean clear+repaint. While
|
|
2750
|
+
* pending, spinner ticks skip paint so they don't thrash mid-drag.
|
|
2751
|
+
*/
|
|
2752
|
+
scheduleResizeRedraw() {
|
|
2753
|
+
this.resizePending = true;
|
|
2754
|
+
if (this.resizeTimer) clearTimeout(this.resizeTimer);
|
|
2755
|
+
this.resizeTimer = setTimeout(() => {
|
|
2756
|
+
this.resizeTimer = null;
|
|
2757
|
+
this.resizePending = false;
|
|
2758
|
+
if (!this.started || this.takeoverHandler) return;
|
|
2759
|
+
this.renderBottom();
|
|
2760
|
+
}, 40);
|
|
2761
|
+
}
|
|
2474
2762
|
get colors() {
|
|
2475
2763
|
return C;
|
|
2476
2764
|
}
|
|
@@ -2512,6 +2800,10 @@ var Tui = class _Tui {
|
|
|
2512
2800
|
clearTimeout(this.streamRedrawTimer);
|
|
2513
2801
|
this.streamRedrawTimer = null;
|
|
2514
2802
|
}
|
|
2803
|
+
if (this.resizeTimer) {
|
|
2804
|
+
clearTimeout(this.resizeTimer);
|
|
2805
|
+
this.resizeTimer = null;
|
|
2806
|
+
}
|
|
2515
2807
|
if (this.spinnerTimer) {
|
|
2516
2808
|
clearInterval(this.spinnerTimer);
|
|
2517
2809
|
this.spinnerTimer = null;
|
|
@@ -2962,7 +3254,7 @@ var Tui = class _Tui {
|
|
|
2962
3254
|
const q = this.inputBuffer.slice(1).trim().toLowerCase();
|
|
2963
3255
|
if (q === "") return this.commands;
|
|
2964
3256
|
return this.commands.filter(
|
|
2965
|
-
(
|
|
3257
|
+
(c4) => c4.name.startsWith(q) || c4.name.includes(q) || c4.label.toLowerCase().includes(q)
|
|
2966
3258
|
);
|
|
2967
3259
|
}
|
|
2968
3260
|
runCommand(cmd) {
|
|
@@ -3109,25 +3401,38 @@ var Tui = class _Tui {
|
|
|
3109
3401
|
return line;
|
|
3110
3402
|
});
|
|
3111
3403
|
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Physical rows a previously painted line of visible width `w` occupies after
|
|
3406
|
+
* the terminal rewraps it to `cols`. Hard-written HUD lines reflow this way.
|
|
3407
|
+
*/
|
|
3408
|
+
rewrapRows(w, cols2) {
|
|
3409
|
+
return Math.max(1, Math.ceil(Math.max(w, 1) / Math.max(1, cols2)));
|
|
3410
|
+
}
|
|
3112
3411
|
/**
|
|
3113
3412
|
* Move the cursor to the top-left of the current bottom region.
|
|
3114
3413
|
*
|
|
3115
3414
|
* Same width as the last draw → the caret's recorded row offset is exact.
|
|
3116
3415
|
* Width CHANGED (terminal resized) → previously drawn rows re-wrapped, so
|
|
3117
|
-
* that offset is stale
|
|
3118
|
-
*
|
|
3119
|
-
*
|
|
3120
|
-
* position in the input text, which inputLayout locates at the new width).
|
|
3416
|
+
* that offset is stale. Recompute using every stored region row width under
|
|
3417
|
+
* the NEW wrap: rows above the caret row + (caret row's rewrap − 1) so we
|
|
3418
|
+
* prefer a slight over-move (clean wipe) over under-move (ghost chrome).
|
|
3121
3419
|
*/
|
|
3122
3420
|
moveToRegionTop() {
|
|
3123
3421
|
process.stdout.write("\r");
|
|
3124
3422
|
if (!this.bottomDrawn) return;
|
|
3125
3423
|
const cols2 = process.stdout.columns || 80;
|
|
3126
3424
|
let up;
|
|
3127
|
-
if (cols2 !== this.lastDrawnCols && this.lastDrawnCols > 0) {
|
|
3425
|
+
if (cols2 !== this.lastDrawnCols && this.lastDrawnCols > 0 && this.drawnRegionWidths.length) {
|
|
3426
|
+
const widths = this.drawnRegionWidths.length > 0 ? this.drawnRegionWidths : this.drawnHudWidths;
|
|
3427
|
+
const caretIdx = Math.max(
|
|
3428
|
+
0,
|
|
3429
|
+
Math.min(this.lastCaretRegionIndex, Math.max(0, widths.length - 1))
|
|
3430
|
+
);
|
|
3128
3431
|
let above = 0;
|
|
3129
|
-
for (
|
|
3130
|
-
|
|
3432
|
+
for (let i = 0; i < caretIdx; i++) above += this.rewrapRows(widths[i], cols2);
|
|
3433
|
+
const caretPhysical = this.rewrapRows(widths[caretIdx] ?? 1, cols2);
|
|
3434
|
+
up = above + Math.max(0, caretPhysical - 1);
|
|
3435
|
+
up += 1;
|
|
3131
3436
|
} else {
|
|
3132
3437
|
up = this.lastCursorRow;
|
|
3133
3438
|
}
|
|
@@ -3145,6 +3450,7 @@ var Tui = class _Tui {
|
|
|
3145
3450
|
*/
|
|
3146
3451
|
renderBottom() {
|
|
3147
3452
|
if (!this.started || this.takeoverHandler) return;
|
|
3453
|
+
if (this.resizePending) return;
|
|
3148
3454
|
const cols2 = process.stdout.columns || 80;
|
|
3149
3455
|
this.moveToRegionTop();
|
|
3150
3456
|
const hudWidths = [];
|
|
@@ -3181,6 +3487,8 @@ var Tui = class _Tui {
|
|
|
3181
3487
|
for (const line of goalLines) writeHudRow(workPad + line);
|
|
3182
3488
|
const prefix = this.promptPrefix();
|
|
3183
3489
|
const pw = this.visibleWidth(prefix);
|
|
3490
|
+
const borderAnimating = this.working;
|
|
3491
|
+
const borderPhase = borderAnimating ? Date.now() % 1400 / 1400 : null;
|
|
3184
3492
|
const boxRows = buildInputBoxRows({
|
|
3185
3493
|
cols: cols2,
|
|
3186
3494
|
inputBuffer: this.inputBuffer,
|
|
@@ -3188,14 +3496,14 @@ var Tui = class _Tui {
|
|
|
3188
3496
|
prefixWidth: pw,
|
|
3189
3497
|
level: this.level,
|
|
3190
3498
|
levelColor: this.levelColor(),
|
|
3191
|
-
borderColor: inputBoxBorderColor()
|
|
3499
|
+
borderColor: inputBoxBorderColor(),
|
|
3500
|
+
borderPhase
|
|
3192
3501
|
});
|
|
3193
3502
|
const boxTop = boxRows[0];
|
|
3194
3503
|
const boxBottom = boxRows[boxRows.length - 1];
|
|
3195
3504
|
const boxBody = boxRows.slice(1, -1);
|
|
3196
3505
|
writeHudRow(boxTop);
|
|
3197
|
-
const
|
|
3198
|
-
const aboveBodyRows = aboveBodyWidths.length;
|
|
3506
|
+
const bodyStartIdx = hudWidths.length;
|
|
3199
3507
|
for (const row of boxBody) writeHudRow(row);
|
|
3200
3508
|
writeHudRow(boxBottom);
|
|
3201
3509
|
const paletteBlock = this.paletteBlockLines(cols2);
|
|
@@ -3203,10 +3511,12 @@ var Tui = class _Tui {
|
|
|
3203
3511
|
const layout = this.inputLayout();
|
|
3204
3512
|
const bodyRowCount = Math.max(1, boxBody.length);
|
|
3205
3513
|
const caretBodyRow = Math.max(0, Math.min(layout.caretRow, bodyRowCount - 1));
|
|
3206
|
-
const caretRegionRow =
|
|
3514
|
+
const caretRegionRow = bodyStartIdx + caretBodyRow;
|
|
3207
3515
|
const caretScreenCol = Math.min(rowCap - 1, geo.leftPad + layout.caretCol);
|
|
3208
3516
|
this.lastCursorRow = caretRegionRow;
|
|
3209
|
-
this.
|
|
3517
|
+
this.lastCaretRegionIndex = caretRegionRow;
|
|
3518
|
+
this.drawnRegionWidths = hudWidths.slice();
|
|
3519
|
+
this.drawnHudWidths = hudWidths.slice(0, bodyStartIdx);
|
|
3210
3520
|
this.lastDrawnCols = cols2;
|
|
3211
3521
|
this.bottomDrawn = true;
|
|
3212
3522
|
const totalRows = hudRows.length;
|
|
@@ -3563,70 +3873,249 @@ var Tui = class _Tui {
|
|
|
3563
3873
|
}
|
|
3564
3874
|
this.renderBottom();
|
|
3565
3875
|
}
|
|
3566
|
-
/**
|
|
3567
|
-
*
|
|
3876
|
+
/**
|
|
3877
|
+
* ## Boxed prompt — reusable HUD free-text component
|
|
3878
|
+
*
|
|
3879
|
+
* Single-line free-text capture using the **same chrome as the main input
|
|
3880
|
+
* box** (side margin, grey rounded rim, level-tinted `❯`, real caret). A
|
|
3881
|
+
* left-aligned label sits in the top border rim instead of the permission
|
|
3882
|
+
* level dots.
|
|
3883
|
+
*
|
|
3884
|
+
* Prefer this over {@link Tui.prompt} whenever the capture should feel like
|
|
3885
|
+
* part of the HUD rather than a legacy bar — denial feedback, handoff notes,
|
|
3886
|
+
* reject-with-reason, rename, or any other short free-text moment.
|
|
3887
|
+
*
|
|
3888
|
+
* ```
|
|
3889
|
+
* ╭ Denial feedback ────────────────────────╮
|
|
3890
|
+
* │ ❯ the user-typed note │
|
|
3891
|
+
* ╰──────────────────────────────────────────╯
|
|
3892
|
+
* ```
|
|
3893
|
+
*
|
|
3894
|
+
* **Keys**
|
|
3895
|
+
* - Enter — submit (resolves the trimmed string; may be `""`)
|
|
3896
|
+
* - Esc — clear a non-empty draft; Esc again (empty) cancels → `null`
|
|
3897
|
+
* (override with `escClearsFirst: false` to always cancel)
|
|
3898
|
+
*
|
|
3899
|
+
* **Nested mode** (`nested: true`) — call from inside another takeover
|
|
3900
|
+
* (e.g. approval → feedback) so this component only steals the key handler
|
|
3901
|
+
* and paints/erases its own three rows; the parent still owns begin/end.
|
|
3902
|
+
*
|
|
3903
|
+
* Do **not** use brand-gradient text for the label — gradient is reserved
|
|
3904
|
+
* for success moments (`✓ Goal complete.`, farewell, etc.).
|
|
3905
|
+
*
|
|
3906
|
+
* @returns trimmed text on Enter, or `null` if the user cancelled with Esc
|
|
3907
|
+
*/
|
|
3908
|
+
boxedPrompt(opts) {
|
|
3909
|
+
return new Promise((resolve) => {
|
|
3910
|
+
const nested = !!opts.nested;
|
|
3911
|
+
const escClearsFirst = opts.escClearsFirst !== false;
|
|
3912
|
+
const borderLabel = opts.borderLabel || "Feedback";
|
|
3913
|
+
const labelColor = opts.labelColor ?? "\x1B[90m";
|
|
3914
|
+
let buf = opts.initial ?? "";
|
|
3915
|
+
if (!nested) this.beginTakeover();
|
|
3916
|
+
process.stdout.write("\x1B[?25h");
|
|
3917
|
+
const cols2 = process.stdout.columns || 80;
|
|
3918
|
+
const geo = inputBoxGeometry(cols2);
|
|
3919
|
+
const border = inputBoxBorderColor();
|
|
3920
|
+
const pad = " ".repeat(geo.margin);
|
|
3921
|
+
const rowCap = Math.max(1, cols2 - 1);
|
|
3922
|
+
const reset = C.reset;
|
|
3923
|
+
const prompt = `${this.levelColor()}\u276F${reset} `;
|
|
3924
|
+
const promptW = 2;
|
|
3925
|
+
const writeRow = (line) => {
|
|
3926
|
+
const row = this.clampVisible(sanitizeHudRow(line), rowCap);
|
|
3927
|
+
process.stdout.write(`\r\x1B[K${row}
|
|
3928
|
+
`);
|
|
3929
|
+
};
|
|
3930
|
+
const draw = () => {
|
|
3931
|
+
writeRow(pad + buildBoxTopLeftLabel(geo.boxW, borderLabel, border, labelColor));
|
|
3932
|
+
writeRow(pad + buildBoxBody(prompt + buf, geo.contentW, border));
|
|
3933
|
+
writeRow(pad + buildBoxBottom(geo.boxW, border));
|
|
3934
|
+
process.stdout.write("\r\x1B[2A");
|
|
3935
|
+
const col = geo.leftPad + promptW + buf.length;
|
|
3936
|
+
if (col > 0) process.stdout.write(`\x1B[${Math.min(rowCap - 1, col)}C`);
|
|
3937
|
+
};
|
|
3938
|
+
const clearFrame = () => {
|
|
3939
|
+
process.stdout.write("\r\x1B[1A\x1B[J");
|
|
3940
|
+
};
|
|
3941
|
+
const redraw = () => {
|
|
3942
|
+
process.stdout.write("\r\x1B[1A");
|
|
3943
|
+
draw();
|
|
3944
|
+
};
|
|
3945
|
+
const finish = (value) => {
|
|
3946
|
+
clearFrame();
|
|
3947
|
+
if (!nested) this.endTakeover();
|
|
3948
|
+
else this.takeoverHandler = null;
|
|
3949
|
+
resolve(value);
|
|
3950
|
+
};
|
|
3951
|
+
draw();
|
|
3952
|
+
this.takeoverHandler = (str, key) => {
|
|
3953
|
+
if (key?.name === "escape") {
|
|
3954
|
+
if (escClearsFirst && buf.length > 0) {
|
|
3955
|
+
buf = "";
|
|
3956
|
+
redraw();
|
|
3957
|
+
return;
|
|
3958
|
+
}
|
|
3959
|
+
finish(null);
|
|
3960
|
+
return;
|
|
3961
|
+
}
|
|
3962
|
+
if (key?.name === "return" || key?.name === "enter") {
|
|
3963
|
+
finish(buf.trim());
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
if (key?.name === "backspace") {
|
|
3967
|
+
if (!buf.length) return;
|
|
3968
|
+
buf = buf.slice(0, -1);
|
|
3969
|
+
redraw();
|
|
3970
|
+
return;
|
|
3971
|
+
}
|
|
3972
|
+
if (str && !key?.ctrl && !key?.meta && str >= " ") {
|
|
3973
|
+
if (buf.length < Math.max(8, geo.contentW - promptW - 1)) {
|
|
3974
|
+
buf += str;
|
|
3975
|
+
redraw();
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
};
|
|
3979
|
+
});
|
|
3980
|
+
}
|
|
3981
|
+
/**
|
|
3982
|
+
* Permission prompt: boxed chrome matching the HUD, arrow-navigable options
|
|
3983
|
+
* with shortcuts. Includes "Deny with feedback" — choosing it (or Tab)
|
|
3984
|
+
* opens {@link Tui.boxedPrompt} so the reason can ride back to the agent.
|
|
3985
|
+
*/
|
|
3568
3986
|
approval(question, risk) {
|
|
3569
3987
|
return new Promise((resolve) => {
|
|
3570
3988
|
const options = [
|
|
3571
3989
|
{ value: "allow", label: "Allow once", shortcut: "y", color: C.green },
|
|
3572
3990
|
{ value: "always", label: "Always allow this tool", shortcut: "a", color: C.cyan },
|
|
3573
|
-
{
|
|
3574
|
-
|
|
3991
|
+
{
|
|
3992
|
+
value: "always_risk",
|
|
3993
|
+
label: `Allow level ${risk} and below this session`,
|
|
3994
|
+
shortcut: "l",
|
|
3995
|
+
color: C.cyan
|
|
3996
|
+
},
|
|
3997
|
+
{ value: "deny", label: "Deny", shortcut: "n", color: C.red },
|
|
3998
|
+
{
|
|
3999
|
+
value: "deny_feedback",
|
|
4000
|
+
label: "Deny with feedback",
|
|
4001
|
+
shortcut: "d",
|
|
4002
|
+
color: C.magenta,
|
|
4003
|
+
hint: "tell the agent why"
|
|
4004
|
+
}
|
|
3575
4005
|
];
|
|
3576
4006
|
let idx = 0;
|
|
3577
|
-
const
|
|
4007
|
+
const riskLevel = Math.max(1, Math.min(5, Math.round(risk)));
|
|
4008
|
+
const riskBar = borderLevelDotsStyled(riskLevel, C.yellow, "\x1B[38;5;240m");
|
|
3578
4009
|
this.beginTakeover();
|
|
3579
|
-
const cols2 =
|
|
3580
|
-
const
|
|
3581
|
-
const
|
|
3582
|
-
const
|
|
3583
|
-
const
|
|
3584
|
-
const
|
|
3585
|
-
const
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
4010
|
+
const cols2 = process.stdout.columns || 80;
|
|
4011
|
+
const geo = inputBoxGeometry(cols2);
|
|
4012
|
+
const border = inputBoxBorderColor();
|
|
4013
|
+
const pad = " ".repeat(geo.margin);
|
|
4014
|
+
const rowCap = Math.max(1, cols2 - 1);
|
|
4015
|
+
const reset = C.reset;
|
|
4016
|
+
const wrapContent = (text) => {
|
|
4017
|
+
const flat2 = text.replace(/\s+/g, " ").trim();
|
|
4018
|
+
if (!flat2) return [""];
|
|
4019
|
+
const out = [];
|
|
4020
|
+
let rest = flat2;
|
|
4021
|
+
while (rest.length > geo.contentW) {
|
|
4022
|
+
out.push(rest.slice(0, geo.contentW - 1) + "\u2026");
|
|
4023
|
+
rest = rest.slice(geo.contentW - 1);
|
|
4024
|
+
if (out.length >= 4) {
|
|
4025
|
+
break;
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
if (out.length < 4) out.push(rest);
|
|
4029
|
+
return out.length ? out : [""];
|
|
4030
|
+
};
|
|
4031
|
+
const summaryLines = question.split("\n").flatMap((line) => {
|
|
4032
|
+
const plain = line.replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
4033
|
+
if (!plain) return [];
|
|
4034
|
+
const isWhy = /^why:/i.test(plain);
|
|
4035
|
+
return wrapContent(plain).map((l) => isWhy ? `${C.dim}${l}${reset}` : l);
|
|
4036
|
+
});
|
|
4037
|
+
if (!summaryLines.length) summaryLines.push(`${C.dim}(no details)${reset}`);
|
|
4038
|
+
const writeRow = (line) => {
|
|
4039
|
+
const row = this.clampVisible(sanitizeHudRow(line), rowCap);
|
|
4040
|
+
process.stdout.write(`\r\x1B[K${row}
|
|
4041
|
+
`);
|
|
4042
|
+
};
|
|
4043
|
+
const optionCount = options.length;
|
|
4044
|
+
const summaryCount = summaryLines.length;
|
|
4045
|
+
const boxRows = 1 + summaryCount + 1 + optionCount + 1;
|
|
4046
|
+
const titleRows = 2;
|
|
4047
|
+
const totalRows = titleRows + boxRows;
|
|
4048
|
+
const renderOptionContent = (i) => {
|
|
3590
4049
|
const o = options[i];
|
|
3591
4050
|
const sel = i === idx;
|
|
3592
|
-
const pointer = sel ? `${o.color}\u276F${
|
|
3593
|
-
const label = sel ? `${C.bold}${o.label}${
|
|
3594
|
-
|
|
4051
|
+
const pointer = sel ? `${o.color}\u276F${reset} ` : " ";
|
|
4052
|
+
const label = sel ? `${C.bold}${o.color}${o.label}${reset}` : `${C.dim}${o.label}${reset}`;
|
|
4053
|
+
const shortcut = `${C.gray}(${o.shortcut})${reset}`;
|
|
4054
|
+
const hint = o.hint && sel ? `${C.dim} ${o.hint}${reset}` : "";
|
|
4055
|
+
const plainLabel = o.label;
|
|
4056
|
+
const used = 2 + plainLabel.length + 1 + 3 + (o.hint && sel ? 2 + o.hint.length : 0);
|
|
4057
|
+
const gap = Math.max(1, geo.contentW - used);
|
|
4058
|
+
return `${pointer}${label}${" ".repeat(gap)}${shortcut}${hint}`;
|
|
3595
4059
|
};
|
|
3596
|
-
const
|
|
3597
|
-
if (moveUp) process.stdout.write(`\x1B[${
|
|
3598
|
-
|
|
3599
|
-
|
|
4060
|
+
const drawFrame = (moveUp) => {
|
|
4061
|
+
if (moveUp) process.stdout.write(`\x1B[${totalRows}A`);
|
|
4062
|
+
writeRow("");
|
|
4063
|
+
writeRow(
|
|
4064
|
+
`${pad}${C.bold}Permission needed${reset} ${C.dim}risk${reset} ${riskBar}`
|
|
4065
|
+
);
|
|
4066
|
+
writeRow(pad + buildBoxTopPlain(geo.boxW, border));
|
|
4067
|
+
for (const line of summaryLines) {
|
|
4068
|
+
writeRow(pad + buildBoxBody(line, geo.contentW, border));
|
|
4069
|
+
}
|
|
4070
|
+
writeRow(pad + buildBoxBody("", geo.contentW, border));
|
|
4071
|
+
for (let i = 0; i < optionCount; i++) {
|
|
4072
|
+
writeRow(pad + buildBoxBody(renderOptionContent(i), geo.contentW, border));
|
|
4073
|
+
}
|
|
4074
|
+
writeRow(pad + buildBoxBottom(geo.boxW, border));
|
|
3600
4075
|
};
|
|
3601
|
-
|
|
4076
|
+
drawFrame(false);
|
|
3602
4077
|
const erase = () => {
|
|
3603
4078
|
process.stdout.write("\r");
|
|
3604
|
-
|
|
3605
|
-
if (up > 0) process.stdout.write(`\x1B[${up}A`);
|
|
4079
|
+
if (totalRows > 0) process.stdout.write(`\x1B[${totalRows}A`);
|
|
3606
4080
|
process.stdout.write("\x1B[J");
|
|
3607
4081
|
};
|
|
3608
|
-
const
|
|
4082
|
+
const done = (choice, reason) => {
|
|
3609
4083
|
erase();
|
|
3610
4084
|
this.endTakeover();
|
|
3611
|
-
resolve(choice);
|
|
4085
|
+
resolve({ choice });
|
|
4086
|
+
};
|
|
4087
|
+
const collectFeedback = () => {
|
|
4088
|
+
erase();
|
|
4089
|
+
void this.boxedPrompt({ borderLabel: "Denial feedback", nested: true }).then((why) => {
|
|
4090
|
+
this.endTakeover();
|
|
4091
|
+
if (why) resolve({ choice: "deny", reason: why });
|
|
4092
|
+
else resolve({ choice: "deny" });
|
|
4093
|
+
});
|
|
4094
|
+
};
|
|
4095
|
+
const pick = (value) => {
|
|
4096
|
+
if (value === "deny_feedback") collectFeedback();
|
|
4097
|
+
else done(value);
|
|
3612
4098
|
};
|
|
3613
4099
|
this.takeoverHandler = (str, key) => {
|
|
3614
4100
|
if (key?.name === "up" || str === "k") {
|
|
3615
|
-
idx = (idx - 1 +
|
|
3616
|
-
|
|
4101
|
+
idx = (idx - 1 + optionCount) % optionCount;
|
|
4102
|
+
drawFrame(true);
|
|
3617
4103
|
} else if (key?.name === "down" || str === "j") {
|
|
3618
|
-
idx = (idx + 1) %
|
|
3619
|
-
|
|
4104
|
+
idx = (idx + 1) % optionCount;
|
|
4105
|
+
drawFrame(true);
|
|
3620
4106
|
} else if (key?.name === "tab") {
|
|
3621
|
-
|
|
4107
|
+
pick("deny_feedback");
|
|
3622
4108
|
} else if (key?.name === "return" || key?.name === "enter") {
|
|
3623
|
-
|
|
4109
|
+
pick(options[idx].value);
|
|
4110
|
+
} else if (key?.name === "escape") {
|
|
4111
|
+
done("deny");
|
|
3624
4112
|
} else {
|
|
3625
4113
|
const k = (str || "").toLowerCase();
|
|
3626
|
-
if (k === "y")
|
|
3627
|
-
else if (k === "a")
|
|
3628
|
-
else if (k === "l")
|
|
3629
|
-
else if (k === "n"
|
|
4114
|
+
if (k === "y") pick("allow");
|
|
4115
|
+
else if (k === "a") pick("always");
|
|
4116
|
+
else if (k === "l") pick("always_risk");
|
|
4117
|
+
else if (k === "n") pick("deny");
|
|
4118
|
+
else if (k === "d" || k === "f") pick("deny_feedback");
|
|
3630
4119
|
}
|
|
3631
4120
|
};
|
|
3632
4121
|
});
|
|
@@ -3850,7 +4339,7 @@ function tableCells(row) {
|
|
|
3850
4339
|
let r = row.trim();
|
|
3851
4340
|
if (r.startsWith("|")) r = r.slice(1);
|
|
3852
4341
|
if (r.endsWith("|")) r = r.slice(0, -1);
|
|
3853
|
-
return r.split("|").map((
|
|
4342
|
+
return r.split("|").map((c4) => c4.trim());
|
|
3854
4343
|
}
|
|
3855
4344
|
var SEPARATOR = /^[\s|:-]+$/;
|
|
3856
4345
|
function isTableSeparator(line) {
|
|
@@ -3859,17 +4348,17 @@ function isTableSeparator(line) {
|
|
|
3859
4348
|
function renderTable(rows) {
|
|
3860
4349
|
const cols2 = Math.max(...rows.map((r) => r.length));
|
|
3861
4350
|
const widths = [];
|
|
3862
|
-
for (let
|
|
3863
|
-
widths[
|
|
4351
|
+
for (let c4 = 0; c4 < cols2; c4++) {
|
|
4352
|
+
widths[c4] = Math.max(...rows.map((r) => visibleWidth(inline(r[c4] ?? ""))));
|
|
3864
4353
|
}
|
|
3865
4354
|
const sep = `${GRAY} \u2502 ${R}`;
|
|
3866
4355
|
const out = [];
|
|
3867
4356
|
rows.forEach((r, ri) => {
|
|
3868
4357
|
const cells = [];
|
|
3869
|
-
for (let
|
|
3870
|
-
const raw = r[
|
|
4358
|
+
for (let c4 = 0; c4 < cols2; c4++) {
|
|
4359
|
+
const raw = r[c4] ?? "";
|
|
3871
4360
|
const styled = ri === 0 ? `${BOLD2}${inline(raw)}${R}` : inline(raw);
|
|
3872
|
-
cells.push(padEndVisible(styled, widths[
|
|
4361
|
+
cells.push(padEndVisible(styled, widths[c4]));
|
|
3873
4362
|
}
|
|
3874
4363
|
out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
|
|
3875
4364
|
if (ri === 0) {
|
|
@@ -4189,7 +4678,7 @@ var McpManager = class {
|
|
|
4189
4678
|
}
|
|
4190
4679
|
}
|
|
4191
4680
|
closeAll() {
|
|
4192
|
-
for (const [,
|
|
4681
|
+
for (const [, c4] of this.clients) c4.close();
|
|
4193
4682
|
this.clients.clear();
|
|
4194
4683
|
}
|
|
4195
4684
|
get(name) {
|
|
@@ -4200,13 +4689,13 @@ var McpManager = class {
|
|
|
4200
4689
|
}
|
|
4201
4690
|
toolCount() {
|
|
4202
4691
|
let n = 0;
|
|
4203
|
-
for (const [,
|
|
4692
|
+
for (const [, c4] of this.clients) n += c4.tools.length;
|
|
4204
4693
|
return n;
|
|
4205
4694
|
}
|
|
4206
4695
|
/** A JSON-serializable catalog of every connected server for the KV/context. */
|
|
4207
4696
|
catalog() {
|
|
4208
4697
|
return {
|
|
4209
|
-
servers: Array.from(this.clients.values()).map((
|
|
4698
|
+
servers: Array.from(this.clients.values()).map((c4) => c4.catalogEntry()),
|
|
4210
4699
|
generatedAt: Date.now()
|
|
4211
4700
|
};
|
|
4212
4701
|
}
|
|
@@ -4296,9 +4785,9 @@ function flattenContent(content, structured) {
|
|
|
4296
4785
|
}
|
|
4297
4786
|
function flattenResourceContents(contents) {
|
|
4298
4787
|
const parts = [];
|
|
4299
|
-
for (const
|
|
4300
|
-
if (typeof
|
|
4301
|
-
else if (typeof
|
|
4788
|
+
for (const c4 of contents || []) {
|
|
4789
|
+
if (typeof c4.text === "string") parts.push(c4.text);
|
|
4790
|
+
else if (typeof c4.blob === "string") parts.push(`[binary resource ${String(c4.uri ?? "")} (${c4.blob.length} b64 chars)]`);
|
|
4302
4791
|
}
|
|
4303
4792
|
return parts.join("\n").trim();
|
|
4304
4793
|
}
|
|
@@ -4320,10 +4809,10 @@ function sortKeys(value) {
|
|
|
4320
4809
|
}
|
|
4321
4810
|
return value;
|
|
4322
4811
|
}
|
|
4323
|
-
function sha256(
|
|
4324
|
-
return crypto.createHash("sha256").update(
|
|
4812
|
+
function sha256(input3) {
|
|
4813
|
+
return crypto.createHash("sha256").update(input3).digest("hex");
|
|
4325
4814
|
}
|
|
4326
|
-
var DIR = path3.join(
|
|
4815
|
+
var DIR = path3.join(os9.homedir(), ".standardagents");
|
|
4327
4816
|
var FILE = path3.join(DIR, "credentials");
|
|
4328
4817
|
function normalizeEndpoint(endpoint) {
|
|
4329
4818
|
let e = endpoint.trim();
|
|
@@ -4381,49 +4870,319 @@ function saveDefaultEndpoint(endpoint) {
|
|
|
4381
4870
|
} catch {
|
|
4382
4871
|
}
|
|
4383
4872
|
}
|
|
4384
|
-
var
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4873
|
+
var c = {
|
|
4874
|
+
reset: "\x1B[0m",
|
|
4875
|
+
dim: "\x1B[2m",
|
|
4876
|
+
teal: "\x1B[38;5;37m"
|
|
4877
|
+
};
|
|
4878
|
+
async function deviceLogin(endpoint) {
|
|
4879
|
+
const start = await fetch(`${endpoint}/api/auth/device/start`, { method: "POST" });
|
|
4880
|
+
if (!start.ok) throw new Error(`This instance does not support browser sign-in (HTTP ${start.status}). Paste an API token instead.`);
|
|
4881
|
+
const info = await start.json();
|
|
4882
|
+
stdout.write(`${c.dim}Opening your browser to sign in. If it doesn't open, visit:${c.reset}
|
|
4883
|
+
`);
|
|
4884
|
+
stdout.write(`
|
|
4885
|
+
${c.teal}${info.verify_url}${c.reset}
|
|
4886
|
+
|
|
4887
|
+
`);
|
|
4888
|
+
stdout.write(`${c.dim}Waiting for sign-in to complete\u2026 (Ctrl-C to cancel)${c.reset}
|
|
4889
|
+
`);
|
|
4890
|
+
openUrl(info.verify_url);
|
|
4891
|
+
const deadline = Date.now() + (info.expires_in ?? 600) * 1e3;
|
|
4892
|
+
const interval = Math.max(2, info.interval ?? 2) * 1e3;
|
|
4893
|
+
while (Date.now() < deadline) {
|
|
4894
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
4895
|
+
const res = await fetch(info.poll_url).catch(() => null);
|
|
4896
|
+
if (!res) continue;
|
|
4897
|
+
if (res.status === 404) throw new Error("The sign-in link expired. Try again.");
|
|
4898
|
+
const body = await res.json().catch(() => ({}));
|
|
4899
|
+
if (body.status === "approved" && body.token) return body.token;
|
|
4900
|
+
if (body.status === "denied") throw new Error("Sign-in was denied in the browser.");
|
|
4901
|
+
}
|
|
4902
|
+
throw new Error("Timed out waiting for browser approval. Try again.");
|
|
4397
4903
|
}
|
|
4398
|
-
function
|
|
4904
|
+
function openUrl(url) {
|
|
4905
|
+
const platform = process.platform;
|
|
4906
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
4907
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
4399
4908
|
try {
|
|
4400
|
-
const
|
|
4401
|
-
|
|
4909
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
4910
|
+
child.unref();
|
|
4402
4911
|
} catch {
|
|
4403
|
-
return null;
|
|
4404
4912
|
}
|
|
4405
4913
|
}
|
|
4406
|
-
|
|
4914
|
+
var AGENT_ID = "standard_code_agent";
|
|
4915
|
+
var AGENT_ID_VARIANTS = [
|
|
4916
|
+
AGENT_ID,
|
|
4917
|
+
"standard_code_low_agent",
|
|
4918
|
+
"standard_code_high_agent"
|
|
4919
|
+
];
|
|
4920
|
+
var PRODUCTION_ENDPOINT = "https://api.standardcode.ai";
|
|
4921
|
+
function readVersion() {
|
|
4407
4922
|
try {
|
|
4408
|
-
const
|
|
4409
|
-
|
|
4410
|
-
fs4.writeFileSync(cachePath(), JSON.stringify({ latest, timestamp: Date.now() }));
|
|
4923
|
+
const pkg = JSON.parse(fs4.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
4924
|
+
return typeof pkg.version === "string" ? pkg.version : "";
|
|
4411
4925
|
} catch {
|
|
4926
|
+
return "";
|
|
4412
4927
|
}
|
|
4413
4928
|
}
|
|
4414
|
-
function
|
|
4929
|
+
function isLocalHost(host) {
|
|
4930
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.endsWith(".localhost") || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
|
|
4931
|
+
}
|
|
4932
|
+
function relaxTlsForLocalEndpoint(endpoint) {
|
|
4933
|
+
let host = "";
|
|
4415
4934
|
try {
|
|
4416
|
-
|
|
4417
|
-
const state = JSON.parse(raw);
|
|
4418
|
-
return typeof state?.version === "string" ? state : null;
|
|
4935
|
+
host = new URL(endpoint).hostname;
|
|
4419
4936
|
} catch {
|
|
4420
|
-
return
|
|
4937
|
+
return false;
|
|
4421
4938
|
}
|
|
4939
|
+
if (!endpoint.startsWith("https:") || !isLocalHost(host)) return false;
|
|
4940
|
+
const origEmit = process.emitWarning.bind(process);
|
|
4941
|
+
process.emitWarning = ((warning, ...args) => {
|
|
4942
|
+
const msg = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
4943
|
+
if (msg.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
|
|
4944
|
+
return origEmit(warning, ...args);
|
|
4945
|
+
});
|
|
4946
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
4947
|
+
return true;
|
|
4422
4948
|
}
|
|
4423
|
-
|
|
4949
|
+
var DIR2 = path3.join(os9.homedir(), ".standardagents");
|
|
4950
|
+
var FILE2 = path3.join(DIR2, "machine.json");
|
|
4951
|
+
function loadMachineIdentity() {
|
|
4424
4952
|
try {
|
|
4425
|
-
|
|
4426
|
-
|
|
4953
|
+
const parsed = JSON.parse(fs4.readFileSync(FILE2, "utf8"));
|
|
4954
|
+
if (typeof parsed.machine_id === "string" && parsed.machine_id.length > 0) {
|
|
4955
|
+
return parsed;
|
|
4956
|
+
}
|
|
4957
|
+
} catch {
|
|
4958
|
+
}
|
|
4959
|
+
const identity = {
|
|
4960
|
+
machine_id: crypto.randomUUID(),
|
|
4961
|
+
created_at: Date.now()
|
|
4962
|
+
};
|
|
4963
|
+
saveMachineIdentity(identity);
|
|
4964
|
+
return identity;
|
|
4965
|
+
}
|
|
4966
|
+
function saveMachineIdentity(identity) {
|
|
4967
|
+
fs4.mkdirSync(DIR2, { recursive: true });
|
|
4968
|
+
fs4.writeFileSync(FILE2, JSON.stringify(identity, null, 2), { mode: 384 });
|
|
4969
|
+
}
|
|
4970
|
+
function setMachineName(name) {
|
|
4971
|
+
const identity = loadMachineIdentity();
|
|
4972
|
+
identity.name = name.trim() || void 0;
|
|
4973
|
+
saveMachineIdentity(identity);
|
|
4974
|
+
return identity;
|
|
4975
|
+
}
|
|
4976
|
+
function machineDisplayName(identity) {
|
|
4977
|
+
return identity.name?.trim() || os9.hostname();
|
|
4978
|
+
}
|
|
4979
|
+
function daemonClientId(identity) {
|
|
4980
|
+
return `daemon:${identity.machine_id}`;
|
|
4981
|
+
}
|
|
4982
|
+
function interactiveClientId(identity) {
|
|
4983
|
+
return `cli:${identity.machine_id}:${crypto.randomBytes(4).toString("hex")}`;
|
|
4984
|
+
}
|
|
4985
|
+
function machineIdFromDaemonClientId(clientId) {
|
|
4986
|
+
if (typeof clientId !== "string") return null;
|
|
4987
|
+
return clientId.startsWith("daemon:") ? clientId.slice("daemon:".length) : null;
|
|
4988
|
+
}
|
|
4989
|
+
var KEY_PREFIX = "standardcode.machine.";
|
|
4990
|
+
var DAEMON_ONLINE_WINDOW_MS = 90 * 1e3;
|
|
4991
|
+
function machineKey(machineId) {
|
|
4992
|
+
return `${KEY_PREFIX}${machineId}`;
|
|
4993
|
+
}
|
|
4994
|
+
function parseMachineRecord(value) {
|
|
4995
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4996
|
+
const r = value;
|
|
4997
|
+
if (typeof r.id !== "string" || r.id.length === 0) return null;
|
|
4998
|
+
return {
|
|
4999
|
+
id: r.id,
|
|
5000
|
+
name: typeof r.name === "string" && r.name ? r.name : String(r.hostname ?? r.id),
|
|
5001
|
+
hostname: typeof r.hostname === "string" ? r.hostname : "",
|
|
5002
|
+
platform: typeof r.platform === "string" ? r.platform : "",
|
|
5003
|
+
arch: typeof r.arch === "string" ? r.arch : "",
|
|
5004
|
+
daemon: r.daemon && typeof r.daemon === "object" && !Array.isArray(r.daemon) ? r.daemon : null,
|
|
5005
|
+
projects: r.projects && typeof r.projects === "object" && !Array.isArray(r.projects) ? r.projects : {},
|
|
5006
|
+
created_at: typeof r.created_at === "number" ? r.created_at : 0,
|
|
5007
|
+
updated_at: typeof r.updated_at === "number" ? r.updated_at : 0
|
|
5008
|
+
};
|
|
5009
|
+
}
|
|
5010
|
+
async function loadMachines(api) {
|
|
5011
|
+
const entries = await api.userKvList(KEY_PREFIX);
|
|
5012
|
+
return entries.map((e) => parseMachineRecord(e.value)).filter((m) => m !== null);
|
|
5013
|
+
}
|
|
5014
|
+
async function loadMachine(api, machineId) {
|
|
5015
|
+
return parseMachineRecord(await api.userKvGet(machineKey(machineId)));
|
|
5016
|
+
}
|
|
5017
|
+
function daemonOnline(record, now = Date.now()) {
|
|
5018
|
+
return !!record.daemon && now - record.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
|
|
5019
|
+
}
|
|
5020
|
+
function newRecord(identity) {
|
|
5021
|
+
const now = Date.now();
|
|
5022
|
+
return {
|
|
5023
|
+
id: identity.machine_id,
|
|
5024
|
+
name: machineDisplayName(identity),
|
|
5025
|
+
hostname: os9.hostname(),
|
|
5026
|
+
platform: process.platform,
|
|
5027
|
+
arch: process.arch,
|
|
5028
|
+
daemon: null,
|
|
5029
|
+
projects: {},
|
|
5030
|
+
created_at: now,
|
|
5031
|
+
updated_at: now
|
|
5032
|
+
};
|
|
5033
|
+
}
|
|
5034
|
+
async function updateOwnMachineRecord(api, identity, mutate) {
|
|
5035
|
+
const existing = await loadMachine(api, identity.machine_id);
|
|
5036
|
+
const record = existing ?? newRecord(identity);
|
|
5037
|
+
record.name = machineDisplayName(identity);
|
|
5038
|
+
record.hostname = os9.hostname();
|
|
5039
|
+
record.platform = process.platform;
|
|
5040
|
+
record.arch = process.arch;
|
|
5041
|
+
mutate?.(record);
|
|
5042
|
+
record.updated_at = Date.now();
|
|
5043
|
+
await api.userKvSet(machineKey(identity.machine_id), record);
|
|
5044
|
+
return record;
|
|
5045
|
+
}
|
|
5046
|
+
function projectRepository(projectDir) {
|
|
5047
|
+
try {
|
|
5048
|
+
const url = execFileSync("git", ["-C", projectDir, "remote", "get-url", "origin"], {
|
|
5049
|
+
encoding: "utf8",
|
|
5050
|
+
timeout: 3e3,
|
|
5051
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5052
|
+
}).trim();
|
|
5053
|
+
return url || null;
|
|
5054
|
+
} catch {
|
|
5055
|
+
return null;
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
async function registerProject(api, identity, projectDir) {
|
|
5059
|
+
const repository = projectRepository(projectDir);
|
|
5060
|
+
await updateOwnMachineRecord(api, identity, (record) => {
|
|
5061
|
+
record.projects[projectDir] = {
|
|
5062
|
+
name: projectDir.split("/").filter(Boolean).pop() || projectDir,
|
|
5063
|
+
last_used_at: Date.now(),
|
|
5064
|
+
repository
|
|
5065
|
+
};
|
|
5066
|
+
});
|
|
5067
|
+
}
|
|
5068
|
+
async function unregisterProject(api, identity, projectDir) {
|
|
5069
|
+
await updateOwnMachineRecord(api, identity, (record) => {
|
|
5070
|
+
delete record.projects[projectDir];
|
|
5071
|
+
});
|
|
5072
|
+
}
|
|
5073
|
+
async function touchDaemon(api, identity, version) {
|
|
5074
|
+
await updateOwnMachineRecord(api, identity, (record) => {
|
|
5075
|
+
const now = Date.now();
|
|
5076
|
+
record.daemon = {
|
|
5077
|
+
version,
|
|
5078
|
+
installed_at: record.daemon?.installed_at ?? now,
|
|
5079
|
+
last_seen_at: now,
|
|
5080
|
+
pid: process.pid
|
|
5081
|
+
};
|
|
5082
|
+
});
|
|
5083
|
+
}
|
|
5084
|
+
async function clearDaemon(api, identity) {
|
|
5085
|
+
await updateOwnMachineRecord(api, identity, (record) => {
|
|
5086
|
+
record.daemon = null;
|
|
5087
|
+
});
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5090
|
+
// src/relay.ts
|
|
5091
|
+
var APPROVAL_REQUEST_KEY = "approval_request";
|
|
5092
|
+
var APPROVAL_RESPONSE_KEY = "approval_response";
|
|
5093
|
+
function parseApprovalRequest(value) {
|
|
5094
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
5095
|
+
const r = value;
|
|
5096
|
+
if (typeof r.tool_call_id !== "string" || typeof r.tool !== "string") return null;
|
|
5097
|
+
return {
|
|
5098
|
+
tool_call_id: r.tool_call_id,
|
|
5099
|
+
tool: r.tool,
|
|
5100
|
+
summary: typeof r.summary === "string" ? r.summary : r.tool,
|
|
5101
|
+
permission: typeof r.permission === "string" ? r.permission : null,
|
|
5102
|
+
risk: typeof r.risk === "number" ? r.risk : 3,
|
|
5103
|
+
machine: typeof r.machine === "string" ? r.machine : "",
|
|
5104
|
+
requested_at: typeof r.requested_at === "number" ? r.requested_at : 0
|
|
5105
|
+
};
|
|
5106
|
+
}
|
|
5107
|
+
function parseApprovalResponse(value) {
|
|
5108
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
5109
|
+
const r = value;
|
|
5110
|
+
if (typeof r.tool_call_id !== "string" || typeof r.choice !== "string") return null;
|
|
5111
|
+
if (!["allow", "deny", "always", "always_risk"].includes(r.choice)) return null;
|
|
5112
|
+
return {
|
|
5113
|
+
tool_call_id: r.tool_call_id,
|
|
5114
|
+
choice: r.choice,
|
|
5115
|
+
reason: typeof r.reason === "string" ? r.reason : void 0,
|
|
5116
|
+
decided_at: typeof r.decided_at === "number" ? r.decided_at : 0
|
|
5117
|
+
};
|
|
5118
|
+
}
|
|
5119
|
+
async function writeApprovalResponse(api, threadId, response) {
|
|
5120
|
+
await api.kvSet(threadId, APPROVAL_RESPONSE_KEY, response);
|
|
5121
|
+
}
|
|
5122
|
+
async function awaitApprovalViaRelay(api, threadId, request, options = {}) {
|
|
5123
|
+
const pollMs = options.pollMs ?? 2e3;
|
|
5124
|
+
const timeoutMs = options.timeoutMs ?? 24 * 60 * 60 * 1e3;
|
|
5125
|
+
await api.kvSet(threadId, APPROVAL_REQUEST_KEY, request);
|
|
5126
|
+
const deadline = Date.now() + timeoutMs;
|
|
5127
|
+
try {
|
|
5128
|
+
while (Date.now() < deadline) {
|
|
5129
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
5130
|
+
const response = parseApprovalResponse(await api.kvGet(threadId, APPROVAL_RESPONSE_KEY));
|
|
5131
|
+
if (response && response.tool_call_id === request.tool_call_id) {
|
|
5132
|
+
return response;
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
return null;
|
|
5136
|
+
} finally {
|
|
5137
|
+
await api.kvSet(threadId, APPROVAL_REQUEST_KEY, null).catch(() => {
|
|
5138
|
+
});
|
|
5139
|
+
await api.kvSet(threadId, APPROVAL_RESPONSE_KEY, null).catch(() => {
|
|
5140
|
+
});
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
var PKG_NAME = "@standardagents/code";
|
|
5144
|
+
var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}`;
|
|
5145
|
+
var CACHE_REL_DIR = ".config/standardagents-cli";
|
|
5146
|
+
var CACHE_FILE = "update-check.json";
|
|
5147
|
+
var STATE_FILE = "auto-update.json";
|
|
5148
|
+
var CACHE_TTL_MS = 1e3 * 60 * 60 * 6;
|
|
5149
|
+
var CHECK_TIMEOUT_MS = 4e3;
|
|
5150
|
+
var IN_FLIGHT_TTL_MS = 15 * 60 * 1e3;
|
|
5151
|
+
function cacheDir() {
|
|
5152
|
+
return path3.join(homedir(), CACHE_REL_DIR);
|
|
5153
|
+
}
|
|
5154
|
+
function cachePath() {
|
|
5155
|
+
return path3.join(cacheDir(), CACHE_FILE);
|
|
5156
|
+
}
|
|
5157
|
+
function readCache() {
|
|
5158
|
+
try {
|
|
5159
|
+
const raw = fs4.readFileSync(cachePath(), "utf-8");
|
|
5160
|
+
return JSON.parse(raw);
|
|
5161
|
+
} catch {
|
|
5162
|
+
return null;
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
function writeCache(latest) {
|
|
5166
|
+
try {
|
|
5167
|
+
const dir = cacheDir();
|
|
5168
|
+
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
5169
|
+
fs4.writeFileSync(cachePath(), JSON.stringify({ latest, timestamp: Date.now() }));
|
|
5170
|
+
} catch {
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
function readAutoUpdateState(dir = cacheDir()) {
|
|
5174
|
+
try {
|
|
5175
|
+
const raw = fs4.readFileSync(path3.join(dir, STATE_FILE), "utf-8");
|
|
5176
|
+
const state = JSON.parse(raw);
|
|
5177
|
+
return typeof state?.version === "string" ? state : null;
|
|
5178
|
+
} catch {
|
|
5179
|
+
return null;
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
function writeAutoUpdateState(state, dir = cacheDir()) {
|
|
5183
|
+
try {
|
|
5184
|
+
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
5185
|
+
fs4.writeFileSync(path3.join(dir, STATE_FILE), JSON.stringify(state));
|
|
4427
5186
|
} catch {
|
|
4428
5187
|
}
|
|
4429
5188
|
}
|
|
@@ -4558,11 +5317,728 @@ function runUpdate(pm) {
|
|
|
4558
5317
|
});
|
|
4559
5318
|
}
|
|
4560
5319
|
|
|
5320
|
+
// src/daemon.ts
|
|
5321
|
+
var HEARTBEAT_MS = 3e4;
|
|
5322
|
+
var RECLAIM_PROBE_MS = 2 * 6e4;
|
|
5323
|
+
var UPDATE_CHECK_MS = 6 * 60 * 6e4;
|
|
5324
|
+
var SWEEP_MS = 10 * 6e4;
|
|
5325
|
+
var MAX_WORKERS = 30;
|
|
5326
|
+
var LOG_MAX_BYTES = 1e6;
|
|
5327
|
+
var LOG_FILE = path3.join(os9.homedir(), ".standardagents", "daemon.log");
|
|
5328
|
+
function daemonLog(line) {
|
|
5329
|
+
try {
|
|
5330
|
+
fs4.mkdirSync(path3.dirname(LOG_FILE), { recursive: true });
|
|
5331
|
+
try {
|
|
5332
|
+
if (fs4.statSync(LOG_FILE).size > LOG_MAX_BYTES) {
|
|
5333
|
+
fs4.renameSync(LOG_FILE, `${LOG_FILE}.old`);
|
|
5334
|
+
}
|
|
5335
|
+
} catch {
|
|
5336
|
+
}
|
|
5337
|
+
fs4.appendFileSync(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
5338
|
+
`);
|
|
5339
|
+
} catch {
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
function pathFromTags(tags) {
|
|
5343
|
+
const tag = tags.find((t) => t.startsWith("path:"));
|
|
5344
|
+
if (!tag) return null;
|
|
5345
|
+
const raw = tag.slice("path:".length);
|
|
5346
|
+
return raw.replace(/^~(?=\/|$)/, os9.homedir());
|
|
5347
|
+
}
|
|
5348
|
+
var ThreadWorker = class {
|
|
5349
|
+
constructor(api, identity, threadId, projectDir, createdAt) {
|
|
5350
|
+
this.api = api;
|
|
5351
|
+
this.identity = identity;
|
|
5352
|
+
this.threadId = threadId;
|
|
5353
|
+
this.projectDir = projectDir;
|
|
5354
|
+
this.createdAt = createdAt;
|
|
5355
|
+
const machineName = machineDisplayName(identity);
|
|
5356
|
+
const registry = new ProcessRegistry(api, threadId, machineName);
|
|
5357
|
+
const mcp = new McpManager(projectDir);
|
|
5358
|
+
const publishCatalog = () => void api.kvSet(threadId, "mcp_catalog", mcp.catalog()).catch(() => {
|
|
5359
|
+
});
|
|
5360
|
+
const host = new HostTools(projectDir, registry, threadId, machineName, mcp, publishCatalog, api);
|
|
5361
|
+
this.perm = { level: 1, alwaysAllow: /* @__PURE__ */ new Set(), allowRisk: /* @__PURE__ */ new Set() };
|
|
5362
|
+
this.bridge = new Bridge(
|
|
5363
|
+
api,
|
|
5364
|
+
threadId,
|
|
5365
|
+
host,
|
|
5366
|
+
this.perm,
|
|
5367
|
+
{
|
|
5368
|
+
onActivity: (line) => daemonLog(`[${threadId.slice(0, 8)}] ${line}`),
|
|
5369
|
+
onStatus: (_id, summary) => {
|
|
5370
|
+
this.inFlight += summary ? 1 : -1;
|
|
5371
|
+
if (this.inFlight < 0) this.inFlight = 0;
|
|
5372
|
+
},
|
|
5373
|
+
onConnection: (state, attempt) => {
|
|
5374
|
+
if (state !== "reconnecting" || attempt === 1 || attempt % 10 === 0) {
|
|
5375
|
+
daemonLog(`[${threadId.slice(0, 8)}] bridge ${state}${attempt ? ` (attempt ${attempt})` : ""}`);
|
|
5376
|
+
}
|
|
5377
|
+
},
|
|
5378
|
+
onOwnership: (isOwner, owner) => {
|
|
5379
|
+
this.isOwner = isOwner;
|
|
5380
|
+
daemonLog(
|
|
5381
|
+
`[${threadId.slice(0, 8)}] ownership: ${isOwner ? "OWNER" : "watcher"}` + (owner ? ` (owner: ${owner.client_name || owner.client_id})` : "")
|
|
5382
|
+
);
|
|
5383
|
+
},
|
|
5384
|
+
onSuperseded: () => {
|
|
5385
|
+
daemonLog(`[${threadId.slice(0, 8)}] superseded by another process with this identity \u2014 standing down`);
|
|
5386
|
+
this.onEvicted?.(threadId);
|
|
5387
|
+
},
|
|
5388
|
+
// The daemon's approvals are edited by OTHER clients (the watching
|
|
5389
|
+
// CLI's level menu writes the thread KV) — re-sync before every
|
|
5390
|
+
// permission decision so a level change applies to the next call.
|
|
5391
|
+
refreshPermissions: () => this.reloadApprovals(),
|
|
5392
|
+
requestApproval: async (req, summary, effectiveRisk) => {
|
|
5393
|
+
const permKey = permissionKey(req);
|
|
5394
|
+
const fresh = decide(this.perm, permKey, effectiveRisk, !!req.requestPermission);
|
|
5395
|
+
if (fresh === "allow") return { choice: "allow" };
|
|
5396
|
+
if (!req.toolCallId) {
|
|
5397
|
+
return { choice: "deny", reason: "No one is available to approve this right now." };
|
|
5398
|
+
}
|
|
5399
|
+
daemonLog(`[${threadId.slice(0, 8)}] relaying approval: ${summary} (risk ${effectiveRisk})`);
|
|
5400
|
+
const response = await awaitApprovalViaRelay(api, threadId, {
|
|
5401
|
+
tool_call_id: req.toolCallId,
|
|
5402
|
+
tool: req.tool,
|
|
5403
|
+
summary,
|
|
5404
|
+
permission: req.requestPermission,
|
|
5405
|
+
risk: effectiveRisk,
|
|
5406
|
+
machine: machineName,
|
|
5407
|
+
requested_at: Date.now()
|
|
5408
|
+
});
|
|
5409
|
+
if (!response) {
|
|
5410
|
+
return { choice: "deny", reason: "The approval request timed out with no one to approve it." };
|
|
5411
|
+
}
|
|
5412
|
+
if (response.choice === "always") this.perm.alwaysAllow.add(permKey);
|
|
5413
|
+
if (response.choice === "always_risk") this.perm.allowRisk.add(effectiveRisk);
|
|
5414
|
+
if (response.choice === "always" || response.choice === "always_risk") {
|
|
5415
|
+
saveApprovals(api, threadId, this.perm);
|
|
5416
|
+
}
|
|
5417
|
+
return { choice: response.choice, reason: response.reason };
|
|
5418
|
+
}
|
|
5419
|
+
},
|
|
5420
|
+
{
|
|
5421
|
+
clientId: daemonClientId(identity),
|
|
5422
|
+
clientName: machineName,
|
|
5423
|
+
clientKind: "daemon",
|
|
5424
|
+
claim: "if_unowned"
|
|
5425
|
+
}
|
|
5426
|
+
);
|
|
5427
|
+
void (async () => {
|
|
5428
|
+
for (const s of listMcpServers().filter((s2) => s2.enabled)) {
|
|
5429
|
+
try {
|
|
5430
|
+
await mcp.connect(s);
|
|
5431
|
+
} catch (e) {
|
|
5432
|
+
daemonLog(`[${threadId.slice(0, 8)}] MCP "${s.name}" failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
5433
|
+
}
|
|
5434
|
+
}
|
|
5435
|
+
publishCatalog();
|
|
5436
|
+
})();
|
|
5437
|
+
}
|
|
5438
|
+
api;
|
|
5439
|
+
identity;
|
|
5440
|
+
threadId;
|
|
5441
|
+
projectDir;
|
|
5442
|
+
createdAt;
|
|
5443
|
+
bridge;
|
|
5444
|
+
perm;
|
|
5445
|
+
inFlight = 0;
|
|
5446
|
+
isOwner = false;
|
|
5447
|
+
/** Set by the daemon so a superseded worker can remove itself. */
|
|
5448
|
+
onEvicted;
|
|
5449
|
+
get busy() {
|
|
5450
|
+
return this.inFlight > 0;
|
|
5451
|
+
}
|
|
5452
|
+
async reloadApprovals() {
|
|
5453
|
+
try {
|
|
5454
|
+
const saved = await loadApprovals(this.api, this.threadId);
|
|
5455
|
+
this.perm.alwaysAllow = new Set(saved.allowTools);
|
|
5456
|
+
this.perm.allowRisk = new Set(saved.allowRisk);
|
|
5457
|
+
if (saved.level) this.perm.level = saved.level;
|
|
5458
|
+
} catch {
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
async start() {
|
|
5462
|
+
await this.reloadApprovals();
|
|
5463
|
+
void this.api.kvSet(this.threadId, "session_info", {
|
|
5464
|
+
cwd: this.projectDir,
|
|
5465
|
+
machine: machineDisplayName(this.identity)
|
|
5466
|
+
}).catch(() => {
|
|
5467
|
+
});
|
|
5468
|
+
await this.bridge.connect();
|
|
5469
|
+
}
|
|
5470
|
+
stop() {
|
|
5471
|
+
this.bridge.close();
|
|
5472
|
+
}
|
|
5473
|
+
};
|
|
5474
|
+
async function runDaemon(options = {}) {
|
|
5475
|
+
const endpoint = normalizeEndpoint(options.endpoint || defaultEndpoint() || PRODUCTION_ENDPOINT);
|
|
5476
|
+
relaxTlsForLocalEndpoint(endpoint);
|
|
5477
|
+
const version = readVersion();
|
|
5478
|
+
const identity = loadMachineIdentity();
|
|
5479
|
+
const runnerTag = `runner:${identity.machine_id}`;
|
|
5480
|
+
const cred = getCredential(endpoint);
|
|
5481
|
+
if (!cred) {
|
|
5482
|
+
process.stderr.write(
|
|
5483
|
+
`No saved sign-in for ${endpoint}.
|
|
5484
|
+
Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
5485
|
+
`
|
|
5486
|
+
);
|
|
5487
|
+
process.exit(1);
|
|
5488
|
+
}
|
|
5489
|
+
const api = new ApiClient(endpoint, cred.access_token);
|
|
5490
|
+
const check = await api.verifyDetailed();
|
|
5491
|
+
if (!check.ok) {
|
|
5492
|
+
daemonLog(`startup: credential check failed: ${check.reason}`);
|
|
5493
|
+
process.stderr.write(`Sign-in check failed: ${check.reason}
|
|
5494
|
+
`);
|
|
5495
|
+
process.exit(1);
|
|
5496
|
+
}
|
|
5497
|
+
const applied = consumeAppliedUpdate(version);
|
|
5498
|
+
daemonLog(
|
|
5499
|
+
`daemon starting \u2014 v${version}${applied ? " (freshly auto-updated)" : ""}, machine ${identity.machine_id} (${machineDisplayName(identity)}), endpoint ${endpoint}`
|
|
5500
|
+
);
|
|
5501
|
+
process.on("uncaughtException", (err) => {
|
|
5502
|
+
daemonLog(`FATAL uncaughtException: ${err?.stack || err}`);
|
|
5503
|
+
process.exit(1);
|
|
5504
|
+
});
|
|
5505
|
+
process.on("unhandledRejection", (err) => {
|
|
5506
|
+
daemonLog(`unhandledRejection: ${err instanceof Error ? err.stack : String(err)}`);
|
|
5507
|
+
});
|
|
5508
|
+
const workers = /* @__PURE__ */ new Map();
|
|
5509
|
+
const attach = async (threadId, tags, createdAt = 0) => {
|
|
5510
|
+
if (workers.has(threadId)) return;
|
|
5511
|
+
const projectDir = pathFromTags(tags);
|
|
5512
|
+
if (!projectDir) return;
|
|
5513
|
+
if (workers.size >= MAX_WORKERS) {
|
|
5514
|
+
const oldest = [...workers.values()].filter((w) => !w.busy).sort((a, b) => a.createdAt - b.createdAt)[0];
|
|
5515
|
+
if (!oldest) return;
|
|
5516
|
+
oldest.stop();
|
|
5517
|
+
workers.delete(oldest.threadId);
|
|
5518
|
+
daemonLog(`detached ${oldest.threadId.slice(0, 8)} (worker cap)`);
|
|
5519
|
+
}
|
|
5520
|
+
try {
|
|
5521
|
+
fs4.mkdirSync(projectDir, { recursive: true });
|
|
5522
|
+
} catch (e) {
|
|
5523
|
+
daemonLog(`cannot prepare project dir ${projectDir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
5524
|
+
return;
|
|
5525
|
+
}
|
|
5526
|
+
const worker = new ThreadWorker(api, identity, threadId, projectDir, createdAt || Date.now());
|
|
5527
|
+
worker.onEvicted = (id) => detach(id);
|
|
5528
|
+
workers.set(threadId, worker);
|
|
5529
|
+
daemonLog(`attached ${threadId.slice(0, 8)} \u2192 ${projectDir}`);
|
|
5530
|
+
await worker.start();
|
|
5531
|
+
void registerProject(api, identity, projectDir).catch(() => {
|
|
5532
|
+
});
|
|
5533
|
+
};
|
|
5534
|
+
const detach = (threadId) => {
|
|
5535
|
+
const worker = workers.get(threadId);
|
|
5536
|
+
if (!worker) return;
|
|
5537
|
+
worker.stop();
|
|
5538
|
+
workers.delete(threadId);
|
|
5539
|
+
daemonLog(`detached ${threadId.slice(0, 8)}`);
|
|
5540
|
+
};
|
|
5541
|
+
const sweep = async () => {
|
|
5542
|
+
try {
|
|
5543
|
+
const threads = await api.listThreads(AGENT_ID_VARIANTS, [runnerTag]);
|
|
5544
|
+
const recent = threads.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)).slice(0, MAX_WORKERS);
|
|
5545
|
+
for (const t of recent) {
|
|
5546
|
+
await attach(t.id, t.tags, (t.created_at ?? 0) * 1e3);
|
|
5547
|
+
}
|
|
5548
|
+
} catch (e) {
|
|
5549
|
+
daemonLog(`sweep failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
5550
|
+
}
|
|
5551
|
+
};
|
|
5552
|
+
const events = new SystemEvents(api, {
|
|
5553
|
+
onOpen: () => void sweep(),
|
|
5554
|
+
onThreadCreated: (t) => {
|
|
5555
|
+
if (t.tags?.includes(runnerTag)) void attach(t.id, t.tags ?? [], (t.created_at ?? 0) * 1e3);
|
|
5556
|
+
},
|
|
5557
|
+
onThreadUpdated: (t) => {
|
|
5558
|
+
if (t.terminated) detach(t.id);
|
|
5559
|
+
else if (t.tags?.includes(runnerTag)) void attach(t.id, t.tags ?? [], (t.created_at ?? 0) * 1e3);
|
|
5560
|
+
},
|
|
5561
|
+
onThreadDeleted: (id) => detach(id)
|
|
5562
|
+
});
|
|
5563
|
+
events.connect();
|
|
5564
|
+
await sweep();
|
|
5565
|
+
await touchDaemon(api, identity, version).catch(
|
|
5566
|
+
(e) => daemonLog(`heartbeat failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
5567
|
+
);
|
|
5568
|
+
const heartbeat = setInterval(() => {
|
|
5569
|
+
void touchDaemon(api, identity, version).catch(() => {
|
|
5570
|
+
});
|
|
5571
|
+
}, HEARTBEAT_MS);
|
|
5572
|
+
const reclaim = setInterval(() => {
|
|
5573
|
+
for (const worker of workers.values()) {
|
|
5574
|
+
if (!worker.isOwner) {
|
|
5575
|
+
worker.bridge.setClaim("if_stale");
|
|
5576
|
+
worker.bridge.refresh();
|
|
5577
|
+
} else {
|
|
5578
|
+
worker.bridge.setClaim("if_unowned");
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
}, RECLAIM_PROBE_MS);
|
|
5582
|
+
let updateReady = false;
|
|
5583
|
+
const checkUpdates = async () => {
|
|
5584
|
+
try {
|
|
5585
|
+
const info = await checkForUpdate(version);
|
|
5586
|
+
if (!info) return;
|
|
5587
|
+
const pm = detectPackageManager();
|
|
5588
|
+
const decision = decideAutoUpdate(info, { state: readAutoUpdateState(), pm });
|
|
5589
|
+
if (decision === "start" && pm) {
|
|
5590
|
+
daemonLog(`auto-update: installing v${info.latest} in the background`);
|
|
5591
|
+
startBackgroundUpdate(info.latest, pm);
|
|
5592
|
+
}
|
|
5593
|
+
const state = readAutoUpdateState();
|
|
5594
|
+
if (state && state.version === info.latest && state.exitCode === 0) {
|
|
5595
|
+
daemonLog(`auto-update: v${info.latest} installed \u2014 restarting when idle`);
|
|
5596
|
+
updateReady = true;
|
|
5597
|
+
}
|
|
5598
|
+
} catch {
|
|
5599
|
+
}
|
|
5600
|
+
};
|
|
5601
|
+
void checkUpdates();
|
|
5602
|
+
const updateTimer = setInterval(() => void checkUpdates(), UPDATE_CHECK_MS);
|
|
5603
|
+
const sweeper = setInterval(() => {
|
|
5604
|
+
if (updateReady && ![...workers.values()].some((w) => w.busy)) {
|
|
5605
|
+
daemonLog("restarting to apply the installed update");
|
|
5606
|
+
shutdown(0);
|
|
5607
|
+
return;
|
|
5608
|
+
}
|
|
5609
|
+
void sweep();
|
|
5610
|
+
}, SWEEP_MS);
|
|
5611
|
+
const shutdown = (code) => {
|
|
5612
|
+
clearInterval(heartbeat);
|
|
5613
|
+
clearInterval(reclaim);
|
|
5614
|
+
clearInterval(updateTimer);
|
|
5615
|
+
clearInterval(sweeper);
|
|
5616
|
+
for (const worker of workers.values()) worker.stop();
|
|
5617
|
+
events.close();
|
|
5618
|
+
daemonLog(`daemon exiting (code ${code})`);
|
|
5619
|
+
process.exit(code);
|
|
5620
|
+
};
|
|
5621
|
+
process.on("SIGTERM", () => shutdown(0));
|
|
5622
|
+
process.on("SIGINT", () => shutdown(0));
|
|
5623
|
+
daemonLog(`daemon ready \u2014 watching for threads tagged ${runnerTag}`);
|
|
5624
|
+
await new Promise(() => {
|
|
5625
|
+
});
|
|
5626
|
+
}
|
|
5627
|
+
var SERVICE_LABEL = "ai.standardcode.daemon";
|
|
5628
|
+
var SYSTEMD_UNIT = "standardcode-daemon.service";
|
|
5629
|
+
function resolveDaemonCommand(extraArgs = []) {
|
|
5630
|
+
const entry = path3.resolve(process.argv[1] ?? "");
|
|
5631
|
+
if (!entry) throw new Error("Cannot determine how this CLI was launched.");
|
|
5632
|
+
const argv = [process.execPath];
|
|
5633
|
+
if (entry.endsWith(".ts")) {
|
|
5634
|
+
let dir = path3.dirname(entry);
|
|
5635
|
+
let tsx = null;
|
|
5636
|
+
for (let i = 0; i < 6 && dir !== path3.dirname(dir); i++) {
|
|
5637
|
+
const candidate = path3.join(dir, "node_modules", "tsx", "dist", "cli.mjs");
|
|
5638
|
+
if (fs4.existsSync(candidate)) {
|
|
5639
|
+
tsx = candidate;
|
|
5640
|
+
break;
|
|
5641
|
+
}
|
|
5642
|
+
dir = path3.dirname(dir);
|
|
5643
|
+
}
|
|
5644
|
+
if (!tsx) {
|
|
5645
|
+
throw new Error(
|
|
5646
|
+
"This is a source checkout and tsx wasn't found \u2014 run `pnpm install` in the repo, or install the CLI globally and re-run daemon install."
|
|
5647
|
+
);
|
|
5648
|
+
}
|
|
5649
|
+
argv.push(tsx);
|
|
5650
|
+
}
|
|
5651
|
+
argv.push(entry, "daemon", "run", ...extraArgs);
|
|
5652
|
+
return { argv };
|
|
5653
|
+
}
|
|
5654
|
+
function servicePath() {
|
|
5655
|
+
const parts = [
|
|
5656
|
+
path3.dirname(process.execPath),
|
|
5657
|
+
"/opt/homebrew/bin",
|
|
5658
|
+
"/usr/local/bin",
|
|
5659
|
+
"/usr/bin",
|
|
5660
|
+
"/bin",
|
|
5661
|
+
"/usr/sbin",
|
|
5662
|
+
"/sbin"
|
|
5663
|
+
];
|
|
5664
|
+
const current = (process.env.PATH || "").split(":").filter(Boolean);
|
|
5665
|
+
return [.../* @__PURE__ */ new Set([...current, ...parts])].join(":");
|
|
5666
|
+
}
|
|
5667
|
+
function run2(cmd, args) {
|
|
5668
|
+
const res = spawnSync(cmd, args, { encoding: "utf8" });
|
|
5669
|
+
const output4 = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
|
|
5670
|
+
return { ok: res.status === 0, output: output4 };
|
|
5671
|
+
}
|
|
5672
|
+
var plistPath = () => path3.join(os9.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
5673
|
+
var unitPath = () => path3.join(os9.homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
5674
|
+
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
5675
|
+
function installService(command, endpoint) {
|
|
5676
|
+
if (process.platform === "darwin") return installLaunchd(command, endpoint);
|
|
5677
|
+
if (process.platform === "linux") return installSystemd(command, endpoint);
|
|
5678
|
+
return {
|
|
5679
|
+
ok: false,
|
|
5680
|
+
detail: `Unsupported platform for the daemon service: ${process.platform}`,
|
|
5681
|
+
manualHint: "Run `standardcode daemon run` under your own supervisor."
|
|
5682
|
+
};
|
|
5683
|
+
}
|
|
5684
|
+
function installLaunchd(command, endpoint) {
|
|
5685
|
+
const logDir = path3.join(os9.homedir(), ".standardagents");
|
|
5686
|
+
fs4.mkdirSync(logDir, { recursive: true });
|
|
5687
|
+
fs4.mkdirSync(path3.dirname(plistPath()), { recursive: true });
|
|
5688
|
+
const envEntries = [
|
|
5689
|
+
` <key>PATH</key><string>${xmlEscape(servicePath())}</string>`,
|
|
5690
|
+
...endpoint ? [` <key>STANDARD_CODE_DAEMON_ENDPOINT</key><string>${xmlEscape(endpoint)}</string>`] : []
|
|
5691
|
+
].join("\n");
|
|
5692
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
5693
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
5694
|
+
<plist version="1.0">
|
|
5695
|
+
<dict>
|
|
5696
|
+
<key>Label</key><string>${SERVICE_LABEL}</string>
|
|
5697
|
+
<key>ProgramArguments</key>
|
|
5698
|
+
<array>
|
|
5699
|
+
${command.argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n")}
|
|
5700
|
+
</array>
|
|
5701
|
+
<key>RunAtLoad</key><true/>
|
|
5702
|
+
<key>KeepAlive</key><true/>
|
|
5703
|
+
<key>ThrottleInterval</key><integer>5</integer>
|
|
5704
|
+
<key>StandardOutPath</key><string>${xmlEscape(path3.join(logDir, "daemon.out.log"))}</string>
|
|
5705
|
+
<key>StandardErrorPath</key><string>${xmlEscape(path3.join(logDir, "daemon.err.log"))}</string>
|
|
5706
|
+
<key>EnvironmentVariables</key>
|
|
5707
|
+
<dict>
|
|
5708
|
+
${envEntries}
|
|
5709
|
+
</dict>
|
|
5710
|
+
</dict>
|
|
5711
|
+
</plist>
|
|
5712
|
+
`;
|
|
5713
|
+
fs4.writeFileSync(plistPath(), plist);
|
|
5714
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 501;
|
|
5715
|
+
run2("launchctl", ["bootout", `gui/${uid}`, plistPath()]);
|
|
5716
|
+
const boot = run2("launchctl", ["bootstrap", `gui/${uid}`, plistPath()]);
|
|
5717
|
+
if (!boot.ok) {
|
|
5718
|
+
const load = run2("launchctl", ["load", "-w", plistPath()]);
|
|
5719
|
+
if (!load.ok) {
|
|
5720
|
+
return {
|
|
5721
|
+
ok: false,
|
|
5722
|
+
detail: `launchctl failed: ${boot.output || load.output}`,
|
|
5723
|
+
manualHint: `Load it manually: launchctl bootstrap gui/${uid} ${plistPath()}`
|
|
5724
|
+
};
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
return { ok: true, detail: `LaunchAgent installed (${plistPath()})` };
|
|
5728
|
+
}
|
|
5729
|
+
function installSystemd(command, endpoint) {
|
|
5730
|
+
fs4.mkdirSync(path3.dirname(unitPath()), { recursive: true });
|
|
5731
|
+
const unit = `[Unit]
|
|
5732
|
+
Description=Standard Code daemon (headless coding-agent execution client)
|
|
5733
|
+
After=network-online.target
|
|
5734
|
+
|
|
5735
|
+
[Service]
|
|
5736
|
+
ExecStart=${command.argv.map((a) => a.includes(" ") ? `"${a}"` : a).join(" ")}
|
|
5737
|
+
Restart=always
|
|
5738
|
+
RestartSec=5
|
|
5739
|
+
Environment=PATH=${servicePath()}
|
|
5740
|
+
${endpoint ? `Environment=STANDARD_CODE_DAEMON_ENDPOINT=${endpoint}
|
|
5741
|
+
` : ""}
|
|
5742
|
+
[Install]
|
|
5743
|
+
WantedBy=default.target
|
|
5744
|
+
`;
|
|
5745
|
+
fs4.writeFileSync(unitPath(), unit);
|
|
5746
|
+
const reload = run2("systemctl", ["--user", "daemon-reload"]);
|
|
5747
|
+
if (!reload.ok) {
|
|
5748
|
+
return {
|
|
5749
|
+
ok: false,
|
|
5750
|
+
detail: `systemd user manager unavailable: ${reload.output}`,
|
|
5751
|
+
manualHint: `Unit written to ${unitPath()}. On a server without a user manager, copy it to /etc/systemd/system/ (sudo), change ExecStart's user if needed, then: sudo systemctl enable --now ${SYSTEMD_UNIT}`
|
|
5752
|
+
};
|
|
5753
|
+
}
|
|
5754
|
+
const enable = run2("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
|
|
5755
|
+
if (!enable.ok) {
|
|
5756
|
+
return { ok: false, detail: `systemctl enable failed: ${enable.output}` };
|
|
5757
|
+
}
|
|
5758
|
+
const linger = run2("loginctl", ["enable-linger", os9.userInfo().username]);
|
|
5759
|
+
return {
|
|
5760
|
+
ok: true,
|
|
5761
|
+
detail: `systemd user unit installed (${unitPath()})` + (linger.ok ? ", lingering enabled" : ` \u2014 enable lingering manually: sudo loginctl enable-linger ${os9.userInfo().username}`)
|
|
5762
|
+
};
|
|
5763
|
+
}
|
|
5764
|
+
function uninstallService() {
|
|
5765
|
+
if (process.platform === "darwin") {
|
|
5766
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 501;
|
|
5767
|
+
run2("launchctl", ["bootout", `gui/${uid}`, plistPath()]);
|
|
5768
|
+
try {
|
|
5769
|
+
fs4.unlinkSync(plistPath());
|
|
5770
|
+
} catch {
|
|
5771
|
+
}
|
|
5772
|
+
return { ok: true, detail: "LaunchAgent removed" };
|
|
5773
|
+
}
|
|
5774
|
+
if (process.platform === "linux") {
|
|
5775
|
+
run2("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
|
|
5776
|
+
try {
|
|
5777
|
+
fs4.unlinkSync(unitPath());
|
|
5778
|
+
} catch {
|
|
5779
|
+
}
|
|
5780
|
+
run2("systemctl", ["--user", "daemon-reload"]);
|
|
5781
|
+
return { ok: true, detail: "systemd user unit removed" };
|
|
5782
|
+
}
|
|
5783
|
+
return { ok: false, detail: `Unsupported platform: ${process.platform}` };
|
|
5784
|
+
}
|
|
5785
|
+
function serviceStatus() {
|
|
5786
|
+
if (process.platform === "darwin") {
|
|
5787
|
+
const installed = fs4.existsSync(plistPath());
|
|
5788
|
+
const list = run2("launchctl", ["list", SERVICE_LABEL]);
|
|
5789
|
+
const pidMatch = list.output.match(/"PID"\s*=\s*(\d+)/);
|
|
5790
|
+
return {
|
|
5791
|
+
installed,
|
|
5792
|
+
running: list.ok && !!pidMatch,
|
|
5793
|
+
detail: installed ? list.ok ? pidMatch ? `running (pid ${pidMatch[1]})` : "loaded, not running" : "installed, not loaded" : "not installed"
|
|
5794
|
+
};
|
|
5795
|
+
}
|
|
5796
|
+
if (process.platform === "linux") {
|
|
5797
|
+
const installed = fs4.existsSync(unitPath());
|
|
5798
|
+
const active = run2("systemctl", ["--user", "is-active", SYSTEMD_UNIT]);
|
|
5799
|
+
return {
|
|
5800
|
+
installed,
|
|
5801
|
+
running: active.output.trim() === "active",
|
|
5802
|
+
detail: installed ? active.output.trim() || "unknown" : "not installed"
|
|
5803
|
+
};
|
|
5804
|
+
}
|
|
5805
|
+
return { installed: false, running: false, detail: `unsupported platform ${process.platform}` };
|
|
5806
|
+
}
|
|
5807
|
+
|
|
5808
|
+
// src/daemon-cli.ts
|
|
5809
|
+
var c2 = {
|
|
5810
|
+
reset: "\x1B[0m",
|
|
5811
|
+
dim: "\x1B[2m",
|
|
5812
|
+
bold: "\x1B[1m",
|
|
5813
|
+
green: "\x1B[32m",
|
|
5814
|
+
red: "\x1B[31m",
|
|
5815
|
+
yellow: "\x1B[33m",
|
|
5816
|
+
teal: "\x1B[38;5;37m"
|
|
5817
|
+
};
|
|
5818
|
+
function usage() {
|
|
5819
|
+
stdout.write(
|
|
5820
|
+
[
|
|
5821
|
+
"",
|
|
5822
|
+
`${c2.bold}standardcode daemon${c2.reset} \u2014 headless execution client for this machine`,
|
|
5823
|
+
"",
|
|
5824
|
+
`${c2.bold}Commands${c2.reset}`,
|
|
5825
|
+
" install [--endpoint url] Sign in (if needed), name this machine, and install",
|
|
5826
|
+
" the always-on service (launchd / systemd).",
|
|
5827
|
+
" uninstall Stop and remove the service.",
|
|
5828
|
+
" status Service + registry status for this machine.",
|
|
5829
|
+
" run [--endpoint url] Run the daemon in the foreground (what the service runs).",
|
|
5830
|
+
" add-project <path> Register a project directory for remote sessions.",
|
|
5831
|
+
" remove-project <path> Remove a registered project directory.",
|
|
5832
|
+
""
|
|
5833
|
+
].join("\n")
|
|
5834
|
+
);
|
|
5835
|
+
}
|
|
5836
|
+
function parseEndpointFlag(args) {
|
|
5837
|
+
const rest = [];
|
|
5838
|
+
let endpoint;
|
|
5839
|
+
for (let i = 0; i < args.length; i++) {
|
|
5840
|
+
const arg = args[i];
|
|
5841
|
+
if (arg === "--endpoint" || arg === "-e") {
|
|
5842
|
+
endpoint = args[++i];
|
|
5843
|
+
} else if (arg.startsWith("--endpoint=")) {
|
|
5844
|
+
endpoint = arg.slice("--endpoint=".length);
|
|
5845
|
+
} else {
|
|
5846
|
+
rest.push(arg);
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
return { endpoint, rest };
|
|
5850
|
+
}
|
|
5851
|
+
function resolveEndpoint(flag) {
|
|
5852
|
+
return normalizeEndpoint(
|
|
5853
|
+
flag || process.env.STANDARD_CODE_DAEMON_ENDPOINT || defaultEndpoint() || PRODUCTION_ENDPOINT
|
|
5854
|
+
);
|
|
5855
|
+
}
|
|
5856
|
+
async function ensureSignedIn(endpoint) {
|
|
5857
|
+
relaxTlsForLocalEndpoint(endpoint);
|
|
5858
|
+
const host = endpoint.replace(/^https?:\/\//, "");
|
|
5859
|
+
const stored = getCredential(endpoint);
|
|
5860
|
+
if (stored) {
|
|
5861
|
+
const api2 = new ApiClient(endpoint, stored.access_token);
|
|
5862
|
+
const check2 = await api2.verifyDetailed();
|
|
5863
|
+
if (check2.ok) return api2;
|
|
5864
|
+
stdout.write(`${c2.yellow}Saved sign-in for ${host} failed:${c2.reset} ${check2.reason}
|
|
5865
|
+
|
|
5866
|
+
`);
|
|
5867
|
+
}
|
|
5868
|
+
stdout.write(`${c2.bold}Sign in to Standard Code${c2.reset} ${c2.dim}(${host})${c2.reset}
|
|
5869
|
+
|
|
5870
|
+
`);
|
|
5871
|
+
const token = await deviceLogin(endpoint);
|
|
5872
|
+
const api = new ApiClient(endpoint, token);
|
|
5873
|
+
const check = await api.verifyDetailed();
|
|
5874
|
+
if (!check.ok) throw new Error(`Sign-in didn't verify: ${check.reason}`);
|
|
5875
|
+
saveCredential(
|
|
5876
|
+
{ endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
|
|
5877
|
+
{ updateDefault: false }
|
|
5878
|
+
);
|
|
5879
|
+
stdout.write(`${c2.green}\u2713${c2.reset} Signed in to ${c2.teal}${host}${c2.reset}
|
|
5880
|
+
|
|
5881
|
+
`);
|
|
5882
|
+
return api;
|
|
5883
|
+
}
|
|
5884
|
+
async function installCommand(endpointFlag) {
|
|
5885
|
+
const endpoint = resolveEndpoint(endpointFlag);
|
|
5886
|
+
const api = await ensureSignedIn(endpoint);
|
|
5887
|
+
const identity = loadMachineIdentity();
|
|
5888
|
+
const rl = readline2.createInterface({ input: stdin, output: stdout });
|
|
5889
|
+
const suggested = machineDisplayName(identity);
|
|
5890
|
+
const answer = (await rl.question(
|
|
5891
|
+
`${c2.bold}Machine name${c2.reset} ${c2.dim}(shown in the session picker)${c2.reset} [${suggested}]: `
|
|
5892
|
+
)).trim();
|
|
5893
|
+
rl.close();
|
|
5894
|
+
if (answer) setMachineName(answer);
|
|
5895
|
+
const named = loadMachineIdentity();
|
|
5896
|
+
await updateOwnMachineRecord(api, named);
|
|
5897
|
+
stdout.write(`${c2.dim}Installing the always-on service\u2026${c2.reset}
|
|
5898
|
+
`);
|
|
5899
|
+
const extra = endpointFlag ? ["--endpoint", endpoint] : [];
|
|
5900
|
+
const result = installService(resolveDaemonCommand(extra), endpointFlag ? endpoint : void 0);
|
|
5901
|
+
if (!result.ok) {
|
|
5902
|
+
stdout.write(`${c2.red}\u2717${c2.reset} ${result.detail}
|
|
5903
|
+
`);
|
|
5904
|
+
if (result.manualHint) stdout.write(`${c2.dim}${result.manualHint}${c2.reset}
|
|
5905
|
+
`);
|
|
5906
|
+
process.exit(1);
|
|
5907
|
+
}
|
|
5908
|
+
stdout.write(`${c2.green}\u2713${c2.reset} ${result.detail}
|
|
5909
|
+
`);
|
|
5910
|
+
stdout.write(`${c2.dim}Waiting for the daemon's first heartbeat\u2026${c2.reset}
|
|
5911
|
+
`);
|
|
5912
|
+
const deadline = Date.now() + 3e4;
|
|
5913
|
+
let alive = false;
|
|
5914
|
+
while (Date.now() < deadline) {
|
|
5915
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
5916
|
+
const record = await loadMachine(api, named.machine_id);
|
|
5917
|
+
if (record && daemonOnline(record)) {
|
|
5918
|
+
alive = true;
|
|
5919
|
+
break;
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5922
|
+
if (alive) {
|
|
5923
|
+
stdout.write(
|
|
5924
|
+
`${c2.green}\u2713${c2.reset} ${c2.bold}${machineDisplayName(named)}${c2.reset} is online.
|
|
5925
|
+
|
|
5926
|
+
Sessions started elsewhere can now run on this machine.
|
|
5927
|
+
${c2.dim}Projects register automatically when you run standardcode in a directory here,
|
|
5928
|
+
or add one now: standardcode daemon add-project <path>${c2.reset}
|
|
5929
|
+
`
|
|
5930
|
+
);
|
|
5931
|
+
} else {
|
|
5932
|
+
stdout.write(
|
|
5933
|
+
`${c2.yellow}\u26A0${c2.reset} The service installed but no heartbeat arrived yet.
|
|
5934
|
+
${c2.dim}Check ~/.standardagents/daemon.log and \`standardcode daemon status\`.${c2.reset}
|
|
5935
|
+
`
|
|
5936
|
+
);
|
|
5937
|
+
}
|
|
5938
|
+
}
|
|
5939
|
+
async function statusCommand() {
|
|
5940
|
+
const status = serviceStatus();
|
|
5941
|
+
const identity = loadMachineIdentity();
|
|
5942
|
+
stdout.write(`${c2.bold}Service:${c2.reset} ${status.detail}
|
|
5943
|
+
`);
|
|
5944
|
+
stdout.write(`${c2.bold}Machine:${c2.reset} ${machineDisplayName(identity)} ${c2.dim}(${identity.machine_id})${c2.reset}
|
|
5945
|
+
`);
|
|
5946
|
+
const endpoint = resolveEndpoint();
|
|
5947
|
+
const cred = getCredential(endpoint);
|
|
5948
|
+
if (!cred) {
|
|
5949
|
+
stdout.write(`${c2.bold}Account:${c2.reset} ${c2.yellow}not signed in to ${endpoint}${c2.reset}
|
|
5950
|
+
`);
|
|
5951
|
+
return;
|
|
5952
|
+
}
|
|
5953
|
+
relaxTlsForLocalEndpoint(endpoint);
|
|
5954
|
+
const api = new ApiClient(endpoint, cred.access_token);
|
|
5955
|
+
const record = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
5956
|
+
if (!record) {
|
|
5957
|
+
stdout.write(`${c2.bold}Registry:${c2.reset} not registered yet
|
|
5958
|
+
`);
|
|
5959
|
+
return;
|
|
5960
|
+
}
|
|
5961
|
+
const online = daemonOnline(record);
|
|
5962
|
+
const seen = record.daemon ? `${Math.round((Date.now() - record.daemon.last_seen_at) / 1e3)}s ago (v${record.daemon.version})` : "never";
|
|
5963
|
+
stdout.write(`${c2.bold}Registry:${c2.reset} ${online ? `${c2.green}online${c2.reset}` : `${c2.yellow}offline${c2.reset}`} \xB7 last heartbeat ${seen}
|
|
5964
|
+
`);
|
|
5965
|
+
const projects = Object.keys(record.projects);
|
|
5966
|
+
stdout.write(`${c2.bold}Projects:${c2.reset} ${projects.length ? "" : c2.dim + "none registered" + c2.reset}
|
|
5967
|
+
`);
|
|
5968
|
+
for (const p of projects.sort()) stdout.write(` ${c2.dim}${p}${c2.reset}
|
|
5969
|
+
`);
|
|
5970
|
+
}
|
|
5971
|
+
async function projectCommand(action, target) {
|
|
5972
|
+
if (!target) {
|
|
5973
|
+
stdout.write(`${c2.red}\u2717${c2.reset} Expected a project path.
|
|
5974
|
+
`);
|
|
5975
|
+
process.exit(1);
|
|
5976
|
+
}
|
|
5977
|
+
const dir = path3.resolve(target);
|
|
5978
|
+
if (action === "add" && !fs4.existsSync(dir)) {
|
|
5979
|
+
stdout.write(`${c2.red}\u2717${c2.reset} ${dir} does not exist on this machine.
|
|
5980
|
+
`);
|
|
5981
|
+
process.exit(1);
|
|
5982
|
+
}
|
|
5983
|
+
const endpoint = resolveEndpoint();
|
|
5984
|
+
const api = await ensureSignedIn(endpoint);
|
|
5985
|
+
const identity = loadMachineIdentity();
|
|
5986
|
+
if (action === "add") {
|
|
5987
|
+
await registerProject(api, identity, dir);
|
|
5988
|
+
stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir} for remote sessions on ${machineDisplayName(identity)}.
|
|
5989
|
+
`);
|
|
5990
|
+
} else {
|
|
5991
|
+
await unregisterProject(api, identity, dir);
|
|
5992
|
+
stdout.write(`${c2.green}\u2713${c2.reset} Removed ${dir} from this machine's projects.
|
|
5993
|
+
`);
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
async function runDaemonCommand(argv) {
|
|
5997
|
+
const [command, ...restArgs] = argv;
|
|
5998
|
+
const { endpoint, rest } = parseEndpointFlag(restArgs);
|
|
5999
|
+
switch (command) {
|
|
6000
|
+
case "run":
|
|
6001
|
+
await runDaemon({ endpoint: endpoint || process.env.STANDARD_CODE_DAEMON_ENDPOINT });
|
|
6002
|
+
return;
|
|
6003
|
+
case "install":
|
|
6004
|
+
await installCommand(endpoint);
|
|
6005
|
+
return;
|
|
6006
|
+
case "uninstall": {
|
|
6007
|
+
const result = uninstallService();
|
|
6008
|
+
stdout.write(`${result.ok ? c2.green + "\u2713" : c2.red + "\u2717"}${c2.reset} ${result.detail}
|
|
6009
|
+
`);
|
|
6010
|
+
const ep = resolveEndpoint(endpoint);
|
|
6011
|
+
const cred = getCredential(ep);
|
|
6012
|
+
if (cred) {
|
|
6013
|
+
relaxTlsForLocalEndpoint(ep);
|
|
6014
|
+
await clearDaemon(new ApiClient(ep, cred.access_token), loadMachineIdentity()).catch(() => {
|
|
6015
|
+
});
|
|
6016
|
+
}
|
|
6017
|
+
return;
|
|
6018
|
+
}
|
|
6019
|
+
case "status":
|
|
6020
|
+
await statusCommand();
|
|
6021
|
+
return;
|
|
6022
|
+
case "add-project":
|
|
6023
|
+
await projectCommand("add", rest[0]);
|
|
6024
|
+
return;
|
|
6025
|
+
case "remove-project":
|
|
6026
|
+
await projectCommand("remove", rest[0]);
|
|
6027
|
+
return;
|
|
6028
|
+
case "version":
|
|
6029
|
+
stdout.write(`standardcode daemon v${readVersion()}
|
|
6030
|
+
`);
|
|
6031
|
+
return;
|
|
6032
|
+
default:
|
|
6033
|
+
usage();
|
|
6034
|
+
if (command && command !== "help" && command !== "--help" && command !== "-h") {
|
|
6035
|
+
process.exit(1);
|
|
6036
|
+
}
|
|
6037
|
+
}
|
|
6038
|
+
}
|
|
6039
|
+
|
|
4561
6040
|
// src/index.ts
|
|
4562
|
-
var
|
|
4563
|
-
var AGENT_ID_VARIANTS = [AGENT_ID, "standard_code_high_agent"];
|
|
4564
|
-
var PRODUCTION_ENDPOINT = "https://api.standardcode.ai";
|
|
4565
|
-
var c = {
|
|
6041
|
+
var c3 = {
|
|
4566
6042
|
reset: "\x1B[0m",
|
|
4567
6043
|
dim: "\x1B[2m",
|
|
4568
6044
|
bold: "\x1B[1m",
|
|
@@ -4591,10 +6067,10 @@ function printUsage() {
|
|
|
4591
6067
|
stdout.write(
|
|
4592
6068
|
[
|
|
4593
6069
|
"",
|
|
4594
|
-
`${
|
|
6070
|
+
`${c3.bold}Usage${c3.reset}`,
|
|
4595
6071
|
" standardcode [options] [dir]",
|
|
4596
6072
|
"",
|
|
4597
|
-
`${
|
|
6073
|
+
`${c3.bold}Options${c3.reset}`,
|
|
4598
6074
|
" -e, --endpoint [url] Use a different Standard Agents instance for this run",
|
|
4599
6075
|
" (default: https://api.standardcode.ai).",
|
|
4600
6076
|
" If url is omitted, prompt for it.",
|
|
@@ -4652,7 +6128,7 @@ function printAssistant(tui, text) {
|
|
|
4652
6128
|
let dotted = false;
|
|
4653
6129
|
for (const line of renderMarkdown(text, cols2)) {
|
|
4654
6130
|
if (!dotted && line.trim()) {
|
|
4655
|
-
tui.print(`${
|
|
6131
|
+
tui.print(`${c3.gray}\u2022${c3.reset} ${line}`);
|
|
4656
6132
|
dotted = true;
|
|
4657
6133
|
} else {
|
|
4658
6134
|
tui.print(` ${line}`);
|
|
@@ -4667,7 +6143,7 @@ function startLoader(label) {
|
|
|
4667
6143
|
const draw = () => {
|
|
4668
6144
|
const now = Date.now();
|
|
4669
6145
|
const f = frames[Math.floor(now / 70) % frames.length];
|
|
4670
|
-
stdout.write(`\r\x1B[K${pad}${brandCycleColor(now)}${f}${
|
|
6146
|
+
stdout.write(`\r\x1B[K${pad}${brandCycleColor(now)}${f}${c3.reset} ${c3.dim}${label}\u2026${c3.reset}`);
|
|
4671
6147
|
};
|
|
4672
6148
|
draw();
|
|
4673
6149
|
const timer = setInterval(draw, 70);
|
|
@@ -4683,53 +6159,25 @@ function farewell(stoppedProcs = 0) {
|
|
|
4683
6159
|
if (stoppedProcs > 0) {
|
|
4684
6160
|
stdout.write(
|
|
4685
6161
|
`
|
|
4686
|
-
${
|
|
6162
|
+
${c3.cyan}\u2699${c3.reset} Stopped ${stoppedProcs} background process${stoppedProcs === 1 ? "" : "es"}.
|
|
4687
6163
|
`
|
|
4688
6164
|
);
|
|
4689
6165
|
}
|
|
4690
6166
|
stdout.write(`
|
|
4691
|
-
${
|
|
6167
|
+
${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.reset}
|
|
4692
6168
|
`);
|
|
4693
6169
|
}
|
|
4694
|
-
function isLocalHost(host) {
|
|
4695
|
-
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.endsWith(".localhost") || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
|
|
4696
|
-
}
|
|
4697
|
-
function relaxTlsForLocalEndpoint(endpoint) {
|
|
4698
|
-
let host = "";
|
|
4699
|
-
try {
|
|
4700
|
-
host = new URL(endpoint).hostname;
|
|
4701
|
-
} catch {
|
|
4702
|
-
return false;
|
|
4703
|
-
}
|
|
4704
|
-
if (!endpoint.startsWith("https:") || !isLocalHost(host)) return false;
|
|
4705
|
-
const origEmit = process.emitWarning.bind(process);
|
|
4706
|
-
process.emitWarning = ((warning, ...args) => {
|
|
4707
|
-
const msg = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
4708
|
-
if (msg.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
|
|
4709
|
-
return origEmit(warning, ...args);
|
|
4710
|
-
});
|
|
4711
|
-
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
4712
|
-
return true;
|
|
4713
|
-
}
|
|
4714
|
-
function readVersion() {
|
|
4715
|
-
try {
|
|
4716
|
-
const pkg = JSON.parse(fs4.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
4717
|
-
return typeof pkg.version === "string" ? pkg.version : "";
|
|
4718
|
-
} catch {
|
|
4719
|
-
return "";
|
|
4720
|
-
}
|
|
4721
|
-
}
|
|
4722
6170
|
function printWelcome(endpoint, projectDir) {
|
|
4723
|
-
const home =
|
|
6171
|
+
const home = os9.homedir();
|
|
4724
6172
|
const dir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
4725
6173
|
const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
4726
6174
|
const version = readVersion();
|
|
4727
6175
|
const pad = " ";
|
|
4728
6176
|
const meta = [
|
|
4729
|
-
`${
|
|
4730
|
-
`${
|
|
4731
|
-
...endpoint === PRODUCTION_ENDPOINT ? [] : [`${
|
|
4732
|
-
`${
|
|
6177
|
+
`${c3.bold}${gradientText("Standard Code")}${c3.reset}${version ? ` ${c3.dim}v${version}${c3.reset}` : ""}`,
|
|
6178
|
+
`${c3.dim}terminal coding agent${c3.reset}`,
|
|
6179
|
+
...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c3.teal}${host}${c3.reset}`],
|
|
6180
|
+
`${c3.dim}${dir}${c3.reset}`
|
|
4733
6181
|
];
|
|
4734
6182
|
const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
|
|
4735
6183
|
const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
|
|
@@ -4744,11 +6192,11 @@ function printWelcome(endpoint, projectDir) {
|
|
|
4744
6192
|
}
|
|
4745
6193
|
function colorActivity(line) {
|
|
4746
6194
|
const m = line.match(/^(\s*)([✓✗⛔])\s?([\s\S]*)$/);
|
|
4747
|
-
if (!m) return `${
|
|
6195
|
+
if (!m) return `${c3.dim}${line}${c3.reset}`;
|
|
4748
6196
|
const [, indent, glyph, rest] = m;
|
|
4749
6197
|
if (glyph === "\u2713") {
|
|
4750
|
-
const body = rest.replace(/\s(\([^()]*\))\s*$/, ` ${
|
|
4751
|
-
return `${indent}${
|
|
6198
|
+
const body = rest.replace(/\s(\([^()]*\))\s*$/, ` ${c3.dim}$1${c3.reset}`);
|
|
6199
|
+
return `${indent}${c3.green}\u2713${c3.reset} ${body}`;
|
|
4752
6200
|
}
|
|
4753
6201
|
if (glyph === "\u2717") {
|
|
4754
6202
|
const ERR_MAX_LINES = 7;
|
|
@@ -4756,22 +6204,26 @@ function colorActivity(line) {
|
|
|
4756
6204
|
const shown = lines.slice(0, ERR_MAX_LINES);
|
|
4757
6205
|
const hidden = lines.length - shown.length;
|
|
4758
6206
|
const body = shown.map(
|
|
4759
|
-
(l, i) => i === 0 ? `${indent}${
|
|
6207
|
+
(l, i) => i === 0 ? `${indent}${c3.red}\u2717 ${l}${c3.reset}` : `${indent}${c3.red}${c3.dim}${l}${c3.reset}`
|
|
4760
6208
|
).join("\n");
|
|
4761
6209
|
if (hidden > 0) {
|
|
4762
6210
|
return `${body}
|
|
4763
|
-
${indent}${
|
|
6211
|
+
${indent}${c3.dim}\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}${c3.reset}`;
|
|
4764
6212
|
}
|
|
4765
6213
|
return body;
|
|
4766
6214
|
}
|
|
4767
|
-
return `${indent}${
|
|
6215
|
+
return `${indent}${c3.yellow}\u26D4 ${rest}${c3.reset}`;
|
|
4768
6216
|
}
|
|
4769
6217
|
async function main() {
|
|
6218
|
+
if (process.argv[2] === "daemon") {
|
|
6219
|
+
await runDaemonCommand(process.argv.slice(3));
|
|
6220
|
+
return;
|
|
6221
|
+
}
|
|
4770
6222
|
let cliArgs;
|
|
4771
6223
|
try {
|
|
4772
6224
|
cliArgs = parseArgs2(process.argv.slice(2));
|
|
4773
6225
|
} catch (error) {
|
|
4774
|
-
stdout.write(`${
|
|
6226
|
+
stdout.write(`${c3.red}error:${c3.reset} ${error instanceof Error ? error.message : String(error)}
|
|
4775
6227
|
`);
|
|
4776
6228
|
printUsage();
|
|
4777
6229
|
process.exit(1);
|
|
@@ -4784,7 +6236,7 @@ async function main() {
|
|
|
4784
6236
|
const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
|
|
4785
6237
|
const dirArg = cliArgs.dir;
|
|
4786
6238
|
const projectDir = path3.resolve(dirArg || process.cwd());
|
|
4787
|
-
const machine =
|
|
6239
|
+
const machine = os9.hostname();
|
|
4788
6240
|
const reader = { rl: null };
|
|
4789
6241
|
let handoffClosing = false;
|
|
4790
6242
|
let preflightArmed = false;
|
|
@@ -4798,7 +6250,7 @@ async function main() {
|
|
|
4798
6250
|
}
|
|
4799
6251
|
preflightArmed = true;
|
|
4800
6252
|
stdout.write(`
|
|
4801
|
-
${
|
|
6253
|
+
${c3.dim}Press Control-C again to exit${c3.reset}
|
|
4802
6254
|
`);
|
|
4803
6255
|
preflightTimer = setTimeout(() => {
|
|
4804
6256
|
preflightArmed = false;
|
|
@@ -4820,10 +6272,10 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
4820
6272
|
const askEndpoint = async () => {
|
|
4821
6273
|
for (; ; ) {
|
|
4822
6274
|
const answer = (await ask(
|
|
4823
|
-
`${
|
|
6275
|
+
`${c3.cyan}Standard Agents instance URL${c3.reset} (e.g. http://localhost:5178): `
|
|
4824
6276
|
)).trim();
|
|
4825
6277
|
if (answer) return answer;
|
|
4826
|
-
stdout.write(`${
|
|
6278
|
+
stdout.write(`${c3.dim}An endpoint URL is required.${c3.reset}
|
|
4827
6279
|
`);
|
|
4828
6280
|
}
|
|
4829
6281
|
};
|
|
@@ -4838,7 +6290,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
4838
6290
|
const tlsRelaxed = relaxTlsForLocalEndpoint(endpoint);
|
|
4839
6291
|
printWelcome(endpoint, projectDir);
|
|
4840
6292
|
if (tlsRelaxed) {
|
|
4841
|
-
stdout.write(`${
|
|
6293
|
+
stdout.write(`${c3.dim} TLS verification relaxed for local endpoint.${c3.reset}
|
|
4842
6294
|
|
|
4843
6295
|
`);
|
|
4844
6296
|
}
|
|
@@ -4850,7 +6302,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
4850
6302
|
loading.stop();
|
|
4851
6303
|
const applied = consumeAppliedUpdate(version);
|
|
4852
6304
|
if (applied) {
|
|
4853
|
-
stdout.write(` ${
|
|
6305
|
+
stdout.write(` ${c3.green}\u2713${c3.reset} ${c3.dim}Standard Code updated to v${version}.${c3.reset}
|
|
4854
6306
|
|
|
4855
6307
|
`);
|
|
4856
6308
|
}
|
|
@@ -4859,21 +6311,21 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
4859
6311
|
const decision = decideAutoUpdate(updateAvailable, { state: readAutoUpdateState(), pm });
|
|
4860
6312
|
if (decision === "start" && pm && startBackgroundUpdate(updateAvailable.latest, pm)) {
|
|
4861
6313
|
stdout.write(
|
|
4862
|
-
` ${
|
|
6314
|
+
` ${c3.teal}\u27F3${c3.reset} ${c3.dim}Standard Code ${c3.reset}${c3.bold}v${updateAvailable.latest}${c3.reset}${c3.dim} is installing in the background \u2014 it applies on your next launch.${c3.reset}
|
|
4863
6315
|
|
|
4864
6316
|
`
|
|
4865
6317
|
);
|
|
4866
6318
|
} else if (decision === "in_flight") {
|
|
4867
6319
|
stdout.write(
|
|
4868
|
-
` ${
|
|
6320
|
+
` ${c3.teal}\u27F3${c3.reset} ${c3.dim}Standard Code v${updateAvailable.latest} is still installing in the background.${c3.reset}
|
|
4869
6321
|
|
|
4870
6322
|
`
|
|
4871
6323
|
);
|
|
4872
6324
|
} else {
|
|
4873
6325
|
const display = updateCommand(pm ?? "npm").display;
|
|
4874
6326
|
stdout.write(
|
|
4875
|
-
` ${
|
|
4876
|
-
${
|
|
6327
|
+
` ${c3.teal}\u25C7${c3.reset} ${c3.dim}Update available:${c3.reset} ${c3.dim}v${updateAvailable.current}${c3.reset} \u2192 ${c3.bold}v${updateAvailable.latest}${c3.reset}
|
|
6328
|
+
${c3.dim}Run ${c3.reset}${c3.bold}${display}${c3.reset}${c3.dim} to update${c3.reset}
|
|
4877
6329
|
|
|
4878
6330
|
`
|
|
4879
6331
|
);
|
|
@@ -4891,39 +6343,39 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
4891
6343
|
if (!api || !storedCheck?.ok) {
|
|
4892
6344
|
const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
4893
6345
|
if (storedCheck && !storedCheck.ok) {
|
|
4894
|
-
stdout.write(`${
|
|
6346
|
+
stdout.write(`${c3.red}\u2717${c3.reset} ${c3.dim}Saved sign-in for this endpoint failed:${c3.reset} ${storedCheck.reason}
|
|
4895
6347
|
`);
|
|
4896
|
-
if (storedCheck.hint) stdout.write(` ${
|
|
6348
|
+
if (storedCheck.hint) stdout.write(` ${c3.dim}${storedCheck.hint}${c3.reset}
|
|
4897
6349
|
`);
|
|
4898
6350
|
stdout.write("\n");
|
|
4899
6351
|
}
|
|
4900
6352
|
const explainFailure = (result, prefix) => {
|
|
4901
|
-
stdout.write(`${
|
|
6353
|
+
stdout.write(`${c3.red}\u2717${c3.reset} ${prefix}${result.reason}
|
|
4902
6354
|
`);
|
|
4903
|
-
if (result.hint) stdout.write(` ${
|
|
6355
|
+
if (result.hint) stdout.write(` ${c3.dim}${result.hint}${c3.reset}
|
|
4904
6356
|
`);
|
|
4905
6357
|
};
|
|
4906
|
-
stdout.write(`${
|
|
6358
|
+
stdout.write(`${c3.bold}${gradientText("Sign in to Standard Code")}${c3.reset}
|
|
4907
6359
|
`);
|
|
4908
6360
|
if (`https://${host}` !== PRODUCTION_ENDPOINT) {
|
|
4909
|
-
stdout.write(`${
|
|
6361
|
+
stdout.write(`${c3.dim}Connecting to${c3.reset} ${c3.teal}${host}${c3.reset}
|
|
4910
6362
|
`);
|
|
4911
6363
|
}
|
|
4912
6364
|
stdout.write(
|
|
4913
|
-
`${
|
|
6365
|
+
`${c3.dim}You'll only need to do this once on this machine.${c3.reset}
|
|
4914
6366
|
|
|
4915
6367
|
`
|
|
4916
6368
|
);
|
|
4917
6369
|
stdout.write(
|
|
4918
|
-
`${
|
|
6370
|
+
`${c3.white}Press ${c3.bold}Enter${c3.reset}${c3.white} to open your browser and sign in.${c3.reset} ${c3.dim}(or paste an API token)${c3.reset}
|
|
4919
6371
|
|
|
4920
6372
|
`
|
|
4921
6373
|
);
|
|
4922
6374
|
for (; ; ) {
|
|
4923
|
-
const token = (await ask(`${
|
|
6375
|
+
const token = (await ask(`${c3.teal}\u276F${c3.reset} `)).trim();
|
|
4924
6376
|
if (!token) {
|
|
4925
6377
|
const got = await deviceLogin(endpoint).catch((e) => {
|
|
4926
|
-
stdout.write(`${
|
|
6378
|
+
stdout.write(`${c3.red}\u2717${c3.reset} ${c3.dim}${e instanceof Error ? e.message : String(e)}${c3.reset}
|
|
4927
6379
|
`);
|
|
4928
6380
|
return null;
|
|
4929
6381
|
});
|
|
@@ -4937,7 +6389,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
4937
6389
|
{ endpoint, access_token: got, token_type: "Bearer", saved_at: Date.now() },
|
|
4938
6390
|
{ updateDefault: !endpointOverride }
|
|
4939
6391
|
);
|
|
4940
|
-
stdout.write(`${
|
|
6392
|
+
stdout.write(`${c3.green}\u2713${c3.reset} Connected to ${c3.teal}${host}${c3.reset}
|
|
4941
6393
|
`);
|
|
4942
6394
|
break;
|
|
4943
6395
|
}
|
|
@@ -4953,7 +6405,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
4953
6405
|
{ endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
|
|
4954
6406
|
{ updateDefault: !endpointOverride }
|
|
4955
6407
|
);
|
|
4956
|
-
stdout.write(`${
|
|
6408
|
+
stdout.write(`${c3.green}\u2713${c3.reset} Connected to ${c3.teal}${host}${c3.reset}
|
|
4957
6409
|
`);
|
|
4958
6410
|
break;
|
|
4959
6411
|
}
|
|
@@ -4965,17 +6417,72 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
4965
6417
|
if (!api) process.exit(1);
|
|
4966
6418
|
handoffClosing = true;
|
|
4967
6419
|
reader.rl?.close();
|
|
4968
|
-
const
|
|
6420
|
+
const identity = loadMachineIdentity();
|
|
6421
|
+
void registerProject(api, identity, projectDir).catch(() => {
|
|
6422
|
+
});
|
|
6423
|
+
const tui = new Tui(1);
|
|
6424
|
+
const home = os9.homedir();
|
|
6425
|
+
const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
6426
|
+
const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
|
|
6427
|
+
const session = { mode: "local", identity };
|
|
6428
|
+
{
|
|
6429
|
+
const loadingMachines = startLoader("Checking your machines");
|
|
6430
|
+
const machines = await loadMachines(api).catch(() => []);
|
|
6431
|
+
loadingMachines.stop();
|
|
6432
|
+
session.suggestDaemonInstall = machines.every((m) => !m.daemon);
|
|
6433
|
+
const remoteTargets = machines.filter((m) => m.id !== identity.machine_id && daemonOnline(m));
|
|
6434
|
+
if (remoteTargets.length > 0) {
|
|
6435
|
+
const where = await tui.select(
|
|
6436
|
+
`${c3.bold}${gradientText("Where should this session run?")}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
|
|
6437
|
+
[
|
|
6438
|
+
{ label: `This machine \u2014 ${shortDir}`, hint: "tools run locally", value: null },
|
|
6439
|
+
...remoteTargets.map((m) => ({
|
|
6440
|
+
label: m.name,
|
|
6441
|
+
hint: `${m.hostname} \xB7 daemon online${m.daemon ? ` \xB7 v${m.daemon.version}` : ""}`,
|
|
6442
|
+
value: m
|
|
6443
|
+
}))
|
|
6444
|
+
]
|
|
6445
|
+
);
|
|
6446
|
+
if (where) {
|
|
6447
|
+
const remotePath = await pickRemoteProject(tui, where);
|
|
6448
|
+
if (remotePath) {
|
|
6449
|
+
session.mode = "remote";
|
|
6450
|
+
session.runner = where;
|
|
6451
|
+
session.remotePath = remotePath;
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
let tags;
|
|
6457
|
+
let resumeTags;
|
|
6458
|
+
if (session.mode === "remote" && session.runner && session.remotePath) {
|
|
6459
|
+
tags = [
|
|
6460
|
+
`path:${session.remotePath}`,
|
|
6461
|
+
`machine:${session.runner.hostname || session.runner.name}`,
|
|
6462
|
+
`runner:${session.runner.id}`
|
|
6463
|
+
];
|
|
6464
|
+
resumeTags = [`path:${session.remotePath}`, `runner:${session.runner.id}`];
|
|
6465
|
+
} else {
|
|
6466
|
+
tags = [`path:${projectDir}`, `machine:${machine}`, `runner:${identity.machine_id}`];
|
|
6467
|
+
resumeTags = [`path:${projectDir}`, `machine:${machine}`];
|
|
6468
|
+
}
|
|
4969
6469
|
const loadingSessions = startLoader("Loading sessions");
|
|
4970
6470
|
let existing = [];
|
|
4971
6471
|
try {
|
|
4972
|
-
existing = await api.listThreads(AGENT_ID_VARIANTS,
|
|
6472
|
+
existing = await api.listThreads(AGENT_ID_VARIANTS, resumeTags);
|
|
4973
6473
|
} catch {
|
|
4974
6474
|
existing = [];
|
|
4975
6475
|
}
|
|
4976
6476
|
const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
|
|
4977
6477
|
loadingSessions.stop();
|
|
4978
|
-
const
|
|
6478
|
+
const createSessionThread = async () => {
|
|
6479
|
+
const id = await api.createThread(AGENT_ID, tags);
|
|
6480
|
+
if (session.mode === "remote" && session.runner && session.remotePath) {
|
|
6481
|
+
await api.kvSet(id, "session_info", { cwd: session.remotePath, machine: session.runner.name }).catch(() => {
|
|
6482
|
+
});
|
|
6483
|
+
}
|
|
6484
|
+
return id;
|
|
6485
|
+
};
|
|
4979
6486
|
let threadId;
|
|
4980
6487
|
let resumed = false;
|
|
4981
6488
|
let historySeed;
|
|
@@ -4986,31 +6493,61 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
4986
6493
|
value: s.id
|
|
4987
6494
|
}));
|
|
4988
6495
|
items.push({ label: "\uFF0B Start a new session", value: null });
|
|
4989
|
-
const
|
|
4990
|
-
const tilde = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
4991
|
-
const shortDir = tilde.length > 38 ? "\u2026" + tilde.slice(-37) : tilde;
|
|
6496
|
+
const whereLabel = session.mode === "remote" && session.runner ? `${session.runner.name}:${shortenPath(session.remotePath ?? "")}` : shortDir;
|
|
4992
6497
|
const picked = await tui.select(
|
|
4993
|
-
`${
|
|
6498
|
+
`${c3.bold}${gradientText("Resume a session")}${c3.reset} ${c3.gray}${whereLabel}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
|
|
4994
6499
|
items
|
|
4995
6500
|
);
|
|
4996
6501
|
if (typeof picked === "string") {
|
|
4997
6502
|
threadId = picked;
|
|
4998
6503
|
resumed = true;
|
|
4999
6504
|
} else {
|
|
5000
|
-
threadId = await
|
|
6505
|
+
threadId = await createSessionThread();
|
|
5001
6506
|
historySeed = existing[0]?.id;
|
|
5002
6507
|
}
|
|
5003
6508
|
} else {
|
|
5004
|
-
threadId = await
|
|
6509
|
+
threadId = await createSessionThread();
|
|
5005
6510
|
}
|
|
5006
6511
|
for (; ; ) {
|
|
5007
|
-
await runInteractive(tui, api, threadId, projectDir, machine, resumed, historySeed);
|
|
6512
|
+
await runInteractive(tui, api, threadId, projectDir, machine, resumed, session, historySeed);
|
|
5008
6513
|
historySeed = threadId;
|
|
5009
|
-
threadId = await
|
|
6514
|
+
threadId = await createSessionThread();
|
|
5010
6515
|
await api.kvSet(threadId, "lease_supersedes", historySeed);
|
|
5011
6516
|
resumed = false;
|
|
5012
6517
|
}
|
|
5013
6518
|
}
|
|
6519
|
+
function shortenPath(p, max = 38) {
|
|
6520
|
+
return p.length > max ? "\u2026" + p.slice(-(max - 1)) : p;
|
|
6521
|
+
}
|
|
6522
|
+
async function pickRemoteProject(tui, runner) {
|
|
6523
|
+
const ENTER_PATH = "__enter_path__";
|
|
6524
|
+
const projects = Object.entries(runner.projects).sort(
|
|
6525
|
+
(a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
|
|
6526
|
+
);
|
|
6527
|
+
const items = projects.map(([dir, p]) => ({
|
|
6528
|
+
label: shortenPath(dir, 48),
|
|
6529
|
+
hint: p?.last_used_at ? relativeTime(p.last_used_at / 1e3) : "",
|
|
6530
|
+
value: dir
|
|
6531
|
+
}));
|
|
6532
|
+
items.push({ label: `\uFF0B Another path on ${runner.name}\u2026`, hint: "type a directory", value: ENTER_PATH });
|
|
6533
|
+
const picked = await tui.select(
|
|
6534
|
+
`${c3.bold}${gradientText(`Project on ${runner.name}`)}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
|
|
6535
|
+
items
|
|
6536
|
+
);
|
|
6537
|
+
if (!picked) return null;
|
|
6538
|
+
if (picked !== ENTER_PATH) return picked;
|
|
6539
|
+
const typed = await tui.prompt(
|
|
6540
|
+
`Directory on ${runner.name} (absolute, created if missing)`,
|
|
6541
|
+
"~/projects/my-app"
|
|
6542
|
+
);
|
|
6543
|
+
if (!typed) return null;
|
|
6544
|
+
const trimmed = typed.trim();
|
|
6545
|
+
if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
|
|
6546
|
+
tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
|
|
6547
|
+
return null;
|
|
6548
|
+
}
|
|
6549
|
+
return trimmed;
|
|
6550
|
+
}
|
|
5014
6551
|
function isSilentMessage(m) {
|
|
5015
6552
|
return m?.silent === true || m?.metadata?.silent === true;
|
|
5016
6553
|
}
|
|
@@ -5033,46 +6570,10 @@ async function summarizeThreads(api, threads) {
|
|
|
5033
6570
|
}
|
|
5034
6571
|
function subagentLabel(s, titles) {
|
|
5035
6572
|
const agentName = (s.agent_name || "").trim();
|
|
5036
|
-
const title = (s.title || "").trim() || titles.get(agentName) || (agentName ? agentName.replace(/[_-]+/g, " ").replace(/\b\w/g, (
|
|
6573
|
+
const title = (s.title || "").trim() || titles.get(agentName) || (agentName ? agentName.replace(/[_-]+/g, " ").replace(/\b\w/g, (c4) => c4.toUpperCase()) : "Subagent");
|
|
5037
6574
|
const tagged = (s.threadName || "").trim();
|
|
5038
6575
|
return tagged ? `${title} \xB7 ${tagged}` : title;
|
|
5039
6576
|
}
|
|
5040
|
-
async function deviceLogin(endpoint) {
|
|
5041
|
-
const start = await fetch(`${endpoint}/api/auth/device/start`, { method: "POST" });
|
|
5042
|
-
if (!start.ok) throw new Error(`This instance does not support browser sign-in (HTTP ${start.status}). Paste an API token instead.`);
|
|
5043
|
-
const info = await start.json();
|
|
5044
|
-
stdout.write(`${c.dim}Opening your browser to sign in. If it doesn't open, visit:${c.reset}
|
|
5045
|
-
`);
|
|
5046
|
-
stdout.write(`
|
|
5047
|
-
${c.teal}${info.verify_url}${c.reset}
|
|
5048
|
-
|
|
5049
|
-
`);
|
|
5050
|
-
stdout.write(`${c.dim}Waiting for sign-in to complete\u2026 (Ctrl-C to cancel)${c.reset}
|
|
5051
|
-
`);
|
|
5052
|
-
openUrl(info.verify_url);
|
|
5053
|
-
const deadline = Date.now() + (info.expires_in ?? 600) * 1e3;
|
|
5054
|
-
const interval = Math.max(2, info.interval ?? 2) * 1e3;
|
|
5055
|
-
while (Date.now() < deadline) {
|
|
5056
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
5057
|
-
const res = await fetch(info.poll_url).catch(() => null);
|
|
5058
|
-
if (!res) continue;
|
|
5059
|
-
if (res.status === 404) throw new Error("The sign-in link expired. Try again.");
|
|
5060
|
-
const body = await res.json().catch(() => ({}));
|
|
5061
|
-
if (body.status === "approved" && body.token) return body.token;
|
|
5062
|
-
if (body.status === "denied") throw new Error("Sign-in was denied in the browser.");
|
|
5063
|
-
}
|
|
5064
|
-
throw new Error("Timed out waiting for browser approval. Try again.");
|
|
5065
|
-
}
|
|
5066
|
-
function openUrl(url) {
|
|
5067
|
-
const platform = process.platform;
|
|
5068
|
-
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
5069
|
-
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
5070
|
-
try {
|
|
5071
|
-
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
5072
|
-
child.unref();
|
|
5073
|
-
} catch {
|
|
5074
|
-
}
|
|
5075
|
-
}
|
|
5076
6577
|
function relativeTime(unixSeconds) {
|
|
5077
6578
|
const diff = Date.now() / 1e3 - unixSeconds;
|
|
5078
6579
|
if (diff < 60) return "just now";
|
|
@@ -5115,8 +6616,8 @@ async function printHistory(api, threadId, tui) {
|
|
|
5115
6616
|
const convo = msgs.filter((m) => m.role === "user" || m.role === "assistant" && messageText(m.content).trim()).sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
|
|
5116
6617
|
if (!convo.length) return;
|
|
5117
6618
|
const shown = convo.slice(-24);
|
|
5118
|
-
tui.print(`${
|
|
5119
|
-
if (shown.length < convo.length) tui.print(`${
|
|
6619
|
+
tui.print(`${c3.dim}\u2500\u2500 resuming session \xB7 ${convo.length} message${convo.length === 1 ? "" : "s"} \u2500\u2500${c3.reset}`);
|
|
6620
|
+
if (shown.length < convo.length) tui.print(`${c3.dim} \u2026 earlier messages omitted${c3.reset}`);
|
|
5120
6621
|
for (const m of shown) {
|
|
5121
6622
|
const text = messageText(m.content).trim();
|
|
5122
6623
|
if (!text) continue;
|
|
@@ -5124,12 +6625,16 @@ async function printHistory(api, threadId, tui) {
|
|
|
5124
6625
|
else printAssistant(tui, text);
|
|
5125
6626
|
}
|
|
5126
6627
|
}
|
|
5127
|
-
async function runInteractive(tui, api, threadId, projectDir, machine, resumed, historySeedThreadId) {
|
|
6628
|
+
async function runInteractive(tui, api, threadId, projectDir, machine, resumed, session, historySeedThreadId) {
|
|
6629
|
+
const remote = session.mode === "remote";
|
|
6630
|
+
const runnerName = session.runner?.name ?? "the remote machine";
|
|
5128
6631
|
const registry = new ProcessRegistry(api, threadId, machine);
|
|
5129
|
-
const mcp = new McpManager(projectDir);
|
|
5130
|
-
const publishMcpCatalog = () =>
|
|
5131
|
-
|
|
5132
|
-
|
|
6632
|
+
const mcp = remote ? null : new McpManager(projectDir);
|
|
6633
|
+
const publishMcpCatalog = () => {
|
|
6634
|
+
if (mcp) void api.kvSet(threadId, "mcp_catalog", mcp.catalog()).catch(() => {
|
|
6635
|
+
});
|
|
6636
|
+
};
|
|
6637
|
+
const host = new HostTools(projectDir, registry, threadId, machine, mcp ?? void 0, publishMcpCatalog, api);
|
|
5133
6638
|
const refreshBgCount = () => {
|
|
5134
6639
|
void registry.runningCount().then((n) => tui.setBackgroundCount(n)).catch(() => {
|
|
5135
6640
|
});
|
|
@@ -5147,8 +6652,6 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
|
|
|
5147
6652
|
saveApprovals(api, threadId, perm);
|
|
5148
6653
|
});
|
|
5149
6654
|
saveApprovals(api, threadId, perm);
|
|
5150
|
-
void api.kvSet(threadId, "session_info", { cwd: projectDir, machine }).catch(() => {
|
|
5151
|
-
});
|
|
5152
6655
|
const attaching = startLoader("Attaching to thread");
|
|
5153
6656
|
let busy = false;
|
|
5154
6657
|
let interrupting = false;
|
|
@@ -5169,42 +6672,76 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
|
|
|
5169
6672
|
for (const v of activeSteps.values()) label = v;
|
|
5170
6673
|
tui.setStep(label, liveOut);
|
|
5171
6674
|
};
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
6675
|
+
let claim = "if_stale";
|
|
6676
|
+
if (!remote) {
|
|
6677
|
+
const ownerRec = await api.kvGet(threadId, "execution_owner").catch(() => null);
|
|
6678
|
+
const ownerDaemonMachine = machineIdFromDaemonClientId(ownerRec?.client_id);
|
|
6679
|
+
if (ownerDaemonMachine === session.identity.machine_id) claim = "takeover";
|
|
6680
|
+
}
|
|
6681
|
+
const bridge = remote ? null : new Bridge(
|
|
6682
|
+
api,
|
|
6683
|
+
threadId,
|
|
6684
|
+
host,
|
|
6685
|
+
perm,
|
|
6686
|
+
{
|
|
6687
|
+
onActivity: (line, detail) => {
|
|
6688
|
+
tui.print(colorActivity(line));
|
|
6689
|
+
if (detail) for (const d of detail) tui.print(d);
|
|
6690
|
+
refreshBgCount();
|
|
6691
|
+
},
|
|
6692
|
+
onOwnership: (isOwner, owner) => {
|
|
6693
|
+
if (isOwner) {
|
|
6694
|
+
bridge?.setClaim("takeover");
|
|
6695
|
+
void api.kvSet(threadId, "session_info", { cwd: projectDir, machine }).catch(() => {
|
|
6696
|
+
});
|
|
6697
|
+
} else {
|
|
6698
|
+
tui.print(
|
|
6699
|
+
`${c3.yellow}\u26A0 Another client${owner?.client_name ? ` (${owner.client_name})` : ""} is executing tools for this session \u2014 this terminal is watching.${c3.reset}`
|
|
6700
|
+
);
|
|
6701
|
+
}
|
|
6702
|
+
},
|
|
6703
|
+
onClaimRefused: (reason) => {
|
|
6704
|
+
if (reason === "in_flight") {
|
|
6705
|
+
tui.print(
|
|
6706
|
+
`${c3.yellow}\u26A0 The session's current client is mid-operation \u2014 execution can't move here until it finishes. Watching for now.${c3.reset}`
|
|
6707
|
+
);
|
|
6708
|
+
}
|
|
6709
|
+
},
|
|
6710
|
+
onSuperseded: () => {
|
|
6711
|
+
tui.print(
|
|
6712
|
+
`${c3.yellow}\u26A0 Another Standard Code process on this machine took over this session \u2014 this terminal is watching.${c3.reset}`
|
|
6713
|
+
);
|
|
6714
|
+
},
|
|
6715
|
+
onStatus: (id, summary) => {
|
|
6716
|
+
if (summary) {
|
|
6717
|
+
if (!activeSteps.has(id)) activeSteps.set(id, summary);
|
|
6718
|
+
} else {
|
|
6719
|
+
activeSteps.delete(id);
|
|
6720
|
+
}
|
|
6721
|
+
refreshStatus();
|
|
6722
|
+
},
|
|
6723
|
+
onConnection: (state, attempt) => {
|
|
6724
|
+
if (state === "reconnecting") {
|
|
6725
|
+
if (attempt >= 4) tui.setConnected(false);
|
|
6726
|
+
} else {
|
|
6727
|
+
tui.setConnected(true);
|
|
6728
|
+
}
|
|
6729
|
+
},
|
|
6730
|
+
requestApproval: async (req, summary, risk) => {
|
|
6731
|
+
return tui.approval(
|
|
6732
|
+
`${summary}${req.requestPermission ? `
|
|
6733
|
+
why: ${req.requestPermission}` : ""}`,
|
|
6734
|
+
risk
|
|
5202
6735
|
);
|
|
5203
|
-
return { choice: "deny", reason: reason ?? void 0 };
|
|
5204
6736
|
}
|
|
5205
|
-
|
|
6737
|
+
},
|
|
6738
|
+
{
|
|
6739
|
+
clientId: interactiveClientId(session.identity),
|
|
6740
|
+
clientName: `${machineDisplayName(session.identity)} (terminal)`,
|
|
6741
|
+
clientKind: "interactive",
|
|
6742
|
+
claim
|
|
5206
6743
|
}
|
|
5207
|
-
|
|
6744
|
+
);
|
|
5208
6745
|
const stream = new MessageStream(api, threadId, {
|
|
5209
6746
|
// Live streaming preview: answer text and (opt-in) internal reasoning feed
|
|
5210
6747
|
// the TUI's ephemeral preview; the committed message still renders from
|
|
@@ -5301,14 +6838,14 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
|
|
|
5301
6838
|
const sessionEnded = new Promise((r) => endSession = r);
|
|
5302
6839
|
const quit = async () => {
|
|
5303
6840
|
tui.end();
|
|
5304
|
-
const stopped2 = api.stop(threadId).catch(() => {
|
|
5305
|
-
});
|
|
6841
|
+
const stopped2 = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
|
|
6842
|
+
}) : Promise.resolve();
|
|
5306
6843
|
const procsStopped2 = host.stopAllLocalProcesses().catch(() => 0);
|
|
5307
|
-
bridge
|
|
6844
|
+
bridge?.close();
|
|
5308
6845
|
stream.close();
|
|
5309
6846
|
events.close();
|
|
5310
6847
|
subActivity.closeAll();
|
|
5311
|
-
mcp
|
|
6848
|
+
mcp?.closeAll();
|
|
5312
6849
|
const [, killed2] = await Promise.race([
|
|
5313
6850
|
Promise.all([stopped2, procsStopped2]),
|
|
5314
6851
|
new Promise((r) => setTimeout(() => r([void 0, 0]), 1500))
|
|
@@ -5320,21 +6857,28 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
|
|
|
5320
6857
|
const logout = async () => {
|
|
5321
6858
|
deleteCredential(api.origin);
|
|
5322
6859
|
const instanceHost = api.origin.replace(/^https?:\/\//, "");
|
|
5323
|
-
tui.print(`${
|
|
6860
|
+
tui.print(`${c3.gray}Signed out \u2014 removed the saved token for ${c3.teal}${instanceHost}${c3.reset}${c3.gray}. Run standardcode to sign in again.${c3.reset}`);
|
|
5324
6861
|
await quit();
|
|
5325
6862
|
};
|
|
5326
6863
|
const bgMgr = {
|
|
5327
6864
|
list: () => registry.list(),
|
|
5328
6865
|
stop: async (id) => {
|
|
6866
|
+
if (remote) {
|
|
6867
|
+
tui.print(
|
|
6868
|
+
`${c3.gray}That process runs on ${runnerName} \u2014 ask the agent to stop it (it manages processes there).${c3.reset}`
|
|
6869
|
+
);
|
|
6870
|
+
return;
|
|
6871
|
+
}
|
|
5329
6872
|
await host.execute("background_process", { action: "stop", id });
|
|
5330
6873
|
refreshBgCount();
|
|
5331
6874
|
}
|
|
5332
6875
|
};
|
|
5333
6876
|
const mcpCtl = {
|
|
5334
6877
|
configured: () => listMcpServers(),
|
|
5335
|
-
connectedNames: () => mcp
|
|
5336
|
-
catalog: () => mcp
|
|
6878
|
+
connectedNames: () => mcp?.connectedNames() ?? [],
|
|
6879
|
+
catalog: () => mcp?.catalog() ?? { servers: [] },
|
|
5337
6880
|
connect: async (cfg) => {
|
|
6881
|
+
if (!mcp) return { ok: false, error: "MCP servers run on the session's machine." };
|
|
5338
6882
|
try {
|
|
5339
6883
|
const client = await mcp.connect(cfg);
|
|
5340
6884
|
publishMcpCatalog();
|
|
@@ -5345,7 +6889,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
|
|
|
5345
6889
|
}
|
|
5346
6890
|
},
|
|
5347
6891
|
disconnect: (name) => {
|
|
5348
|
-
mcp
|
|
6892
|
+
mcp?.disconnect(name);
|
|
5349
6893
|
publishMcpCatalog();
|
|
5350
6894
|
},
|
|
5351
6895
|
add: async (cfg) => {
|
|
@@ -5361,7 +6905,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
|
|
|
5361
6905
|
);
|
|
5362
6906
|
},
|
|
5363
6907
|
remove: (name) => {
|
|
5364
|
-
mcp
|
|
6908
|
+
mcp?.disconnect(name);
|
|
5365
6909
|
removeMcpServer(name);
|
|
5366
6910
|
publishMcpCatalog();
|
|
5367
6911
|
},
|
|
@@ -5380,7 +6924,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
|
|
|
5380
6924
|
const n = (pendingSent.get(key) ?? 1) - 1;
|
|
5381
6925
|
if (n > 0) pendingSent.set(key, n);
|
|
5382
6926
|
else pendingSent.delete(key);
|
|
5383
|
-
tui.print(`${
|
|
6927
|
+
tui.print(`${c3.dim}failed to send: ${e instanceof Error ? e.message : String(e)}${c3.reset}`);
|
|
5384
6928
|
return;
|
|
5385
6929
|
}
|
|
5386
6930
|
interrupting = false;
|
|
@@ -5397,16 +6941,16 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
|
|
|
5397
6941
|
try {
|
|
5398
6942
|
await api.compact(threadId);
|
|
5399
6943
|
} catch (err) {
|
|
5400
|
-
tui.print(`${
|
|
6944
|
+
tui.print(`${c3.red}\u2717${c3.reset} couldn't start compaction: ${err.message}`);
|
|
5401
6945
|
}
|
|
5402
6946
|
};
|
|
5403
6947
|
const runAccountCommand = async () => {
|
|
5404
|
-
tui.print(`${
|
|
6948
|
+
tui.print(`${c3.gray}Opening your account\u2026${c3.reset}`);
|
|
5405
6949
|
const link = await api.accountLink(threadId).catch(() => null);
|
|
5406
6950
|
const target = link?.url ?? "https://standardcode.ai/account";
|
|
5407
6951
|
openUrl(target);
|
|
5408
6952
|
tui.print(
|
|
5409
|
-
link?.preauthed ? `${
|
|
6953
|
+
link?.preauthed ? `${c3.gray}\u2192 account dashboard opened in your browser (signed in)${c3.reset}` : `${c3.gray}\u2192 opened ${target} \u2014 sign in with your account email${c3.reset}`
|
|
5410
6954
|
);
|
|
5411
6955
|
};
|
|
5412
6956
|
const ordinal = (n) => {
|
|
@@ -5417,24 +6961,24 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
|
|
|
5417
6961
|
const renderUpgradePanel = (q) => {
|
|
5418
6962
|
const dots = [];
|
|
5419
6963
|
for (let i = 0; i < q.max; i++) {
|
|
5420
|
-
if (i < q.current) dots.push(`${
|
|
5421
|
-
else if (i === q.current) dots.push(`${
|
|
5422
|
-
else dots.push(`${
|
|
6964
|
+
if (i < q.current) dots.push(`${c3.teal}\u25CF${c3.reset}`);
|
|
6965
|
+
else if (i === q.current) dots.push(`${c3.bold}${gradientText("\uFF0B")}${c3.reset}`);
|
|
6966
|
+
else dots.push(`${c3.dim}\xB7${c3.reset}`);
|
|
5423
6967
|
}
|
|
5424
6968
|
const cost = fmtCost(q);
|
|
5425
6969
|
const lines = [
|
|
5426
6970
|
"",
|
|
5427
|
-
`${
|
|
6971
|
+
`${c3.bold}${gradientText("\u2726 Add a parallel session")}${c3.reset}`,
|
|
5428
6972
|
"",
|
|
5429
|
-
`${dots.join(" ")} ${
|
|
6973
|
+
`${dots.join(" ")} ${c3.dim}${q.current} of ${q.current} session${q.current === 1 ? "" : "s"} in use${c3.reset}`
|
|
5430
6974
|
];
|
|
5431
6975
|
if (q.ends_trial) {
|
|
5432
6976
|
lines.push(
|
|
5433
|
-
`${
|
|
5434
|
-
`${
|
|
6977
|
+
`${c3.yellow}Your $5 trial covers 1 session. Adding a ${ordinal(q.sessions)} ends the trial${c3.reset}`,
|
|
6978
|
+
`${c3.yellow}and starts your $49/mo plan now${cost ? ` \u2014 ${c3.bold}${cost} charged today${c3.reset}${c3.yellow}` : ""}.${c3.reset}`
|
|
5435
6979
|
);
|
|
5436
6980
|
} else if (cost) {
|
|
5437
|
-
lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 ${
|
|
6981
|
+
lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 ${c3.bold}${cost} charged now${c3.reset}.`);
|
|
5438
6982
|
} else {
|
|
5439
6983
|
lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 prorated on your next invoice.`);
|
|
5440
6984
|
}
|
|
@@ -5447,7 +6991,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
|
|
|
5447
6991
|
try {
|
|
5448
6992
|
if (opts.auto) {
|
|
5449
6993
|
tui.print(
|
|
5450
|
-
`${
|
|
6994
|
+
`${c3.yellow}You're out of simultaneous sessions \u2014 another Standard Code session is using your slot.${c3.reset}`
|
|
5451
6995
|
);
|
|
5452
6996
|
}
|
|
5453
6997
|
const quote = await api.sessionsQuote(threadId);
|
|
@@ -5455,16 +6999,16 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
|
|
|
5455
6999
|
const link = await api.accountLink(threadId).catch(() => null);
|
|
5456
7000
|
const target = link?.url ?? "https://standardcode.ai/account";
|
|
5457
7001
|
tui.print(
|
|
5458
|
-
`${
|
|
7002
|
+
`${c3.gray}Close the other session (its slot frees within ~90s) \u2014 or add another simultaneous session to your plan, then resend your message.${c3.reset}`
|
|
5459
7003
|
);
|
|
5460
7004
|
openUrl(target);
|
|
5461
|
-
tui.print(`${
|
|
7005
|
+
tui.print(`${c3.gray}\u2192 opened ${target} to manage your plan${c3.reset}`);
|
|
5462
7006
|
return;
|
|
5463
7007
|
}
|
|
5464
7008
|
if (quote.current >= quote.max) {
|
|
5465
7009
|
tui.print(
|
|
5466
|
-
`${
|
|
5467
|
-
${
|
|
7010
|
+
`${c3.yellow}You're at the maximum of ${quote.max} parallel session${quote.max === 1 ? "" : "s"}.${c3.reset}
|
|
7011
|
+
${c3.gray}Close another session (its slot frees within ~90s), then resend your message.${c3.reset}`
|
|
5468
7012
|
);
|
|
5469
7013
|
return;
|
|
5470
7014
|
}
|
|
@@ -5476,25 +7020,25 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5476
7020
|
{ label: "Not now", value: "no" }
|
|
5477
7021
|
]);
|
|
5478
7022
|
if (choice !== "go") {
|
|
5479
|
-
tui.print(`${
|
|
7023
|
+
tui.print(`${c3.gray}No change made \u2014 you can upgrade anytime with /upgrade.${c3.reset}`);
|
|
5480
7024
|
return;
|
|
5481
7025
|
}
|
|
5482
|
-
tui.print(`${
|
|
7026
|
+
tui.print(`${c3.gray}Applying\u2026${c3.reset}`);
|
|
5483
7027
|
let applied;
|
|
5484
7028
|
try {
|
|
5485
7029
|
applied = await api.sessionsUpgrade(threadId, quote.sessions);
|
|
5486
7030
|
} catch (e) {
|
|
5487
|
-
tui.print(`${
|
|
7031
|
+
tui.print(`${c3.red}\u2717${c3.reset} Upgrade failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
5488
7032
|
return;
|
|
5489
7033
|
}
|
|
5490
7034
|
if (!applied?.ok) {
|
|
5491
|
-
tui.print(`${
|
|
7035
|
+
tui.print(`${c3.red}\u2717${c3.reset} Upgrade failed: ${applied?.error ?? "unknown error"}`);
|
|
5492
7036
|
return;
|
|
5493
7037
|
}
|
|
5494
7038
|
const n = applied.sessions ?? quote.sessions;
|
|
5495
|
-
tui.print(`${
|
|
7039
|
+
tui.print(`${c3.green}\u2713${c3.reset} ${c3.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c3.reset}`);
|
|
5496
7040
|
if (opts.auto && lastSent) {
|
|
5497
|
-
tui.print(`${
|
|
7041
|
+
tui.print(`${c3.gray}Continuing\u2026${c3.reset}`);
|
|
5498
7042
|
await sendNow(lastSent.text, lastSent.images);
|
|
5499
7043
|
}
|
|
5500
7044
|
} finally {
|
|
@@ -5538,14 +7082,24 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5538
7082
|
},
|
|
5539
7083
|
run: () => runApprovalsMenu(tui, perm, () => saveApprovals(api, threadId, perm))
|
|
5540
7084
|
},
|
|
7085
|
+
// MCP servers are hosted by whichever client executes the tools — hide
|
|
7086
|
+
// the local MCP manager in a remote session (the daemon hosts them there).
|
|
7087
|
+
...remote ? [] : [
|
|
7088
|
+
{
|
|
7089
|
+
name: "mcp",
|
|
7090
|
+
label: "MCP servers",
|
|
7091
|
+
hint: () => {
|
|
7092
|
+
const n = mcpCtl.connectedNames().length;
|
|
7093
|
+
return n ? `${n} connected` : "none";
|
|
7094
|
+
},
|
|
7095
|
+
run: () => runMcpMenu(tui, mcpCtl)
|
|
7096
|
+
}
|
|
7097
|
+
],
|
|
5541
7098
|
{
|
|
5542
|
-
name: "
|
|
5543
|
-
label: "
|
|
5544
|
-
hint: () =>
|
|
5545
|
-
|
|
5546
|
-
return n ? `${n} connected` : "none";
|
|
5547
|
-
},
|
|
5548
|
-
run: () => runMcpMenu(tui, mcpCtl)
|
|
7099
|
+
name: "daemon",
|
|
7100
|
+
label: "Machine daemon",
|
|
7101
|
+
hint: () => serviceStatus().installed ? "installed on this machine" : "not installed here",
|
|
7102
|
+
run: () => showDaemonInfo(tui, session)
|
|
5549
7103
|
},
|
|
5550
7104
|
{
|
|
5551
7105
|
name: "skills",
|
|
@@ -5579,20 +7133,20 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5579
7133
|
editingQueued = false;
|
|
5580
7134
|
queued.push({ text, images });
|
|
5581
7135
|
tui.setQueuedCount(queued.length);
|
|
5582
|
-
tui.print(`${
|
|
7136
|
+
tui.print(`${c3.gray}\u23F3 queued:${c3.reset} ${text}`);
|
|
5583
7137
|
return;
|
|
5584
7138
|
}
|
|
5585
7139
|
if (busy) {
|
|
5586
7140
|
queued.push({ text, images });
|
|
5587
7141
|
tui.setQueuedCount(queued.length);
|
|
5588
|
-
tui.print(`${
|
|
7142
|
+
tui.print(`${c3.gray}\u23F3 queued:${c3.reset} ${text} ${c3.dim}(esc to steer now)${c3.reset}`);
|
|
5589
7143
|
} else {
|
|
5590
7144
|
void sendNow(text, images);
|
|
5591
7145
|
}
|
|
5592
7146
|
};
|
|
5593
7147
|
tui.onInterrupt = () => {
|
|
5594
7148
|
if (queued.length > 0) {
|
|
5595
|
-
tui.print(`${
|
|
7149
|
+
tui.print(`${c3.yellow}\u21AA steering \u2014 stopping current work and sending your message\u2026${c3.reset}`);
|
|
5596
7150
|
void api.stop(threadId).catch(() => {
|
|
5597
7151
|
}).then(() => flushQueued());
|
|
5598
7152
|
} else if (busy) {
|
|
@@ -5602,7 +7156,7 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5602
7156
|
liveOut = 0;
|
|
5603
7157
|
tui.setWorking(false);
|
|
5604
7158
|
refreshStatus();
|
|
5605
|
-
tui.print(`${
|
|
7159
|
+
tui.print(`${c3.yellow}[interrupted by user]${c3.reset}`);
|
|
5606
7160
|
void api.stop(threadId).catch(() => {
|
|
5607
7161
|
});
|
|
5608
7162
|
}
|
|
@@ -5620,15 +7174,27 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5620
7174
|
return true;
|
|
5621
7175
|
};
|
|
5622
7176
|
events.connect();
|
|
5623
|
-
await Promise.all([bridge.connect(), stream.connect()]);
|
|
7177
|
+
await Promise.all([...bridge ? [bridge.connect()] : [], stream.connect()]);
|
|
7178
|
+
const ownsExecution = bridge ? await bridge.whenOwnershipKnown(3e3) : false;
|
|
5624
7179
|
void api.getGoal(threadId).then((g) => tui.setGoal(g)).catch(() => {
|
|
5625
7180
|
});
|
|
5626
7181
|
attaching.stop();
|
|
5627
|
-
tui.banner(
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
7182
|
+
tui.banner(
|
|
7183
|
+
remote ? [
|
|
7184
|
+
`${c3.bold}${c3.magenta}Standard Code${c3.reset} ${c3.dim}\u2014 coding agent${c3.reset}`,
|
|
7185
|
+
`${c3.gray}project:${c3.reset} ${session.remotePath ?? "?"} ${c3.teal}on ${runnerName}${c3.reset}`,
|
|
7186
|
+
`${c3.gray}runs on:${c3.reset} ${runnerName} ${c3.dim}(daemon executes tools; you're watching from ${machine})${c3.reset} ${c3.gray}thread:${c3.reset} ${threadId.slice(0, 8)}`
|
|
7187
|
+
] : [
|
|
7188
|
+
`${c3.bold}${c3.magenta}Standard Code${c3.reset} ${c3.dim}\u2014 coding agent${c3.reset}`,
|
|
7189
|
+
`${c3.gray}project:${c3.reset} ${projectDir}`,
|
|
7190
|
+
`${c3.gray}machine:${c3.reset} ${machine} ${c3.gray}thread:${c3.reset} ${threadId.slice(0, 8)}`
|
|
7191
|
+
]
|
|
7192
|
+
);
|
|
7193
|
+
if (!remote && session.suggestDaemonInstall && process.platform !== "win32" && !serviceStatus().installed) {
|
|
7194
|
+
tui.print(
|
|
7195
|
+
`${c3.dim}Tip: install the always-on daemon (${c3.reset}standardcode daemon install${c3.dim}) to start sessions on this machine from anywhere.${c3.reset}`
|
|
7196
|
+
);
|
|
7197
|
+
}
|
|
5632
7198
|
if (resumed) await printHistory(api, threadId, tui);
|
|
5633
7199
|
try {
|
|
5634
7200
|
(await api.getMessages(threadId, 200)).forEach((m) => shownIds.add(m.id));
|
|
@@ -5637,22 +7203,50 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5637
7203
|
const runningProcs = (await registry.list()).filter((p) => p.status === "running");
|
|
5638
7204
|
if (runningProcs.length) {
|
|
5639
7205
|
tui.print(
|
|
5640
|
-
`${
|
|
7206
|
+
`${c3.cyan}\u2699 ${runningProcs.length} background process${runningProcs.length === 1 ? "" : "es"} running:${c3.reset}`
|
|
5641
7207
|
);
|
|
5642
|
-
for (const p of runningProcs) tui.print(`${
|
|
7208
|
+
for (const p of runningProcs) tui.print(`${c3.gray} ${p.id} ${p.description || p.command}${c3.reset}`);
|
|
5643
7209
|
}
|
|
5644
7210
|
refreshBgCount();
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
const
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
7211
|
+
if (!remote && ownsExecution) {
|
|
7212
|
+
const enabledServers = listMcpServers().filter((s) => s.enabled);
|
|
7213
|
+
for (const s of enabledServers) {
|
|
7214
|
+
const res = await mcpCtl.connect(s);
|
|
7215
|
+
if (res.ok) {
|
|
7216
|
+
tui.print(`${c3.cyan}\u26A1 MCP "${s.name}" connected${c3.reset} ${c3.gray}(${res.tools} tool${res.tools === 1 ? "" : "s"})${c3.reset}`);
|
|
7217
|
+
} else {
|
|
7218
|
+
tui.print(`${c3.red}\u26A0 MCP "${s.name}" failed:${c3.reset} ${c3.gray}${res.error}${c3.reset}`);
|
|
7219
|
+
}
|
|
5652
7220
|
}
|
|
7221
|
+
publishMcpCatalog();
|
|
5653
7222
|
}
|
|
5654
|
-
publishMcpCatalog();
|
|
5655
7223
|
tui.start();
|
|
7224
|
+
const answeredApprovals = /* @__PURE__ */ new Set();
|
|
7225
|
+
let approvalPromptOpen = false;
|
|
7226
|
+
const relayApprovals = async () => {
|
|
7227
|
+
const watching = remote || (bridge ? !bridge.isOwner : false);
|
|
7228
|
+
if (!watching || approvalPromptOpen) return;
|
|
7229
|
+
const request = parseApprovalRequest(await api.kvGet(threadId, APPROVAL_REQUEST_KEY));
|
|
7230
|
+
if (!request || answeredApprovals.has(request.tool_call_id)) return;
|
|
7231
|
+
approvalPromptOpen = true;
|
|
7232
|
+
try {
|
|
7233
|
+
const { choice, reason } = await tui.approval(
|
|
7234
|
+
`${request.summary}${request.permission ? `
|
|
7235
|
+
${c3.bold}why: ${request.permission}${c3.reset}` : ""}
|
|
7236
|
+
${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
7237
|
+
request.risk
|
|
7238
|
+
);
|
|
7239
|
+
answeredApprovals.add(request.tool_call_id);
|
|
7240
|
+
await writeApprovalResponse(api, threadId, {
|
|
7241
|
+
tool_call_id: request.tool_call_id,
|
|
7242
|
+
choice,
|
|
7243
|
+
reason,
|
|
7244
|
+
decided_at: Date.now()
|
|
7245
|
+
});
|
|
7246
|
+
} finally {
|
|
7247
|
+
approvalPromptOpen = false;
|
|
7248
|
+
}
|
|
7249
|
+
};
|
|
5656
7250
|
const poll = async () => {
|
|
5657
7251
|
let msgs;
|
|
5658
7252
|
try {
|
|
@@ -5671,7 +7265,7 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5671
7265
|
continue;
|
|
5672
7266
|
}
|
|
5673
7267
|
if (m.role === "assistant" && text) printAssistant(tui, text);
|
|
5674
|
-
else if (m.role === "system" && text) tui.print(`${
|
|
7268
|
+
else if (m.role === "system" && text) tui.print(`${c3.dim}${text}${c3.reset}`);
|
|
5675
7269
|
else if (m.role === "user" && text) {
|
|
5676
7270
|
const pending = pendingSent.get(text) ?? 0;
|
|
5677
7271
|
if (pending > 0) {
|
|
@@ -5697,6 +7291,8 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5697
7291
|
if (queued.length > 0 && !editingQueued) await flushQueued();
|
|
5698
7292
|
}
|
|
5699
7293
|
refreshBgCount();
|
|
7294
|
+
void relayApprovals().catch(() => {
|
|
7295
|
+
});
|
|
5700
7296
|
try {
|
|
5701
7297
|
const logs = await api.getLogs(threadId, 100);
|
|
5702
7298
|
let landed = 0;
|
|
@@ -5733,14 +7329,14 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5733
7329
|
await sessionEnded;
|
|
5734
7330
|
clearInterval(pollTimer);
|
|
5735
7331
|
clearInterval(heartbeatPoll);
|
|
5736
|
-
const stopped = api.stop(threadId).catch(() => {
|
|
5737
|
-
});
|
|
7332
|
+
const stopped = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
|
|
7333
|
+
}) : Promise.resolve();
|
|
5738
7334
|
const procsStopped = host.stopAllLocalProcesses().catch(() => 0);
|
|
5739
|
-
bridge
|
|
7335
|
+
bridge?.close();
|
|
5740
7336
|
stream.close();
|
|
5741
7337
|
events.close();
|
|
5742
7338
|
subActivity.closeAll();
|
|
5743
|
-
mcp
|
|
7339
|
+
mcp?.closeAll();
|
|
5744
7340
|
const [, killed] = await Promise.race([
|
|
5745
7341
|
Promise.all([stopped, procsStopped]),
|
|
5746
7342
|
new Promise((r) => setTimeout(() => r([void 0, 0]), 1500))
|
|
@@ -5753,16 +7349,16 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5753
7349
|
tui.setStep(null, 0);
|
|
5754
7350
|
tui.setBackgroundCount(0);
|
|
5755
7351
|
if (killed > 0) {
|
|
5756
|
-
tui.print(`${
|
|
7352
|
+
tui.print(`${c3.cyan}\u2699${c3.reset} Stopped ${killed} background process${killed === 1 ? "" : "es"}.`);
|
|
5757
7353
|
}
|
|
5758
|
-
tui.print(`${
|
|
7354
|
+
tui.print(`${c3.dim}\u2500\u2500 conversation cleared \u2014 starting a fresh session \u2500\u2500${c3.reset}`);
|
|
5759
7355
|
}
|
|
5760
7356
|
async function runSkillsMenu(tui, skills) {
|
|
5761
7357
|
let list;
|
|
5762
7358
|
try {
|
|
5763
7359
|
list = await skills.list();
|
|
5764
7360
|
} catch (e) {
|
|
5765
|
-
tui.print(`${
|
|
7361
|
+
tui.print(`${c3.red}\u2717 couldn't load skills:${c3.reset} ${c3.gray}${e instanceof Error ? e.message : String(e)}${c3.reset}`);
|
|
5766
7362
|
return;
|
|
5767
7363
|
}
|
|
5768
7364
|
const INSTALL = "__install__";
|
|
@@ -5773,7 +7369,7 @@ async function runSkillsMenu(tui, skills) {
|
|
|
5773
7369
|
}));
|
|
5774
7370
|
items.push({ label: "\uFF0B Install a skill\u2026", hint: "find & install", value: INSTALL });
|
|
5775
7371
|
const picked = await tui.select(
|
|
5776
|
-
`${
|
|
7372
|
+
`${c3.bold}Agent skills${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c3.reset}`,
|
|
5777
7373
|
items
|
|
5778
7374
|
);
|
|
5779
7375
|
if (!picked) return;
|
|
@@ -5786,8 +7382,8 @@ async function runSkillsMenu(tui, skills) {
|
|
|
5786
7382
|
return;
|
|
5787
7383
|
}
|
|
5788
7384
|
const skill = list.find((s) => s.name === picked);
|
|
5789
|
-
tui.print(`${
|
|
5790
|
-
const action = await tui.select(`${
|
|
7385
|
+
tui.print(`${c3.cyan}${skill.name}${c3.reset}${skill.version ? ` ${c3.dim}v${skill.version}${c3.reset}` : ""} ${c3.gray}\u2014 ${skill.description}${c3.reset}`);
|
|
7386
|
+
const action = await tui.select(`${c3.bold}${picked}${c3.reset}`, [
|
|
5791
7387
|
skill.enabled ? { label: "Disable (hide from the agent)", value: "disable" } : { label: "Enable", value: "enable" },
|
|
5792
7388
|
{ label: "View files", value: "files" },
|
|
5793
7389
|
{ label: "Remove this skill", value: "remove" },
|
|
@@ -5796,20 +7392,20 @@ async function runSkillsMenu(tui, skills) {
|
|
|
5796
7392
|
try {
|
|
5797
7393
|
if (action === "enable" || action === "disable") {
|
|
5798
7394
|
await skills.setEnabled(picked, action === "enable");
|
|
5799
|
-
tui.print(`${
|
|
7395
|
+
tui.print(`${c3.gray}${action}d ${picked}${c3.reset}`);
|
|
5800
7396
|
} else if (action === "files") {
|
|
5801
|
-
for (const f of skill.files) tui.print(` ${
|
|
7397
|
+
for (const f of skill.files) tui.print(` ${c3.gray}${f}${c3.reset}`);
|
|
5802
7398
|
} else if (action === "remove") {
|
|
5803
7399
|
await skills.remove(picked);
|
|
5804
|
-
tui.print(`${
|
|
7400
|
+
tui.print(`${c3.gray}removed ${picked}${c3.reset}`);
|
|
5805
7401
|
}
|
|
5806
7402
|
} catch (e) {
|
|
5807
|
-
tui.print(`${
|
|
7403
|
+
tui.print(`${c3.red}\u2717 ${e instanceof Error ? e.message : String(e)}${c3.reset}`);
|
|
5808
7404
|
}
|
|
5809
7405
|
}
|
|
5810
7406
|
async function runLevelMenu(tui, perm) {
|
|
5811
7407
|
const picked = await tui.select(
|
|
5812
|
-
`${
|
|
7408
|
+
`${c3.bold}Auto-accept level${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 shift-tab cycles)${c3.reset}`,
|
|
5813
7409
|
LEVELS.map((l) => ({
|
|
5814
7410
|
label: levelLabel(l),
|
|
5815
7411
|
hint: l === tui.level ? "current" : "",
|
|
@@ -5821,28 +7417,51 @@ async function runLevelMenu(tui, perm) {
|
|
|
5821
7417
|
perm.level = picked;
|
|
5822
7418
|
}
|
|
5823
7419
|
}
|
|
7420
|
+
function showDaemonInfo(tui, session) {
|
|
7421
|
+
if (session.mode === "remote" && session.runner) {
|
|
7422
|
+
tui.print(
|
|
7423
|
+
`${c3.gray}This session runs on${c3.reset} ${c3.bold}${session.runner.name}${c3.reset} ${c3.gray}(${session.runner.hostname}) \u2014 its daemon executes the tools.${c3.reset}`
|
|
7424
|
+
);
|
|
7425
|
+
} else {
|
|
7426
|
+
tui.print(`${c3.gray}This session runs on this machine.${c3.reset}`);
|
|
7427
|
+
}
|
|
7428
|
+
const status = serviceStatus();
|
|
7429
|
+
tui.print(
|
|
7430
|
+
`${c3.gray}Daemon on this machine:${c3.reset} ${status.installed ? status.detail : "not installed"}`
|
|
7431
|
+
);
|
|
7432
|
+
if (!status.installed) {
|
|
7433
|
+
tui.print(
|
|
7434
|
+
`${c3.gray}Install it to start sessions on this machine from anywhere:${c3.reset} ${c3.bold}standardcode daemon install${c3.reset}`
|
|
7435
|
+
);
|
|
7436
|
+
tui.print(
|
|
7437
|
+
`${c3.dim}The daemon keeps running after you close the terminal \u2014 it self-restarts, self-updates, and executes sessions you start from other machines.${c3.reset}`
|
|
7438
|
+
);
|
|
7439
|
+
} else {
|
|
7440
|
+
tui.print(`${c3.dim}Manage it with standardcode daemon status | uninstall | add-project <path>.${c3.reset}`);
|
|
7441
|
+
}
|
|
7442
|
+
}
|
|
5824
7443
|
function showKeybindings(tui) {
|
|
5825
|
-
tui.print(`${
|
|
5826
|
-
tui.print(`${
|
|
5827
|
-
tui.print(`${
|
|
5828
|
-
tui.print(`${
|
|
5829
|
-
tui.print(`${
|
|
5830
|
-
tui.print(`${
|
|
5831
|
-
tui.print(`${
|
|
7444
|
+
tui.print(`${c3.gray}shortcuts:${c3.reset}`);
|
|
7445
|
+
tui.print(`${c3.gray} shift-tab${c3.reset} cycle auto-accept level (1\u20135)`);
|
|
7446
|
+
tui.print(`${c3.gray} /${c3.reset} open the command palette (type to filter)`);
|
|
7447
|
+
tui.print(`${c3.gray} ctrl-v${c3.reset} paste an image from the clipboard ([#Image 1])`);
|
|
7448
|
+
tui.print(`${c3.gray} \u2191 / \u2193${c3.reset} cycle past messages (on the input's top line)`);
|
|
7449
|
+
tui.print(`${c3.gray} \u2190${c3.reset} from the start of the input: select the [\u2699 n bg] badge (enter opens it)`);
|
|
7450
|
+
tui.print(`${c3.gray} ctrl-c${c3.reset} quit`);
|
|
5832
7451
|
}
|
|
5833
7452
|
async function runUpdateCommand(tui) {
|
|
5834
7453
|
const version = readVersion();
|
|
5835
7454
|
const result = await forceCheckForUpdate(version);
|
|
5836
7455
|
if (!result) {
|
|
5837
|
-
tui.print(`${
|
|
7456
|
+
tui.print(`${c3.green}\u2713${c3.reset} ${c3.gray}@standardagents/code${c3.reset} is up to date (v${version})`);
|
|
5838
7457
|
return;
|
|
5839
7458
|
}
|
|
5840
7459
|
const { latest } = result;
|
|
5841
7460
|
tui.print(`
|
|
5842
|
-
${
|
|
7461
|
+
${c3.yellow}\u27F3${c3.reset} Update available: ${c3.gray}v${version}${c3.reset} \u2192 ${c3.green}v${latest}${c3.reset}`);
|
|
5843
7462
|
const pm = detectPackageManager();
|
|
5844
7463
|
if (!pm) {
|
|
5845
|
-
tui.print(` ${
|
|
7464
|
+
tui.print(` ${c3.gray}This is a source checkout \u2014 pull the repo to update.${c3.reset}`);
|
|
5846
7465
|
return;
|
|
5847
7466
|
}
|
|
5848
7467
|
const { display } = updateCommand(pm);
|
|
@@ -5851,28 +7470,28 @@ async function runUpdateCommand(tui) {
|
|
|
5851
7470
|
{ label: "No, skip", value: "no" }
|
|
5852
7471
|
]);
|
|
5853
7472
|
if (choice === "yes") {
|
|
5854
|
-
tui.print(` ${
|
|
7473
|
+
tui.print(` ${c3.gray}Running ${display}\u2026${c3.reset}`);
|
|
5855
7474
|
const { ok, output: pmOutput } = await runUpdate(pm);
|
|
5856
7475
|
if (ok) {
|
|
5857
|
-
tui.print(` ${
|
|
7476
|
+
tui.print(` ${c3.green}\u2713${c3.reset} Updated to v${latest}. Restart to use the new version.`);
|
|
5858
7477
|
} else {
|
|
5859
|
-
tui.print(` ${
|
|
7478
|
+
tui.print(` ${c3.red}\u2717${c3.reset} Update failed:`);
|
|
5860
7479
|
for (const line of pmOutput.trim().split("\n").slice(-6)) {
|
|
5861
|
-
tui.print(` ${
|
|
7480
|
+
tui.print(` ${c3.dim}${line}${c3.reset}`);
|
|
5862
7481
|
}
|
|
5863
7482
|
}
|
|
5864
7483
|
} else {
|
|
5865
|
-
tui.print(` ${
|
|
7484
|
+
tui.print(` ${c3.gray}Skipped. Run /update later.${c3.reset}`);
|
|
5866
7485
|
}
|
|
5867
7486
|
}
|
|
5868
7487
|
async function runProcessMenu(tui, bg) {
|
|
5869
7488
|
const procs = await bg.list();
|
|
5870
7489
|
if (!procs.length) {
|
|
5871
|
-
tui.print(`${
|
|
7490
|
+
tui.print(`${c3.gray}No background processes for this session.${c3.reset}`);
|
|
5872
7491
|
return;
|
|
5873
7492
|
}
|
|
5874
7493
|
const items = procs.map((p) => {
|
|
5875
|
-
const status = p.status === "running" ? `${
|
|
7494
|
+
const status = p.status === "running" ? `${c3.green}running${c3.reset}` : `${c3.gray}${p.status}${typeof p.exitCode === "number" ? ` (exit ${p.exitCode})` : ""}${c3.reset}`;
|
|
5876
7495
|
return {
|
|
5877
7496
|
label: `${p.description || p.command}`,
|
|
5878
7497
|
hint: `${p.id} \xB7 ${status}`,
|
|
@@ -5880,22 +7499,22 @@ async function runProcessMenu(tui, bg) {
|
|
|
5880
7499
|
};
|
|
5881
7500
|
});
|
|
5882
7501
|
const picked = await tui.select(
|
|
5883
|
-
`${
|
|
7502
|
+
`${c3.bold}Background processes${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter to manage \xB7 esc to close)${c3.reset}`,
|
|
5884
7503
|
items
|
|
5885
7504
|
);
|
|
5886
7505
|
if (!picked) return;
|
|
5887
7506
|
const proc = procs.find((p) => p.id === picked);
|
|
5888
7507
|
if (!proc || proc.status !== "running") {
|
|
5889
|
-
tui.print(`${
|
|
7508
|
+
tui.print(`${c3.gray}${picked} is not running.${c3.reset}`);
|
|
5890
7509
|
return;
|
|
5891
7510
|
}
|
|
5892
|
-
const action = await tui.select(`${
|
|
7511
|
+
const action = await tui.select(`${c3.bold}${proc.description || proc.command}${c3.reset}`, [
|
|
5893
7512
|
{ label: "Stop this process", value: "stop" },
|
|
5894
7513
|
{ label: "Leave it running", value: "leave" }
|
|
5895
7514
|
]);
|
|
5896
7515
|
if (action === "stop") {
|
|
5897
7516
|
await bg.stop(picked);
|
|
5898
|
-
tui.print(`${
|
|
7517
|
+
tui.print(`${c3.gray}stopped ${picked}${c3.reset}`);
|
|
5899
7518
|
}
|
|
5900
7519
|
}
|
|
5901
7520
|
async function runApprovalsMenu(tui, perm, save) {
|
|
@@ -5903,7 +7522,7 @@ async function runApprovalsMenu(tui, perm, save) {
|
|
|
5903
7522
|
const risks = Array.from(perm.allowRisk).sort((a, b) => a - b);
|
|
5904
7523
|
if (!tools.length && !risks.length) {
|
|
5905
7524
|
tui.print(
|
|
5906
|
-
`${
|
|
7525
|
+
`${c3.gray}No pre-approved commands. At a permission prompt, choose "Always allow this tool" or "Allow all level N" to add some.${c3.reset}`
|
|
5907
7526
|
);
|
|
5908
7527
|
return;
|
|
5909
7528
|
}
|
|
@@ -5913,22 +7532,22 @@ async function runApprovalsMenu(tui, perm, save) {
|
|
|
5913
7532
|
{ label: "Clear all approvals", hint: "", value: "clear" }
|
|
5914
7533
|
];
|
|
5915
7534
|
const picked = await tui.select(
|
|
5916
|
-
`${
|
|
7535
|
+
`${c3.bold}Approved commands${c3.reset} ${c3.dim}(enter to revoke \xB7 esc to close)${c3.reset}`,
|
|
5917
7536
|
items
|
|
5918
7537
|
);
|
|
5919
7538
|
if (!picked) return;
|
|
5920
7539
|
if (picked === "clear") {
|
|
5921
7540
|
perm.alwaysAllow.clear();
|
|
5922
7541
|
perm.allowRisk.clear();
|
|
5923
|
-
tui.print(`${
|
|
7542
|
+
tui.print(`${c3.gray}cleared all approvals${c3.reset}`);
|
|
5924
7543
|
} else if (picked.startsWith("tool:")) {
|
|
5925
7544
|
const t = picked.slice(5);
|
|
5926
7545
|
perm.alwaysAllow.delete(t);
|
|
5927
|
-
tui.print(`${
|
|
7546
|
+
tui.print(`${c3.gray}revoked tool ${t}${c3.reset}`);
|
|
5928
7547
|
} else if (picked.startsWith("risk:")) {
|
|
5929
7548
|
const r = Number(picked.slice(5));
|
|
5930
7549
|
perm.allowRisk.delete(r);
|
|
5931
|
-
tui.print(`${
|
|
7550
|
+
tui.print(`${c3.gray}revoked level ${r}${c3.reset}`);
|
|
5932
7551
|
}
|
|
5933
7552
|
save();
|
|
5934
7553
|
}
|
|
@@ -5946,7 +7565,7 @@ async function runMcpMenu(tui, mcp) {
|
|
|
5946
7565
|
items.push({ label: "\uFF0B Install a new MCP server\u2026", hint: "find & install", value: INSTALL });
|
|
5947
7566
|
items.push({ label: "Add manually (name: command)\u2026", hint: "advanced", value: ADD_MANUAL });
|
|
5948
7567
|
const picked = await tui.select(
|
|
5949
|
-
`${
|
|
7568
|
+
`${c3.bold}MCP servers${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c3.reset}`,
|
|
5950
7569
|
items
|
|
5951
7570
|
);
|
|
5952
7571
|
if (!picked) return;
|
|
@@ -5960,7 +7579,7 @@ async function runMcpMenu(tui, mcp) {
|
|
|
5960
7579
|
}
|
|
5961
7580
|
const server = configured.find((s) => s.name === picked);
|
|
5962
7581
|
const isConnected = connected.has(picked);
|
|
5963
|
-
const action = await tui.select(`${
|
|
7582
|
+
const action = await tui.select(`${c3.bold}${picked}${c3.reset}`, [
|
|
5964
7583
|
{ label: "View tools", value: "tools" },
|
|
5965
7584
|
isConnected ? { label: "Disconnect", value: "disconnect" } : { label: "Connect", value: "connect" },
|
|
5966
7585
|
server.enabled ? { label: "Disable (don't auto-connect)", value: "disable" } : { label: "Enable (auto-connect on start)", value: "enable" },
|
|
@@ -5970,29 +7589,29 @@ async function runMcpMenu(tui, mcp) {
|
|
|
5970
7589
|
if (action === "tools") {
|
|
5971
7590
|
const entry = mcp.catalog().servers.find((e) => e.name === picked);
|
|
5972
7591
|
if (!entry || entry.status !== "connected") {
|
|
5973
|
-
tui.print(`${
|
|
7592
|
+
tui.print(`${c3.gray}${picked} is not connected \u2014 connect it to list tools.${c3.reset}`);
|
|
5974
7593
|
return;
|
|
5975
7594
|
}
|
|
5976
|
-
if (!entry.tools.length) tui.print(`${
|
|
5977
|
-
for (const t of entry.tools) tui.print(` ${
|
|
5978
|
-
if (entry.resources.length) tui.print(` ${
|
|
7595
|
+
if (!entry.tools.length) tui.print(`${c3.gray}${picked} exposes no tools.${c3.reset}`);
|
|
7596
|
+
for (const t of entry.tools) tui.print(` ${c3.cyan}${t.name}${c3.reset}${t.description ? ` ${c3.gray}\u2014 ${t.description}${c3.reset}` : ""}`);
|
|
7597
|
+
if (entry.resources.length) tui.print(` ${c3.gray}${entry.resources.length} resource(s)${c3.reset}`);
|
|
5979
7598
|
} else if (action === "connect") {
|
|
5980
7599
|
const res = await mcp.connect(server);
|
|
5981
|
-
tui.print(res.ok ? `${
|
|
7600
|
+
tui.print(res.ok ? `${c3.cyan}\u26A1 connected (${res.tools} tools)${c3.reset}` : `${c3.red}\u26A0 ${res.error}${c3.reset}`);
|
|
5982
7601
|
} else if (action === "disconnect") {
|
|
5983
7602
|
mcp.disconnect(picked);
|
|
5984
|
-
tui.print(`${
|
|
7603
|
+
tui.print(`${c3.gray}disconnected ${picked}${c3.reset}`);
|
|
5985
7604
|
} else if (action === "enable") {
|
|
5986
7605
|
mcp.setEnabled(picked, true);
|
|
5987
7606
|
const res = await mcp.connect(server);
|
|
5988
|
-
tui.print(res.ok ? `${
|
|
7607
|
+
tui.print(res.ok ? `${c3.cyan}\u26A1 enabled + connected (${res.tools} tools)${c3.reset}` : `${c3.red}\u26A0 enabled but failed: ${res.error}${c3.reset}`);
|
|
5989
7608
|
} else if (action === "disable") {
|
|
5990
7609
|
mcp.setEnabled(picked, false);
|
|
5991
7610
|
mcp.disconnect(picked);
|
|
5992
|
-
tui.print(`${
|
|
7611
|
+
tui.print(`${c3.gray}disabled + disconnected ${picked}${c3.reset}`);
|
|
5993
7612
|
} else if (action === "remove") {
|
|
5994
7613
|
mcp.remove(picked);
|
|
5995
|
-
tui.print(`${
|
|
7614
|
+
tui.print(`${c3.gray}removed ${picked}${c3.reset}`);
|
|
5996
7615
|
}
|
|
5997
7616
|
}
|
|
5998
7617
|
async function addMcpServer(tui, mcp) {
|
|
@@ -6003,13 +7622,13 @@ async function addMcpServer(tui, mcp) {
|
|
|
6003
7622
|
if (!spec) return;
|
|
6004
7623
|
const cfg = parseServerSpec(spec);
|
|
6005
7624
|
if (!cfg) {
|
|
6006
|
-
tui.print(`${
|
|
7625
|
+
tui.print(`${c3.yellow}couldn't parse that. Use name: command [args]${c3.reset}`);
|
|
6007
7626
|
return;
|
|
6008
7627
|
}
|
|
6009
|
-
tui.print(`${
|
|
7628
|
+
tui.print(`${c3.gray}connecting MCP "${cfg.name}" (${cfg.command} ${cfg.args.join(" ")})\u2026${c3.reset}`);
|
|
6010
7629
|
const res = await mcp.add(cfg);
|
|
6011
|
-
if (res.ok) tui.print(`${
|
|
6012
|
-
else tui.print(`${
|
|
7630
|
+
if (res.ok) tui.print(`${c3.cyan}\u26A1 MCP "${cfg.name}" connected${c3.reset} ${c3.gray}(${res.tools} tool${res.tools === 1 ? "" : "s"})${c3.reset}`);
|
|
7631
|
+
else tui.print(`${c3.red}\u26A0 MCP "${cfg.name}" failed:${c3.reset} ${c3.gray}${res.error}${c3.reset} ${c3.dim}(saved; retry from the MCP menu)${c3.reset}`);
|
|
6013
7632
|
}
|
|
6014
7633
|
async function installMcpServerFlow(tui, mcp) {
|
|
6015
7634
|
const query = await tui.prompt(
|