@standardagents/code 0.8.0 → 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 +1680 -375
- 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();",
|
|
@@ -2373,14 +2541,14 @@ function buildInputBoxRows(opts) {
|
|
|
2373
2541
|
const plainChars = [...plain];
|
|
2374
2542
|
let di = 0;
|
|
2375
2543
|
for (const ch of plainChars) {
|
|
2376
|
-
const
|
|
2544
|
+
const c4 = colorAt(perimeterIndex("top", xi++, W, bodyH));
|
|
2377
2545
|
if (ch === "\u25CF" || ch === "\u25CB") {
|
|
2378
2546
|
const levelN = Math.max(1, Math.min(5, level));
|
|
2379
2547
|
const filled = di < levelN;
|
|
2380
2548
|
di++;
|
|
2381
|
-
top += (filled ? levelColor ||
|
|
2549
|
+
top += (filled ? levelColor || c4 : "\x1B[38;5;240m") + (filled ? "\u25CF" : "\u25CB") + reset;
|
|
2382
2550
|
} else {
|
|
2383
|
-
top +=
|
|
2551
|
+
top += c4 + ch + reset;
|
|
2384
2552
|
}
|
|
2385
2553
|
}
|
|
2386
2554
|
}
|
|
@@ -2401,10 +2569,10 @@ function buildInputBoxRows(opts) {
|
|
|
2401
2569
|
let bottom = "";
|
|
2402
2570
|
for (let x = 0; x < W; x++) {
|
|
2403
2571
|
const idx = perimeterIndex("bottom", W - 1 - x, W, bodyH);
|
|
2404
|
-
const
|
|
2405
|
-
if (x === 0) bottom +=
|
|
2406
|
-
else if (x === W - 1) bottom +=
|
|
2407
|
-
else bottom +=
|
|
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;
|
|
2408
2576
|
}
|
|
2409
2577
|
return [pad + top, ...bodyRows, pad + bottom];
|
|
2410
2578
|
}
|
|
@@ -3086,7 +3254,7 @@ var Tui = class _Tui {
|
|
|
3086
3254
|
const q = this.inputBuffer.slice(1).trim().toLowerCase();
|
|
3087
3255
|
if (q === "") return this.commands;
|
|
3088
3256
|
return this.commands.filter(
|
|
3089
|
-
(
|
|
3257
|
+
(c4) => c4.name.startsWith(q) || c4.name.includes(q) || c4.label.toLowerCase().includes(q)
|
|
3090
3258
|
);
|
|
3091
3259
|
}
|
|
3092
3260
|
runCommand(cmd) {
|
|
@@ -4171,7 +4339,7 @@ function tableCells(row) {
|
|
|
4171
4339
|
let r = row.trim();
|
|
4172
4340
|
if (r.startsWith("|")) r = r.slice(1);
|
|
4173
4341
|
if (r.endsWith("|")) r = r.slice(0, -1);
|
|
4174
|
-
return r.split("|").map((
|
|
4342
|
+
return r.split("|").map((c4) => c4.trim());
|
|
4175
4343
|
}
|
|
4176
4344
|
var SEPARATOR = /^[\s|:-]+$/;
|
|
4177
4345
|
function isTableSeparator(line) {
|
|
@@ -4180,17 +4348,17 @@ function isTableSeparator(line) {
|
|
|
4180
4348
|
function renderTable(rows) {
|
|
4181
4349
|
const cols2 = Math.max(...rows.map((r) => r.length));
|
|
4182
4350
|
const widths = [];
|
|
4183
|
-
for (let
|
|
4184
|
-
widths[
|
|
4351
|
+
for (let c4 = 0; c4 < cols2; c4++) {
|
|
4352
|
+
widths[c4] = Math.max(...rows.map((r) => visibleWidth(inline(r[c4] ?? ""))));
|
|
4185
4353
|
}
|
|
4186
4354
|
const sep = `${GRAY} \u2502 ${R}`;
|
|
4187
4355
|
const out = [];
|
|
4188
4356
|
rows.forEach((r, ri) => {
|
|
4189
4357
|
const cells = [];
|
|
4190
|
-
for (let
|
|
4191
|
-
const raw = r[
|
|
4358
|
+
for (let c4 = 0; c4 < cols2; c4++) {
|
|
4359
|
+
const raw = r[c4] ?? "";
|
|
4192
4360
|
const styled = ri === 0 ? `${BOLD2}${inline(raw)}${R}` : inline(raw);
|
|
4193
|
-
cells.push(padEndVisible(styled, widths[
|
|
4361
|
+
cells.push(padEndVisible(styled, widths[c4]));
|
|
4194
4362
|
}
|
|
4195
4363
|
out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
|
|
4196
4364
|
if (ri === 0) {
|
|
@@ -4510,7 +4678,7 @@ var McpManager = class {
|
|
|
4510
4678
|
}
|
|
4511
4679
|
}
|
|
4512
4680
|
closeAll() {
|
|
4513
|
-
for (const [,
|
|
4681
|
+
for (const [, c4] of this.clients) c4.close();
|
|
4514
4682
|
this.clients.clear();
|
|
4515
4683
|
}
|
|
4516
4684
|
get(name) {
|
|
@@ -4521,13 +4689,13 @@ var McpManager = class {
|
|
|
4521
4689
|
}
|
|
4522
4690
|
toolCount() {
|
|
4523
4691
|
let n = 0;
|
|
4524
|
-
for (const [,
|
|
4692
|
+
for (const [, c4] of this.clients) n += c4.tools.length;
|
|
4525
4693
|
return n;
|
|
4526
4694
|
}
|
|
4527
4695
|
/** A JSON-serializable catalog of every connected server for the KV/context. */
|
|
4528
4696
|
catalog() {
|
|
4529
4697
|
return {
|
|
4530
|
-
servers: Array.from(this.clients.values()).map((
|
|
4698
|
+
servers: Array.from(this.clients.values()).map((c4) => c4.catalogEntry()),
|
|
4531
4699
|
generatedAt: Date.now()
|
|
4532
4700
|
};
|
|
4533
4701
|
}
|
|
@@ -4617,9 +4785,9 @@ function flattenContent(content, structured) {
|
|
|
4617
4785
|
}
|
|
4618
4786
|
function flattenResourceContents(contents) {
|
|
4619
4787
|
const parts = [];
|
|
4620
|
-
for (const
|
|
4621
|
-
if (typeof
|
|
4622
|
-
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)]`);
|
|
4623
4791
|
}
|
|
4624
4792
|
return parts.join("\n").trim();
|
|
4625
4793
|
}
|
|
@@ -4641,10 +4809,10 @@ function sortKeys(value) {
|
|
|
4641
4809
|
}
|
|
4642
4810
|
return value;
|
|
4643
4811
|
}
|
|
4644
|
-
function sha256(
|
|
4645
|
-
return crypto.createHash("sha256").update(
|
|
4812
|
+
function sha256(input3) {
|
|
4813
|
+
return crypto.createHash("sha256").update(input3).digest("hex");
|
|
4646
4814
|
}
|
|
4647
|
-
var DIR = path3.join(
|
|
4815
|
+
var DIR = path3.join(os9.homedir(), ".standardagents");
|
|
4648
4816
|
var FILE = path3.join(DIR, "credentials");
|
|
4649
4817
|
function normalizeEndpoint(endpoint) {
|
|
4650
4818
|
let e = endpoint.trim();
|
|
@@ -4702,6 +4870,276 @@ function saveDefaultEndpoint(endpoint) {
|
|
|
4702
4870
|
} catch {
|
|
4703
4871
|
}
|
|
4704
4872
|
}
|
|
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.");
|
|
4903
|
+
}
|
|
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];
|
|
4908
|
+
try {
|
|
4909
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
4910
|
+
child.unref();
|
|
4911
|
+
} catch {
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
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() {
|
|
4922
|
+
try {
|
|
4923
|
+
const pkg = JSON.parse(fs4.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
4924
|
+
return typeof pkg.version === "string" ? pkg.version : "";
|
|
4925
|
+
} catch {
|
|
4926
|
+
return "";
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
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 = "";
|
|
4934
|
+
try {
|
|
4935
|
+
host = new URL(endpoint).hostname;
|
|
4936
|
+
} catch {
|
|
4937
|
+
return false;
|
|
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;
|
|
4948
|
+
}
|
|
4949
|
+
var DIR2 = path3.join(os9.homedir(), ".standardagents");
|
|
4950
|
+
var FILE2 = path3.join(DIR2, "machine.json");
|
|
4951
|
+
function loadMachineIdentity() {
|
|
4952
|
+
try {
|
|
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
|
+
}
|
|
4705
5143
|
var PKG_NAME = "@standardagents/code";
|
|
4706
5144
|
var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}`;
|
|
4707
5145
|
var CACHE_REL_DIR = ".config/standardagents-cli";
|
|
@@ -4879,78 +5317,795 @@ function runUpdate(pm) {
|
|
|
4879
5317
|
});
|
|
4880
5318
|
}
|
|
4881
5319
|
|
|
4882
|
-
// src/
|
|
4883
|
-
var
|
|
4884
|
-
var
|
|
4885
|
-
var
|
|
4886
|
-
var
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
red: "\x1B[31m",
|
|
4897
|
-
teal: "\x1B[38;5;37m"
|
|
4898
|
-
// brand teal (matches the marketing site's teal accent)
|
|
4899
|
-
};
|
|
4900
|
-
var LOGO_MARK = [
|
|
4901
|
-
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
|
4902
|
-
" \u2588\u2588\u2588 \u2588\u2588",
|
|
4903
|
-
" \u2588\u2588 \u2588",
|
|
4904
|
-
"\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588",
|
|
4905
|
-
"\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588",
|
|
4906
|
-
"\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588",
|
|
4907
|
-
"\u2588 \u2588\u2588",
|
|
4908
|
-
"\u2588\u2588 \u2588\u2588\u2588",
|
|
4909
|
-
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"
|
|
4910
|
-
];
|
|
4911
|
-
function printUsage() {
|
|
4912
|
-
stdout.write(
|
|
4913
|
-
[
|
|
4914
|
-
"",
|
|
4915
|
-
`${c.bold}Usage${c.reset}`,
|
|
4916
|
-
" standardcode [options] [dir]",
|
|
4917
|
-
"",
|
|
4918
|
-
`${c.bold}Options${c.reset}`,
|
|
4919
|
-
" -e, --endpoint [url] Use a different Standard Agents instance for this run",
|
|
4920
|
-
" (default: https://api.standardcode.ai).",
|
|
4921
|
-
" If url is omitted, prompt for it.",
|
|
4922
|
-
" Credentials are remembered for that endpoint, but",
|
|
4923
|
-
" the saved default endpoint is not changed.",
|
|
4924
|
-
" -h, --help Show this help.",
|
|
4925
|
-
""
|
|
4926
|
-
].join("\n")
|
|
4927
|
-
);
|
|
4928
|
-
}
|
|
4929
|
-
function parseArgs2(args) {
|
|
4930
|
-
const parsed = { help: false, promptEndpoint: false };
|
|
4931
|
-
for (let i = 0; i < args.length; i++) {
|
|
4932
|
-
const arg = args[i];
|
|
4933
|
-
if (arg === "--help" || arg === "-h") {
|
|
4934
|
-
parsed.help = true;
|
|
4935
|
-
continue;
|
|
4936
|
-
}
|
|
4937
|
-
if (arg === "--endpoint" || arg === "-e") {
|
|
4938
|
-
const value = args[i + 1];
|
|
4939
|
-
if (value && !value.startsWith("-")) {
|
|
4940
|
-
parsed.endpoint = value;
|
|
4941
|
-
i++;
|
|
4942
|
-
} else {
|
|
4943
|
-
parsed.promptEndpoint = true;
|
|
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`);
|
|
4944
5334
|
}
|
|
4945
|
-
|
|
5335
|
+
} catch {
|
|
4946
5336
|
}
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
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
|
+
|
|
6040
|
+
// src/index.ts
|
|
6041
|
+
var c3 = {
|
|
6042
|
+
reset: "\x1B[0m",
|
|
6043
|
+
dim: "\x1B[2m",
|
|
6044
|
+
bold: "\x1B[1m",
|
|
6045
|
+
white: "\x1B[97m",
|
|
6046
|
+
cyan: "\x1B[36m",
|
|
6047
|
+
green: "\x1B[32m",
|
|
6048
|
+
gray: "\x1B[90m",
|
|
6049
|
+
magenta: "\x1B[35m",
|
|
6050
|
+
yellow: "\x1B[33m",
|
|
6051
|
+
red: "\x1B[31m",
|
|
6052
|
+
teal: "\x1B[38;5;37m"
|
|
6053
|
+
// brand teal (matches the marketing site's teal accent)
|
|
6054
|
+
};
|
|
6055
|
+
var LOGO_MARK = [
|
|
6056
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
|
6057
|
+
" \u2588\u2588\u2588 \u2588\u2588",
|
|
6058
|
+
" \u2588\u2588 \u2588",
|
|
6059
|
+
"\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588",
|
|
6060
|
+
"\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588",
|
|
6061
|
+
"\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588",
|
|
6062
|
+
"\u2588 \u2588\u2588",
|
|
6063
|
+
"\u2588\u2588 \u2588\u2588\u2588",
|
|
6064
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"
|
|
6065
|
+
];
|
|
6066
|
+
function printUsage() {
|
|
6067
|
+
stdout.write(
|
|
6068
|
+
[
|
|
6069
|
+
"",
|
|
6070
|
+
`${c3.bold}Usage${c3.reset}`,
|
|
6071
|
+
" standardcode [options] [dir]",
|
|
6072
|
+
"",
|
|
6073
|
+
`${c3.bold}Options${c3.reset}`,
|
|
6074
|
+
" -e, --endpoint [url] Use a different Standard Agents instance for this run",
|
|
6075
|
+
" (default: https://api.standardcode.ai).",
|
|
6076
|
+
" If url is omitted, prompt for it.",
|
|
6077
|
+
" Credentials are remembered for that endpoint, but",
|
|
6078
|
+
" the saved default endpoint is not changed.",
|
|
6079
|
+
" -h, --help Show this help.",
|
|
6080
|
+
""
|
|
6081
|
+
].join("\n")
|
|
6082
|
+
);
|
|
6083
|
+
}
|
|
6084
|
+
function parseArgs2(args) {
|
|
6085
|
+
const parsed = { help: false, promptEndpoint: false };
|
|
6086
|
+
for (let i = 0; i < args.length; i++) {
|
|
6087
|
+
const arg = args[i];
|
|
6088
|
+
if (arg === "--help" || arg === "-h") {
|
|
6089
|
+
parsed.help = true;
|
|
6090
|
+
continue;
|
|
6091
|
+
}
|
|
6092
|
+
if (arg === "--endpoint" || arg === "-e") {
|
|
6093
|
+
const value = args[i + 1];
|
|
6094
|
+
if (value && !value.startsWith("-")) {
|
|
6095
|
+
parsed.endpoint = value;
|
|
6096
|
+
i++;
|
|
6097
|
+
} else {
|
|
6098
|
+
parsed.promptEndpoint = true;
|
|
6099
|
+
}
|
|
6100
|
+
continue;
|
|
6101
|
+
}
|
|
6102
|
+
if (arg.startsWith("--endpoint=")) {
|
|
6103
|
+
const value = arg.slice("--endpoint=".length);
|
|
6104
|
+
if (value) {
|
|
6105
|
+
parsed.endpoint = value;
|
|
6106
|
+
} else {
|
|
6107
|
+
parsed.promptEndpoint = true;
|
|
6108
|
+
}
|
|
4954
6109
|
continue;
|
|
4955
6110
|
}
|
|
4956
6111
|
if (arg === "--") {
|
|
@@ -4973,7 +6128,7 @@ function printAssistant(tui, text) {
|
|
|
4973
6128
|
let dotted = false;
|
|
4974
6129
|
for (const line of renderMarkdown(text, cols2)) {
|
|
4975
6130
|
if (!dotted && line.trim()) {
|
|
4976
|
-
tui.print(`${
|
|
6131
|
+
tui.print(`${c3.gray}\u2022${c3.reset} ${line}`);
|
|
4977
6132
|
dotted = true;
|
|
4978
6133
|
} else {
|
|
4979
6134
|
tui.print(` ${line}`);
|
|
@@ -4988,7 +6143,7 @@ function startLoader(label) {
|
|
|
4988
6143
|
const draw = () => {
|
|
4989
6144
|
const now = Date.now();
|
|
4990
6145
|
const f = frames[Math.floor(now / 70) % frames.length];
|
|
4991
|
-
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}`);
|
|
4992
6147
|
};
|
|
4993
6148
|
draw();
|
|
4994
6149
|
const timer = setInterval(draw, 70);
|
|
@@ -5004,53 +6159,25 @@ function farewell(stoppedProcs = 0) {
|
|
|
5004
6159
|
if (stoppedProcs > 0) {
|
|
5005
6160
|
stdout.write(
|
|
5006
6161
|
`
|
|
5007
|
-
${
|
|
6162
|
+
${c3.cyan}\u2699${c3.reset} Stopped ${stoppedProcs} background process${stoppedProcs === 1 ? "" : "es"}.
|
|
5008
6163
|
`
|
|
5009
6164
|
);
|
|
5010
6165
|
}
|
|
5011
6166
|
stdout.write(`
|
|
5012
|
-
${
|
|
6167
|
+
${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.reset}
|
|
5013
6168
|
`);
|
|
5014
6169
|
}
|
|
5015
|
-
function isLocalHost(host) {
|
|
5016
|
-
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);
|
|
5017
|
-
}
|
|
5018
|
-
function relaxTlsForLocalEndpoint(endpoint) {
|
|
5019
|
-
let host = "";
|
|
5020
|
-
try {
|
|
5021
|
-
host = new URL(endpoint).hostname;
|
|
5022
|
-
} catch {
|
|
5023
|
-
return false;
|
|
5024
|
-
}
|
|
5025
|
-
if (!endpoint.startsWith("https:") || !isLocalHost(host)) return false;
|
|
5026
|
-
const origEmit = process.emitWarning.bind(process);
|
|
5027
|
-
process.emitWarning = ((warning, ...args) => {
|
|
5028
|
-
const msg = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
5029
|
-
if (msg.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
|
|
5030
|
-
return origEmit(warning, ...args);
|
|
5031
|
-
});
|
|
5032
|
-
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
5033
|
-
return true;
|
|
5034
|
-
}
|
|
5035
|
-
function readVersion() {
|
|
5036
|
-
try {
|
|
5037
|
-
const pkg = JSON.parse(fs4.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
5038
|
-
return typeof pkg.version === "string" ? pkg.version : "";
|
|
5039
|
-
} catch {
|
|
5040
|
-
return "";
|
|
5041
|
-
}
|
|
5042
|
-
}
|
|
5043
6170
|
function printWelcome(endpoint, projectDir) {
|
|
5044
|
-
const home =
|
|
6171
|
+
const home = os9.homedir();
|
|
5045
6172
|
const dir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
5046
6173
|
const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
5047
6174
|
const version = readVersion();
|
|
5048
6175
|
const pad = " ";
|
|
5049
6176
|
const meta = [
|
|
5050
|
-
`${
|
|
5051
|
-
`${
|
|
5052
|
-
...endpoint === PRODUCTION_ENDPOINT ? [] : [`${
|
|
5053
|
-
`${
|
|
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}`
|
|
5054
6181
|
];
|
|
5055
6182
|
const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
|
|
5056
6183
|
const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
|
|
@@ -5065,11 +6192,11 @@ function printWelcome(endpoint, projectDir) {
|
|
|
5065
6192
|
}
|
|
5066
6193
|
function colorActivity(line) {
|
|
5067
6194
|
const m = line.match(/^(\s*)([✓✗⛔])\s?([\s\S]*)$/);
|
|
5068
|
-
if (!m) return `${
|
|
6195
|
+
if (!m) return `${c3.dim}${line}${c3.reset}`;
|
|
5069
6196
|
const [, indent, glyph, rest] = m;
|
|
5070
6197
|
if (glyph === "\u2713") {
|
|
5071
|
-
const body = rest.replace(/\s(\([^()]*\))\s*$/, ` ${
|
|
5072
|
-
return `${indent}${
|
|
6198
|
+
const body = rest.replace(/\s(\([^()]*\))\s*$/, ` ${c3.dim}$1${c3.reset}`);
|
|
6199
|
+
return `${indent}${c3.green}\u2713${c3.reset} ${body}`;
|
|
5073
6200
|
}
|
|
5074
6201
|
if (glyph === "\u2717") {
|
|
5075
6202
|
const ERR_MAX_LINES = 7;
|
|
@@ -5077,22 +6204,26 @@ function colorActivity(line) {
|
|
|
5077
6204
|
const shown = lines.slice(0, ERR_MAX_LINES);
|
|
5078
6205
|
const hidden = lines.length - shown.length;
|
|
5079
6206
|
const body = shown.map(
|
|
5080
|
-
(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}`
|
|
5081
6208
|
).join("\n");
|
|
5082
6209
|
if (hidden > 0) {
|
|
5083
6210
|
return `${body}
|
|
5084
|
-
${indent}${
|
|
6211
|
+
${indent}${c3.dim}\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}${c3.reset}`;
|
|
5085
6212
|
}
|
|
5086
6213
|
return body;
|
|
5087
6214
|
}
|
|
5088
|
-
return `${indent}${
|
|
6215
|
+
return `${indent}${c3.yellow}\u26D4 ${rest}${c3.reset}`;
|
|
5089
6216
|
}
|
|
5090
6217
|
async function main() {
|
|
6218
|
+
if (process.argv[2] === "daemon") {
|
|
6219
|
+
await runDaemonCommand(process.argv.slice(3));
|
|
6220
|
+
return;
|
|
6221
|
+
}
|
|
5091
6222
|
let cliArgs;
|
|
5092
6223
|
try {
|
|
5093
6224
|
cliArgs = parseArgs2(process.argv.slice(2));
|
|
5094
6225
|
} catch (error) {
|
|
5095
|
-
stdout.write(`${
|
|
6226
|
+
stdout.write(`${c3.red}error:${c3.reset} ${error instanceof Error ? error.message : String(error)}
|
|
5096
6227
|
`);
|
|
5097
6228
|
printUsage();
|
|
5098
6229
|
process.exit(1);
|
|
@@ -5105,7 +6236,7 @@ async function main() {
|
|
|
5105
6236
|
const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
|
|
5106
6237
|
const dirArg = cliArgs.dir;
|
|
5107
6238
|
const projectDir = path3.resolve(dirArg || process.cwd());
|
|
5108
|
-
const machine =
|
|
6239
|
+
const machine = os9.hostname();
|
|
5109
6240
|
const reader = { rl: null };
|
|
5110
6241
|
let handoffClosing = false;
|
|
5111
6242
|
let preflightArmed = false;
|
|
@@ -5119,7 +6250,7 @@ async function main() {
|
|
|
5119
6250
|
}
|
|
5120
6251
|
preflightArmed = true;
|
|
5121
6252
|
stdout.write(`
|
|
5122
|
-
${
|
|
6253
|
+
${c3.dim}Press Control-C again to exit${c3.reset}
|
|
5123
6254
|
`);
|
|
5124
6255
|
preflightTimer = setTimeout(() => {
|
|
5125
6256
|
preflightArmed = false;
|
|
@@ -5141,10 +6272,10 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
5141
6272
|
const askEndpoint = async () => {
|
|
5142
6273
|
for (; ; ) {
|
|
5143
6274
|
const answer = (await ask(
|
|
5144
|
-
`${
|
|
6275
|
+
`${c3.cyan}Standard Agents instance URL${c3.reset} (e.g. http://localhost:5178): `
|
|
5145
6276
|
)).trim();
|
|
5146
6277
|
if (answer) return answer;
|
|
5147
|
-
stdout.write(`${
|
|
6278
|
+
stdout.write(`${c3.dim}An endpoint URL is required.${c3.reset}
|
|
5148
6279
|
`);
|
|
5149
6280
|
}
|
|
5150
6281
|
};
|
|
@@ -5159,7 +6290,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
5159
6290
|
const tlsRelaxed = relaxTlsForLocalEndpoint(endpoint);
|
|
5160
6291
|
printWelcome(endpoint, projectDir);
|
|
5161
6292
|
if (tlsRelaxed) {
|
|
5162
|
-
stdout.write(`${
|
|
6293
|
+
stdout.write(`${c3.dim} TLS verification relaxed for local endpoint.${c3.reset}
|
|
5163
6294
|
|
|
5164
6295
|
`);
|
|
5165
6296
|
}
|
|
@@ -5171,7 +6302,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
5171
6302
|
loading.stop();
|
|
5172
6303
|
const applied = consumeAppliedUpdate(version);
|
|
5173
6304
|
if (applied) {
|
|
5174
|
-
stdout.write(` ${
|
|
6305
|
+
stdout.write(` ${c3.green}\u2713${c3.reset} ${c3.dim}Standard Code updated to v${version}.${c3.reset}
|
|
5175
6306
|
|
|
5176
6307
|
`);
|
|
5177
6308
|
}
|
|
@@ -5180,21 +6311,21 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
5180
6311
|
const decision = decideAutoUpdate(updateAvailable, { state: readAutoUpdateState(), pm });
|
|
5181
6312
|
if (decision === "start" && pm && startBackgroundUpdate(updateAvailable.latest, pm)) {
|
|
5182
6313
|
stdout.write(
|
|
5183
|
-
` ${
|
|
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}
|
|
5184
6315
|
|
|
5185
6316
|
`
|
|
5186
6317
|
);
|
|
5187
6318
|
} else if (decision === "in_flight") {
|
|
5188
6319
|
stdout.write(
|
|
5189
|
-
` ${
|
|
6320
|
+
` ${c3.teal}\u27F3${c3.reset} ${c3.dim}Standard Code v${updateAvailable.latest} is still installing in the background.${c3.reset}
|
|
5190
6321
|
|
|
5191
6322
|
`
|
|
5192
6323
|
);
|
|
5193
6324
|
} else {
|
|
5194
6325
|
const display = updateCommand(pm ?? "npm").display;
|
|
5195
6326
|
stdout.write(
|
|
5196
|
-
` ${
|
|
5197
|
-
${
|
|
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}
|
|
5198
6329
|
|
|
5199
6330
|
`
|
|
5200
6331
|
);
|
|
@@ -5212,39 +6343,39 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
5212
6343
|
if (!api || !storedCheck?.ok) {
|
|
5213
6344
|
const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
5214
6345
|
if (storedCheck && !storedCheck.ok) {
|
|
5215
|
-
stdout.write(`${
|
|
6346
|
+
stdout.write(`${c3.red}\u2717${c3.reset} ${c3.dim}Saved sign-in for this endpoint failed:${c3.reset} ${storedCheck.reason}
|
|
5216
6347
|
`);
|
|
5217
|
-
if (storedCheck.hint) stdout.write(` ${
|
|
6348
|
+
if (storedCheck.hint) stdout.write(` ${c3.dim}${storedCheck.hint}${c3.reset}
|
|
5218
6349
|
`);
|
|
5219
6350
|
stdout.write("\n");
|
|
5220
6351
|
}
|
|
5221
6352
|
const explainFailure = (result, prefix) => {
|
|
5222
|
-
stdout.write(`${
|
|
6353
|
+
stdout.write(`${c3.red}\u2717${c3.reset} ${prefix}${result.reason}
|
|
5223
6354
|
`);
|
|
5224
|
-
if (result.hint) stdout.write(` ${
|
|
6355
|
+
if (result.hint) stdout.write(` ${c3.dim}${result.hint}${c3.reset}
|
|
5225
6356
|
`);
|
|
5226
6357
|
};
|
|
5227
|
-
stdout.write(`${
|
|
6358
|
+
stdout.write(`${c3.bold}${gradientText("Sign in to Standard Code")}${c3.reset}
|
|
5228
6359
|
`);
|
|
5229
6360
|
if (`https://${host}` !== PRODUCTION_ENDPOINT) {
|
|
5230
|
-
stdout.write(`${
|
|
6361
|
+
stdout.write(`${c3.dim}Connecting to${c3.reset} ${c3.teal}${host}${c3.reset}
|
|
5231
6362
|
`);
|
|
5232
6363
|
}
|
|
5233
6364
|
stdout.write(
|
|
5234
|
-
`${
|
|
6365
|
+
`${c3.dim}You'll only need to do this once on this machine.${c3.reset}
|
|
5235
6366
|
|
|
5236
6367
|
`
|
|
5237
6368
|
);
|
|
5238
6369
|
stdout.write(
|
|
5239
|
-
`${
|
|
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}
|
|
5240
6371
|
|
|
5241
6372
|
`
|
|
5242
6373
|
);
|
|
5243
6374
|
for (; ; ) {
|
|
5244
|
-
const token = (await ask(`${
|
|
6375
|
+
const token = (await ask(`${c3.teal}\u276F${c3.reset} `)).trim();
|
|
5245
6376
|
if (!token) {
|
|
5246
6377
|
const got = await deviceLogin(endpoint).catch((e) => {
|
|
5247
|
-
stdout.write(`${
|
|
6378
|
+
stdout.write(`${c3.red}\u2717${c3.reset} ${c3.dim}${e instanceof Error ? e.message : String(e)}${c3.reset}
|
|
5248
6379
|
`);
|
|
5249
6380
|
return null;
|
|
5250
6381
|
});
|
|
@@ -5258,7 +6389,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
5258
6389
|
{ endpoint, access_token: got, token_type: "Bearer", saved_at: Date.now() },
|
|
5259
6390
|
{ updateDefault: !endpointOverride }
|
|
5260
6391
|
);
|
|
5261
|
-
stdout.write(`${
|
|
6392
|
+
stdout.write(`${c3.green}\u2713${c3.reset} Connected to ${c3.teal}${host}${c3.reset}
|
|
5262
6393
|
`);
|
|
5263
6394
|
break;
|
|
5264
6395
|
}
|
|
@@ -5274,7 +6405,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
5274
6405
|
{ endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
|
|
5275
6406
|
{ updateDefault: !endpointOverride }
|
|
5276
6407
|
);
|
|
5277
|
-
stdout.write(`${
|
|
6408
|
+
stdout.write(`${c3.green}\u2713${c3.reset} Connected to ${c3.teal}${host}${c3.reset}
|
|
5278
6409
|
`);
|
|
5279
6410
|
break;
|
|
5280
6411
|
}
|
|
@@ -5286,17 +6417,72 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
5286
6417
|
if (!api) process.exit(1);
|
|
5287
6418
|
handoffClosing = true;
|
|
5288
6419
|
reader.rl?.close();
|
|
5289
|
-
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
|
+
}
|
|
5290
6469
|
const loadingSessions = startLoader("Loading sessions");
|
|
5291
6470
|
let existing = [];
|
|
5292
6471
|
try {
|
|
5293
|
-
existing = await api.listThreads(AGENT_ID_VARIANTS,
|
|
6472
|
+
existing = await api.listThreads(AGENT_ID_VARIANTS, resumeTags);
|
|
5294
6473
|
} catch {
|
|
5295
6474
|
existing = [];
|
|
5296
6475
|
}
|
|
5297
6476
|
const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
|
|
5298
6477
|
loadingSessions.stop();
|
|
5299
|
-
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
|
+
};
|
|
5300
6486
|
let threadId;
|
|
5301
6487
|
let resumed = false;
|
|
5302
6488
|
let historySeed;
|
|
@@ -5307,31 +6493,61 @@ ${c.dim}Press Control-C again to exit${c.reset}
|
|
|
5307
6493
|
value: s.id
|
|
5308
6494
|
}));
|
|
5309
6495
|
items.push({ label: "\uFF0B Start a new session", value: null });
|
|
5310
|
-
const
|
|
5311
|
-
const tilde = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
5312
|
-
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;
|
|
5313
6497
|
const picked = await tui.select(
|
|
5314
|
-
`${
|
|
6498
|
+
`${c3.bold}${gradientText("Resume a session")}${c3.reset} ${c3.gray}${whereLabel}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
|
|
5315
6499
|
items
|
|
5316
6500
|
);
|
|
5317
6501
|
if (typeof picked === "string") {
|
|
5318
6502
|
threadId = picked;
|
|
5319
6503
|
resumed = true;
|
|
5320
6504
|
} else {
|
|
5321
|
-
threadId = await
|
|
6505
|
+
threadId = await createSessionThread();
|
|
5322
6506
|
historySeed = existing[0]?.id;
|
|
5323
6507
|
}
|
|
5324
6508
|
} else {
|
|
5325
|
-
threadId = await
|
|
6509
|
+
threadId = await createSessionThread();
|
|
5326
6510
|
}
|
|
5327
6511
|
for (; ; ) {
|
|
5328
|
-
await runInteractive(tui, api, threadId, projectDir, machine, resumed, historySeed);
|
|
6512
|
+
await runInteractive(tui, api, threadId, projectDir, machine, resumed, session, historySeed);
|
|
5329
6513
|
historySeed = threadId;
|
|
5330
|
-
threadId = await
|
|
6514
|
+
threadId = await createSessionThread();
|
|
5331
6515
|
await api.kvSet(threadId, "lease_supersedes", historySeed);
|
|
5332
6516
|
resumed = false;
|
|
5333
6517
|
}
|
|
5334
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
|
+
}
|
|
5335
6551
|
function isSilentMessage(m) {
|
|
5336
6552
|
return m?.silent === true || m?.metadata?.silent === true;
|
|
5337
6553
|
}
|
|
@@ -5354,46 +6570,10 @@ async function summarizeThreads(api, threads) {
|
|
|
5354
6570
|
}
|
|
5355
6571
|
function subagentLabel(s, titles) {
|
|
5356
6572
|
const agentName = (s.agent_name || "").trim();
|
|
5357
|
-
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");
|
|
5358
6574
|
const tagged = (s.threadName || "").trim();
|
|
5359
6575
|
return tagged ? `${title} \xB7 ${tagged}` : title;
|
|
5360
6576
|
}
|
|
5361
|
-
async function deviceLogin(endpoint) {
|
|
5362
|
-
const start = await fetch(`${endpoint}/api/auth/device/start`, { method: "POST" });
|
|
5363
|
-
if (!start.ok) throw new Error(`This instance does not support browser sign-in (HTTP ${start.status}). Paste an API token instead.`);
|
|
5364
|
-
const info = await start.json();
|
|
5365
|
-
stdout.write(`${c.dim}Opening your browser to sign in. If it doesn't open, visit:${c.reset}
|
|
5366
|
-
`);
|
|
5367
|
-
stdout.write(`
|
|
5368
|
-
${c.teal}${info.verify_url}${c.reset}
|
|
5369
|
-
|
|
5370
|
-
`);
|
|
5371
|
-
stdout.write(`${c.dim}Waiting for sign-in to complete\u2026 (Ctrl-C to cancel)${c.reset}
|
|
5372
|
-
`);
|
|
5373
|
-
openUrl(info.verify_url);
|
|
5374
|
-
const deadline = Date.now() + (info.expires_in ?? 600) * 1e3;
|
|
5375
|
-
const interval = Math.max(2, info.interval ?? 2) * 1e3;
|
|
5376
|
-
while (Date.now() < deadline) {
|
|
5377
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
5378
|
-
const res = await fetch(info.poll_url).catch(() => null);
|
|
5379
|
-
if (!res) continue;
|
|
5380
|
-
if (res.status === 404) throw new Error("The sign-in link expired. Try again.");
|
|
5381
|
-
const body = await res.json().catch(() => ({}));
|
|
5382
|
-
if (body.status === "approved" && body.token) return body.token;
|
|
5383
|
-
if (body.status === "denied") throw new Error("Sign-in was denied in the browser.");
|
|
5384
|
-
}
|
|
5385
|
-
throw new Error("Timed out waiting for browser approval. Try again.");
|
|
5386
|
-
}
|
|
5387
|
-
function openUrl(url) {
|
|
5388
|
-
const platform = process.platform;
|
|
5389
|
-
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
5390
|
-
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
5391
|
-
try {
|
|
5392
|
-
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
5393
|
-
child.unref();
|
|
5394
|
-
} catch {
|
|
5395
|
-
}
|
|
5396
|
-
}
|
|
5397
6577
|
function relativeTime(unixSeconds) {
|
|
5398
6578
|
const diff = Date.now() / 1e3 - unixSeconds;
|
|
5399
6579
|
if (diff < 60) return "just now";
|
|
@@ -5436,8 +6616,8 @@ async function printHistory(api, threadId, tui) {
|
|
|
5436
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));
|
|
5437
6617
|
if (!convo.length) return;
|
|
5438
6618
|
const shown = convo.slice(-24);
|
|
5439
|
-
tui.print(`${
|
|
5440
|
-
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}`);
|
|
5441
6621
|
for (const m of shown) {
|
|
5442
6622
|
const text = messageText(m.content).trim();
|
|
5443
6623
|
if (!text) continue;
|
|
@@ -5445,12 +6625,16 @@ async function printHistory(api, threadId, tui) {
|
|
|
5445
6625
|
else printAssistant(tui, text);
|
|
5446
6626
|
}
|
|
5447
6627
|
}
|
|
5448
|
-
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";
|
|
5449
6631
|
const registry = new ProcessRegistry(api, threadId, machine);
|
|
5450
|
-
const mcp = new McpManager(projectDir);
|
|
5451
|
-
const publishMcpCatalog = () =>
|
|
5452
|
-
|
|
5453
|
-
|
|
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);
|
|
5454
6638
|
const refreshBgCount = () => {
|
|
5455
6639
|
void registry.runningCount().then((n) => tui.setBackgroundCount(n)).catch(() => {
|
|
5456
6640
|
});
|
|
@@ -5468,8 +6652,6 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
|
|
|
5468
6652
|
saveApprovals(api, threadId, perm);
|
|
5469
6653
|
});
|
|
5470
6654
|
saveApprovals(api, threadId, perm);
|
|
5471
|
-
void api.kvSet(threadId, "session_info", { cwd: projectDir, machine }).catch(() => {
|
|
5472
|
-
});
|
|
5473
6655
|
const attaching = startLoader("Attaching to thread");
|
|
5474
6656
|
let busy = false;
|
|
5475
6657
|
let interrupting = false;
|
|
@@ -5490,35 +6672,76 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
|
|
|
5490
6672
|
for (const v of activeSteps.values()) label = v;
|
|
5491
6673
|
tui.setStep(label, liveOut);
|
|
5492
6674
|
};
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
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
|
|
6735
|
+
);
|
|
5512
6736
|
}
|
|
5513
6737
|
},
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
);
|
|
6738
|
+
{
|
|
6739
|
+
clientId: interactiveClientId(session.identity),
|
|
6740
|
+
clientName: `${machineDisplayName(session.identity)} (terminal)`,
|
|
6741
|
+
clientKind: "interactive",
|
|
6742
|
+
claim
|
|
5520
6743
|
}
|
|
5521
|
-
|
|
6744
|
+
);
|
|
5522
6745
|
const stream = new MessageStream(api, threadId, {
|
|
5523
6746
|
// Live streaming preview: answer text and (opt-in) internal reasoning feed
|
|
5524
6747
|
// the TUI's ephemeral preview; the committed message still renders from
|
|
@@ -5615,14 +6838,14 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
5615
6838
|
const sessionEnded = new Promise((r) => endSession = r);
|
|
5616
6839
|
const quit = async () => {
|
|
5617
6840
|
tui.end();
|
|
5618
|
-
const stopped2 = api.stop(threadId).catch(() => {
|
|
5619
|
-
});
|
|
6841
|
+
const stopped2 = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
|
|
6842
|
+
}) : Promise.resolve();
|
|
5620
6843
|
const procsStopped2 = host.stopAllLocalProcesses().catch(() => 0);
|
|
5621
|
-
bridge
|
|
6844
|
+
bridge?.close();
|
|
5622
6845
|
stream.close();
|
|
5623
6846
|
events.close();
|
|
5624
6847
|
subActivity.closeAll();
|
|
5625
|
-
mcp
|
|
6848
|
+
mcp?.closeAll();
|
|
5626
6849
|
const [, killed2] = await Promise.race([
|
|
5627
6850
|
Promise.all([stopped2, procsStopped2]),
|
|
5628
6851
|
new Promise((r) => setTimeout(() => r([void 0, 0]), 1500))
|
|
@@ -5634,21 +6857,28 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
5634
6857
|
const logout = async () => {
|
|
5635
6858
|
deleteCredential(api.origin);
|
|
5636
6859
|
const instanceHost = api.origin.replace(/^https?:\/\//, "");
|
|
5637
|
-
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}`);
|
|
5638
6861
|
await quit();
|
|
5639
6862
|
};
|
|
5640
6863
|
const bgMgr = {
|
|
5641
6864
|
list: () => registry.list(),
|
|
5642
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
|
+
}
|
|
5643
6872
|
await host.execute("background_process", { action: "stop", id });
|
|
5644
6873
|
refreshBgCount();
|
|
5645
6874
|
}
|
|
5646
6875
|
};
|
|
5647
6876
|
const mcpCtl = {
|
|
5648
6877
|
configured: () => listMcpServers(),
|
|
5649
|
-
connectedNames: () => mcp
|
|
5650
|
-
catalog: () => mcp
|
|
6878
|
+
connectedNames: () => mcp?.connectedNames() ?? [],
|
|
6879
|
+
catalog: () => mcp?.catalog() ?? { servers: [] },
|
|
5651
6880
|
connect: async (cfg) => {
|
|
6881
|
+
if (!mcp) return { ok: false, error: "MCP servers run on the session's machine." };
|
|
5652
6882
|
try {
|
|
5653
6883
|
const client = await mcp.connect(cfg);
|
|
5654
6884
|
publishMcpCatalog();
|
|
@@ -5659,7 +6889,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
5659
6889
|
}
|
|
5660
6890
|
},
|
|
5661
6891
|
disconnect: (name) => {
|
|
5662
|
-
mcp
|
|
6892
|
+
mcp?.disconnect(name);
|
|
5663
6893
|
publishMcpCatalog();
|
|
5664
6894
|
},
|
|
5665
6895
|
add: async (cfg) => {
|
|
@@ -5675,7 +6905,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
5675
6905
|
);
|
|
5676
6906
|
},
|
|
5677
6907
|
remove: (name) => {
|
|
5678
|
-
mcp
|
|
6908
|
+
mcp?.disconnect(name);
|
|
5679
6909
|
removeMcpServer(name);
|
|
5680
6910
|
publishMcpCatalog();
|
|
5681
6911
|
},
|
|
@@ -5694,7 +6924,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
5694
6924
|
const n = (pendingSent.get(key) ?? 1) - 1;
|
|
5695
6925
|
if (n > 0) pendingSent.set(key, n);
|
|
5696
6926
|
else pendingSent.delete(key);
|
|
5697
|
-
tui.print(`${
|
|
6927
|
+
tui.print(`${c3.dim}failed to send: ${e instanceof Error ? e.message : String(e)}${c3.reset}`);
|
|
5698
6928
|
return;
|
|
5699
6929
|
}
|
|
5700
6930
|
interrupting = false;
|
|
@@ -5711,16 +6941,16 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
5711
6941
|
try {
|
|
5712
6942
|
await api.compact(threadId);
|
|
5713
6943
|
} catch (err) {
|
|
5714
|
-
tui.print(`${
|
|
6944
|
+
tui.print(`${c3.red}\u2717${c3.reset} couldn't start compaction: ${err.message}`);
|
|
5715
6945
|
}
|
|
5716
6946
|
};
|
|
5717
6947
|
const runAccountCommand = async () => {
|
|
5718
|
-
tui.print(`${
|
|
6948
|
+
tui.print(`${c3.gray}Opening your account\u2026${c3.reset}`);
|
|
5719
6949
|
const link = await api.accountLink(threadId).catch(() => null);
|
|
5720
6950
|
const target = link?.url ?? "https://standardcode.ai/account";
|
|
5721
6951
|
openUrl(target);
|
|
5722
6952
|
tui.print(
|
|
5723
|
-
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}`
|
|
5724
6954
|
);
|
|
5725
6955
|
};
|
|
5726
6956
|
const ordinal = (n) => {
|
|
@@ -5731,24 +6961,24 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
5731
6961
|
const renderUpgradePanel = (q) => {
|
|
5732
6962
|
const dots = [];
|
|
5733
6963
|
for (let i = 0; i < q.max; i++) {
|
|
5734
|
-
if (i < q.current) dots.push(`${
|
|
5735
|
-
else if (i === q.current) dots.push(`${
|
|
5736
|
-
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}`);
|
|
5737
6967
|
}
|
|
5738
6968
|
const cost = fmtCost(q);
|
|
5739
6969
|
const lines = [
|
|
5740
6970
|
"",
|
|
5741
|
-
`${
|
|
6971
|
+
`${c3.bold}${gradientText("\u2726 Add a parallel session")}${c3.reset}`,
|
|
5742
6972
|
"",
|
|
5743
|
-
`${dots.join(" ")} ${
|
|
6973
|
+
`${dots.join(" ")} ${c3.dim}${q.current} of ${q.current} session${q.current === 1 ? "" : "s"} in use${c3.reset}`
|
|
5744
6974
|
];
|
|
5745
6975
|
if (q.ends_trial) {
|
|
5746
6976
|
lines.push(
|
|
5747
|
-
`${
|
|
5748
|
-
`${
|
|
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}`
|
|
5749
6979
|
);
|
|
5750
6980
|
} else if (cost) {
|
|
5751
|
-
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}.`);
|
|
5752
6982
|
} else {
|
|
5753
6983
|
lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 prorated on your next invoice.`);
|
|
5754
6984
|
}
|
|
@@ -5761,7 +6991,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
5761
6991
|
try {
|
|
5762
6992
|
if (opts.auto) {
|
|
5763
6993
|
tui.print(
|
|
5764
|
-
`${
|
|
6994
|
+
`${c3.yellow}You're out of simultaneous sessions \u2014 another Standard Code session is using your slot.${c3.reset}`
|
|
5765
6995
|
);
|
|
5766
6996
|
}
|
|
5767
6997
|
const quote = await api.sessionsQuote(threadId);
|
|
@@ -5769,16 +6999,16 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
5769
6999
|
const link = await api.accountLink(threadId).catch(() => null);
|
|
5770
7000
|
const target = link?.url ?? "https://standardcode.ai/account";
|
|
5771
7001
|
tui.print(
|
|
5772
|
-
`${
|
|
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}`
|
|
5773
7003
|
);
|
|
5774
7004
|
openUrl(target);
|
|
5775
|
-
tui.print(`${
|
|
7005
|
+
tui.print(`${c3.gray}\u2192 opened ${target} to manage your plan${c3.reset}`);
|
|
5776
7006
|
return;
|
|
5777
7007
|
}
|
|
5778
7008
|
if (quote.current >= quote.max) {
|
|
5779
7009
|
tui.print(
|
|
5780
|
-
`${
|
|
5781
|
-
${
|
|
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}`
|
|
5782
7012
|
);
|
|
5783
7013
|
return;
|
|
5784
7014
|
}
|
|
@@ -5790,25 +7020,25 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5790
7020
|
{ label: "Not now", value: "no" }
|
|
5791
7021
|
]);
|
|
5792
7022
|
if (choice !== "go") {
|
|
5793
|
-
tui.print(`${
|
|
7023
|
+
tui.print(`${c3.gray}No change made \u2014 you can upgrade anytime with /upgrade.${c3.reset}`);
|
|
5794
7024
|
return;
|
|
5795
7025
|
}
|
|
5796
|
-
tui.print(`${
|
|
7026
|
+
tui.print(`${c3.gray}Applying\u2026${c3.reset}`);
|
|
5797
7027
|
let applied;
|
|
5798
7028
|
try {
|
|
5799
7029
|
applied = await api.sessionsUpgrade(threadId, quote.sessions);
|
|
5800
7030
|
} catch (e) {
|
|
5801
|
-
tui.print(`${
|
|
7031
|
+
tui.print(`${c3.red}\u2717${c3.reset} Upgrade failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
5802
7032
|
return;
|
|
5803
7033
|
}
|
|
5804
7034
|
if (!applied?.ok) {
|
|
5805
|
-
tui.print(`${
|
|
7035
|
+
tui.print(`${c3.red}\u2717${c3.reset} Upgrade failed: ${applied?.error ?? "unknown error"}`);
|
|
5806
7036
|
return;
|
|
5807
7037
|
}
|
|
5808
7038
|
const n = applied.sessions ?? quote.sessions;
|
|
5809
|
-
tui.print(`${
|
|
7039
|
+
tui.print(`${c3.green}\u2713${c3.reset} ${c3.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c3.reset}`);
|
|
5810
7040
|
if (opts.auto && lastSent) {
|
|
5811
|
-
tui.print(`${
|
|
7041
|
+
tui.print(`${c3.gray}Continuing\u2026${c3.reset}`);
|
|
5812
7042
|
await sendNow(lastSent.text, lastSent.images);
|
|
5813
7043
|
}
|
|
5814
7044
|
} finally {
|
|
@@ -5852,14 +7082,24 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5852
7082
|
},
|
|
5853
7083
|
run: () => runApprovalsMenu(tui, perm, () => saveApprovals(api, threadId, perm))
|
|
5854
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
|
+
],
|
|
5855
7098
|
{
|
|
5856
|
-
name: "
|
|
5857
|
-
label: "
|
|
5858
|
-
hint: () =>
|
|
5859
|
-
|
|
5860
|
-
return n ? `${n} connected` : "none";
|
|
5861
|
-
},
|
|
5862
|
-
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)
|
|
5863
7103
|
},
|
|
5864
7104
|
{
|
|
5865
7105
|
name: "skills",
|
|
@@ -5893,20 +7133,20 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5893
7133
|
editingQueued = false;
|
|
5894
7134
|
queued.push({ text, images });
|
|
5895
7135
|
tui.setQueuedCount(queued.length);
|
|
5896
|
-
tui.print(`${
|
|
7136
|
+
tui.print(`${c3.gray}\u23F3 queued:${c3.reset} ${text}`);
|
|
5897
7137
|
return;
|
|
5898
7138
|
}
|
|
5899
7139
|
if (busy) {
|
|
5900
7140
|
queued.push({ text, images });
|
|
5901
7141
|
tui.setQueuedCount(queued.length);
|
|
5902
|
-
tui.print(`${
|
|
7142
|
+
tui.print(`${c3.gray}\u23F3 queued:${c3.reset} ${text} ${c3.dim}(esc to steer now)${c3.reset}`);
|
|
5903
7143
|
} else {
|
|
5904
7144
|
void sendNow(text, images);
|
|
5905
7145
|
}
|
|
5906
7146
|
};
|
|
5907
7147
|
tui.onInterrupt = () => {
|
|
5908
7148
|
if (queued.length > 0) {
|
|
5909
|
-
tui.print(`${
|
|
7149
|
+
tui.print(`${c3.yellow}\u21AA steering \u2014 stopping current work and sending your message\u2026${c3.reset}`);
|
|
5910
7150
|
void api.stop(threadId).catch(() => {
|
|
5911
7151
|
}).then(() => flushQueued());
|
|
5912
7152
|
} else if (busy) {
|
|
@@ -5916,7 +7156,7 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5916
7156
|
liveOut = 0;
|
|
5917
7157
|
tui.setWorking(false);
|
|
5918
7158
|
refreshStatus();
|
|
5919
|
-
tui.print(`${
|
|
7159
|
+
tui.print(`${c3.yellow}[interrupted by user]${c3.reset}`);
|
|
5920
7160
|
void api.stop(threadId).catch(() => {
|
|
5921
7161
|
});
|
|
5922
7162
|
}
|
|
@@ -5934,15 +7174,27 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5934
7174
|
return true;
|
|
5935
7175
|
};
|
|
5936
7176
|
events.connect();
|
|
5937
|
-
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;
|
|
5938
7179
|
void api.getGoal(threadId).then((g) => tui.setGoal(g)).catch(() => {
|
|
5939
7180
|
});
|
|
5940
7181
|
attaching.stop();
|
|
5941
|
-
tui.banner(
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
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
|
+
}
|
|
5946
7198
|
if (resumed) await printHistory(api, threadId, tui);
|
|
5947
7199
|
try {
|
|
5948
7200
|
(await api.getMessages(threadId, 200)).forEach((m) => shownIds.add(m.id));
|
|
@@ -5951,22 +7203,50 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5951
7203
|
const runningProcs = (await registry.list()).filter((p) => p.status === "running");
|
|
5952
7204
|
if (runningProcs.length) {
|
|
5953
7205
|
tui.print(
|
|
5954
|
-
`${
|
|
7206
|
+
`${c3.cyan}\u2699 ${runningProcs.length} background process${runningProcs.length === 1 ? "" : "es"} running:${c3.reset}`
|
|
5955
7207
|
);
|
|
5956
|
-
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}`);
|
|
5957
7209
|
}
|
|
5958
7210
|
refreshBgCount();
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
const
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
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
|
+
}
|
|
5966
7220
|
}
|
|
7221
|
+
publishMcpCatalog();
|
|
5967
7222
|
}
|
|
5968
|
-
publishMcpCatalog();
|
|
5969
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
|
+
};
|
|
5970
7250
|
const poll = async () => {
|
|
5971
7251
|
let msgs;
|
|
5972
7252
|
try {
|
|
@@ -5985,7 +7265,7 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
5985
7265
|
continue;
|
|
5986
7266
|
}
|
|
5987
7267
|
if (m.role === "assistant" && text) printAssistant(tui, text);
|
|
5988
|
-
else if (m.role === "system" && text) tui.print(`${
|
|
7268
|
+
else if (m.role === "system" && text) tui.print(`${c3.dim}${text}${c3.reset}`);
|
|
5989
7269
|
else if (m.role === "user" && text) {
|
|
5990
7270
|
const pending = pendingSent.get(text) ?? 0;
|
|
5991
7271
|
if (pending > 0) {
|
|
@@ -6011,6 +7291,8 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
6011
7291
|
if (queued.length > 0 && !editingQueued) await flushQueued();
|
|
6012
7292
|
}
|
|
6013
7293
|
refreshBgCount();
|
|
7294
|
+
void relayApprovals().catch(() => {
|
|
7295
|
+
});
|
|
6014
7296
|
try {
|
|
6015
7297
|
const logs = await api.getLogs(threadId, 100);
|
|
6016
7298
|
let landed = 0;
|
|
@@ -6047,14 +7329,14 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
6047
7329
|
await sessionEnded;
|
|
6048
7330
|
clearInterval(pollTimer);
|
|
6049
7331
|
clearInterval(heartbeatPoll);
|
|
6050
|
-
const stopped = api.stop(threadId).catch(() => {
|
|
6051
|
-
});
|
|
7332
|
+
const stopped = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
|
|
7333
|
+
}) : Promise.resolve();
|
|
6052
7334
|
const procsStopped = host.stopAllLocalProcesses().catch(() => 0);
|
|
6053
|
-
bridge
|
|
7335
|
+
bridge?.close();
|
|
6054
7336
|
stream.close();
|
|
6055
7337
|
events.close();
|
|
6056
7338
|
subActivity.closeAll();
|
|
6057
|
-
mcp
|
|
7339
|
+
mcp?.closeAll();
|
|
6058
7340
|
const [, killed] = await Promise.race([
|
|
6059
7341
|
Promise.all([stopped, procsStopped]),
|
|
6060
7342
|
new Promise((r) => setTimeout(() => r([void 0, 0]), 1500))
|
|
@@ -6067,16 +7349,16 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
|
|
|
6067
7349
|
tui.setStep(null, 0);
|
|
6068
7350
|
tui.setBackgroundCount(0);
|
|
6069
7351
|
if (killed > 0) {
|
|
6070
|
-
tui.print(`${
|
|
7352
|
+
tui.print(`${c3.cyan}\u2699${c3.reset} Stopped ${killed} background process${killed === 1 ? "" : "es"}.`);
|
|
6071
7353
|
}
|
|
6072
|
-
tui.print(`${
|
|
7354
|
+
tui.print(`${c3.dim}\u2500\u2500 conversation cleared \u2014 starting a fresh session \u2500\u2500${c3.reset}`);
|
|
6073
7355
|
}
|
|
6074
7356
|
async function runSkillsMenu(tui, skills) {
|
|
6075
7357
|
let list;
|
|
6076
7358
|
try {
|
|
6077
7359
|
list = await skills.list();
|
|
6078
7360
|
} catch (e) {
|
|
6079
|
-
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}`);
|
|
6080
7362
|
return;
|
|
6081
7363
|
}
|
|
6082
7364
|
const INSTALL = "__install__";
|
|
@@ -6087,7 +7369,7 @@ async function runSkillsMenu(tui, skills) {
|
|
|
6087
7369
|
}));
|
|
6088
7370
|
items.push({ label: "\uFF0B Install a skill\u2026", hint: "find & install", value: INSTALL });
|
|
6089
7371
|
const picked = await tui.select(
|
|
6090
|
-
`${
|
|
7372
|
+
`${c3.bold}Agent skills${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c3.reset}`,
|
|
6091
7373
|
items
|
|
6092
7374
|
);
|
|
6093
7375
|
if (!picked) return;
|
|
@@ -6100,8 +7382,8 @@ async function runSkillsMenu(tui, skills) {
|
|
|
6100
7382
|
return;
|
|
6101
7383
|
}
|
|
6102
7384
|
const skill = list.find((s) => s.name === picked);
|
|
6103
|
-
tui.print(`${
|
|
6104
|
-
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}`, [
|
|
6105
7387
|
skill.enabled ? { label: "Disable (hide from the agent)", value: "disable" } : { label: "Enable", value: "enable" },
|
|
6106
7388
|
{ label: "View files", value: "files" },
|
|
6107
7389
|
{ label: "Remove this skill", value: "remove" },
|
|
@@ -6110,20 +7392,20 @@ async function runSkillsMenu(tui, skills) {
|
|
|
6110
7392
|
try {
|
|
6111
7393
|
if (action === "enable" || action === "disable") {
|
|
6112
7394
|
await skills.setEnabled(picked, action === "enable");
|
|
6113
|
-
tui.print(`${
|
|
7395
|
+
tui.print(`${c3.gray}${action}d ${picked}${c3.reset}`);
|
|
6114
7396
|
} else if (action === "files") {
|
|
6115
|
-
for (const f of skill.files) tui.print(` ${
|
|
7397
|
+
for (const f of skill.files) tui.print(` ${c3.gray}${f}${c3.reset}`);
|
|
6116
7398
|
} else if (action === "remove") {
|
|
6117
7399
|
await skills.remove(picked);
|
|
6118
|
-
tui.print(`${
|
|
7400
|
+
tui.print(`${c3.gray}removed ${picked}${c3.reset}`);
|
|
6119
7401
|
}
|
|
6120
7402
|
} catch (e) {
|
|
6121
|
-
tui.print(`${
|
|
7403
|
+
tui.print(`${c3.red}\u2717 ${e instanceof Error ? e.message : String(e)}${c3.reset}`);
|
|
6122
7404
|
}
|
|
6123
7405
|
}
|
|
6124
7406
|
async function runLevelMenu(tui, perm) {
|
|
6125
7407
|
const picked = await tui.select(
|
|
6126
|
-
`${
|
|
7408
|
+
`${c3.bold}Auto-accept level${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 shift-tab cycles)${c3.reset}`,
|
|
6127
7409
|
LEVELS.map((l) => ({
|
|
6128
7410
|
label: levelLabel(l),
|
|
6129
7411
|
hint: l === tui.level ? "current" : "",
|
|
@@ -6135,28 +7417,51 @@ async function runLevelMenu(tui, perm) {
|
|
|
6135
7417
|
perm.level = picked;
|
|
6136
7418
|
}
|
|
6137
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
|
+
}
|
|
6138
7443
|
function showKeybindings(tui) {
|
|
6139
|
-
tui.print(`${
|
|
6140
|
-
tui.print(`${
|
|
6141
|
-
tui.print(`${
|
|
6142
|
-
tui.print(`${
|
|
6143
|
-
tui.print(`${
|
|
6144
|
-
tui.print(`${
|
|
6145
|
-
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`);
|
|
6146
7451
|
}
|
|
6147
7452
|
async function runUpdateCommand(tui) {
|
|
6148
7453
|
const version = readVersion();
|
|
6149
7454
|
const result = await forceCheckForUpdate(version);
|
|
6150
7455
|
if (!result) {
|
|
6151
|
-
tui.print(`${
|
|
7456
|
+
tui.print(`${c3.green}\u2713${c3.reset} ${c3.gray}@standardagents/code${c3.reset} is up to date (v${version})`);
|
|
6152
7457
|
return;
|
|
6153
7458
|
}
|
|
6154
7459
|
const { latest } = result;
|
|
6155
7460
|
tui.print(`
|
|
6156
|
-
${
|
|
7461
|
+
${c3.yellow}\u27F3${c3.reset} Update available: ${c3.gray}v${version}${c3.reset} \u2192 ${c3.green}v${latest}${c3.reset}`);
|
|
6157
7462
|
const pm = detectPackageManager();
|
|
6158
7463
|
if (!pm) {
|
|
6159
|
-
tui.print(` ${
|
|
7464
|
+
tui.print(` ${c3.gray}This is a source checkout \u2014 pull the repo to update.${c3.reset}`);
|
|
6160
7465
|
return;
|
|
6161
7466
|
}
|
|
6162
7467
|
const { display } = updateCommand(pm);
|
|
@@ -6165,28 +7470,28 @@ async function runUpdateCommand(tui) {
|
|
|
6165
7470
|
{ label: "No, skip", value: "no" }
|
|
6166
7471
|
]);
|
|
6167
7472
|
if (choice === "yes") {
|
|
6168
|
-
tui.print(` ${
|
|
7473
|
+
tui.print(` ${c3.gray}Running ${display}\u2026${c3.reset}`);
|
|
6169
7474
|
const { ok, output: pmOutput } = await runUpdate(pm);
|
|
6170
7475
|
if (ok) {
|
|
6171
|
-
tui.print(` ${
|
|
7476
|
+
tui.print(` ${c3.green}\u2713${c3.reset} Updated to v${latest}. Restart to use the new version.`);
|
|
6172
7477
|
} else {
|
|
6173
|
-
tui.print(` ${
|
|
7478
|
+
tui.print(` ${c3.red}\u2717${c3.reset} Update failed:`);
|
|
6174
7479
|
for (const line of pmOutput.trim().split("\n").slice(-6)) {
|
|
6175
|
-
tui.print(` ${
|
|
7480
|
+
tui.print(` ${c3.dim}${line}${c3.reset}`);
|
|
6176
7481
|
}
|
|
6177
7482
|
}
|
|
6178
7483
|
} else {
|
|
6179
|
-
tui.print(` ${
|
|
7484
|
+
tui.print(` ${c3.gray}Skipped. Run /update later.${c3.reset}`);
|
|
6180
7485
|
}
|
|
6181
7486
|
}
|
|
6182
7487
|
async function runProcessMenu(tui, bg) {
|
|
6183
7488
|
const procs = await bg.list();
|
|
6184
7489
|
if (!procs.length) {
|
|
6185
|
-
tui.print(`${
|
|
7490
|
+
tui.print(`${c3.gray}No background processes for this session.${c3.reset}`);
|
|
6186
7491
|
return;
|
|
6187
7492
|
}
|
|
6188
7493
|
const items = procs.map((p) => {
|
|
6189
|
-
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}`;
|
|
6190
7495
|
return {
|
|
6191
7496
|
label: `${p.description || p.command}`,
|
|
6192
7497
|
hint: `${p.id} \xB7 ${status}`,
|
|
@@ -6194,22 +7499,22 @@ async function runProcessMenu(tui, bg) {
|
|
|
6194
7499
|
};
|
|
6195
7500
|
});
|
|
6196
7501
|
const picked = await tui.select(
|
|
6197
|
-
`${
|
|
7502
|
+
`${c3.bold}Background processes${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter to manage \xB7 esc to close)${c3.reset}`,
|
|
6198
7503
|
items
|
|
6199
7504
|
);
|
|
6200
7505
|
if (!picked) return;
|
|
6201
7506
|
const proc = procs.find((p) => p.id === picked);
|
|
6202
7507
|
if (!proc || proc.status !== "running") {
|
|
6203
|
-
tui.print(`${
|
|
7508
|
+
tui.print(`${c3.gray}${picked} is not running.${c3.reset}`);
|
|
6204
7509
|
return;
|
|
6205
7510
|
}
|
|
6206
|
-
const action = await tui.select(`${
|
|
7511
|
+
const action = await tui.select(`${c3.bold}${proc.description || proc.command}${c3.reset}`, [
|
|
6207
7512
|
{ label: "Stop this process", value: "stop" },
|
|
6208
7513
|
{ label: "Leave it running", value: "leave" }
|
|
6209
7514
|
]);
|
|
6210
7515
|
if (action === "stop") {
|
|
6211
7516
|
await bg.stop(picked);
|
|
6212
|
-
tui.print(`${
|
|
7517
|
+
tui.print(`${c3.gray}stopped ${picked}${c3.reset}`);
|
|
6213
7518
|
}
|
|
6214
7519
|
}
|
|
6215
7520
|
async function runApprovalsMenu(tui, perm, save) {
|
|
@@ -6217,7 +7522,7 @@ async function runApprovalsMenu(tui, perm, save) {
|
|
|
6217
7522
|
const risks = Array.from(perm.allowRisk).sort((a, b) => a - b);
|
|
6218
7523
|
if (!tools.length && !risks.length) {
|
|
6219
7524
|
tui.print(
|
|
6220
|
-
`${
|
|
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}`
|
|
6221
7526
|
);
|
|
6222
7527
|
return;
|
|
6223
7528
|
}
|
|
@@ -6227,22 +7532,22 @@ async function runApprovalsMenu(tui, perm, save) {
|
|
|
6227
7532
|
{ label: "Clear all approvals", hint: "", value: "clear" }
|
|
6228
7533
|
];
|
|
6229
7534
|
const picked = await tui.select(
|
|
6230
|
-
`${
|
|
7535
|
+
`${c3.bold}Approved commands${c3.reset} ${c3.dim}(enter to revoke \xB7 esc to close)${c3.reset}`,
|
|
6231
7536
|
items
|
|
6232
7537
|
);
|
|
6233
7538
|
if (!picked) return;
|
|
6234
7539
|
if (picked === "clear") {
|
|
6235
7540
|
perm.alwaysAllow.clear();
|
|
6236
7541
|
perm.allowRisk.clear();
|
|
6237
|
-
tui.print(`${
|
|
7542
|
+
tui.print(`${c3.gray}cleared all approvals${c3.reset}`);
|
|
6238
7543
|
} else if (picked.startsWith("tool:")) {
|
|
6239
7544
|
const t = picked.slice(5);
|
|
6240
7545
|
perm.alwaysAllow.delete(t);
|
|
6241
|
-
tui.print(`${
|
|
7546
|
+
tui.print(`${c3.gray}revoked tool ${t}${c3.reset}`);
|
|
6242
7547
|
} else if (picked.startsWith("risk:")) {
|
|
6243
7548
|
const r = Number(picked.slice(5));
|
|
6244
7549
|
perm.allowRisk.delete(r);
|
|
6245
|
-
tui.print(`${
|
|
7550
|
+
tui.print(`${c3.gray}revoked level ${r}${c3.reset}`);
|
|
6246
7551
|
}
|
|
6247
7552
|
save();
|
|
6248
7553
|
}
|
|
@@ -6260,7 +7565,7 @@ async function runMcpMenu(tui, mcp) {
|
|
|
6260
7565
|
items.push({ label: "\uFF0B Install a new MCP server\u2026", hint: "find & install", value: INSTALL });
|
|
6261
7566
|
items.push({ label: "Add manually (name: command)\u2026", hint: "advanced", value: ADD_MANUAL });
|
|
6262
7567
|
const picked = await tui.select(
|
|
6263
|
-
`${
|
|
7568
|
+
`${c3.bold}MCP servers${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c3.reset}`,
|
|
6264
7569
|
items
|
|
6265
7570
|
);
|
|
6266
7571
|
if (!picked) return;
|
|
@@ -6274,7 +7579,7 @@ async function runMcpMenu(tui, mcp) {
|
|
|
6274
7579
|
}
|
|
6275
7580
|
const server = configured.find((s) => s.name === picked);
|
|
6276
7581
|
const isConnected = connected.has(picked);
|
|
6277
|
-
const action = await tui.select(`${
|
|
7582
|
+
const action = await tui.select(`${c3.bold}${picked}${c3.reset}`, [
|
|
6278
7583
|
{ label: "View tools", value: "tools" },
|
|
6279
7584
|
isConnected ? { label: "Disconnect", value: "disconnect" } : { label: "Connect", value: "connect" },
|
|
6280
7585
|
server.enabled ? { label: "Disable (don't auto-connect)", value: "disable" } : { label: "Enable (auto-connect on start)", value: "enable" },
|
|
@@ -6284,29 +7589,29 @@ async function runMcpMenu(tui, mcp) {
|
|
|
6284
7589
|
if (action === "tools") {
|
|
6285
7590
|
const entry = mcp.catalog().servers.find((e) => e.name === picked);
|
|
6286
7591
|
if (!entry || entry.status !== "connected") {
|
|
6287
|
-
tui.print(`${
|
|
7592
|
+
tui.print(`${c3.gray}${picked} is not connected \u2014 connect it to list tools.${c3.reset}`);
|
|
6288
7593
|
return;
|
|
6289
7594
|
}
|
|
6290
|
-
if (!entry.tools.length) tui.print(`${
|
|
6291
|
-
for (const t of entry.tools) tui.print(` ${
|
|
6292
|
-
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}`);
|
|
6293
7598
|
} else if (action === "connect") {
|
|
6294
7599
|
const res = await mcp.connect(server);
|
|
6295
|
-
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}`);
|
|
6296
7601
|
} else if (action === "disconnect") {
|
|
6297
7602
|
mcp.disconnect(picked);
|
|
6298
|
-
tui.print(`${
|
|
7603
|
+
tui.print(`${c3.gray}disconnected ${picked}${c3.reset}`);
|
|
6299
7604
|
} else if (action === "enable") {
|
|
6300
7605
|
mcp.setEnabled(picked, true);
|
|
6301
7606
|
const res = await mcp.connect(server);
|
|
6302
|
-
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}`);
|
|
6303
7608
|
} else if (action === "disable") {
|
|
6304
7609
|
mcp.setEnabled(picked, false);
|
|
6305
7610
|
mcp.disconnect(picked);
|
|
6306
|
-
tui.print(`${
|
|
7611
|
+
tui.print(`${c3.gray}disabled + disconnected ${picked}${c3.reset}`);
|
|
6307
7612
|
} else if (action === "remove") {
|
|
6308
7613
|
mcp.remove(picked);
|
|
6309
|
-
tui.print(`${
|
|
7614
|
+
tui.print(`${c3.gray}removed ${picked}${c3.reset}`);
|
|
6310
7615
|
}
|
|
6311
7616
|
}
|
|
6312
7617
|
async function addMcpServer(tui, mcp) {
|
|
@@ -6317,13 +7622,13 @@ async function addMcpServer(tui, mcp) {
|
|
|
6317
7622
|
if (!spec) return;
|
|
6318
7623
|
const cfg = parseServerSpec(spec);
|
|
6319
7624
|
if (!cfg) {
|
|
6320
|
-
tui.print(`${
|
|
7625
|
+
tui.print(`${c3.yellow}couldn't parse that. Use name: command [args]${c3.reset}`);
|
|
6321
7626
|
return;
|
|
6322
7627
|
}
|
|
6323
|
-
tui.print(`${
|
|
7628
|
+
tui.print(`${c3.gray}connecting MCP "${cfg.name}" (${cfg.command} ${cfg.args.join(" ")})\u2026${c3.reset}`);
|
|
6324
7629
|
const res = await mcp.add(cfg);
|
|
6325
|
-
if (res.ok) tui.print(`${
|
|
6326
|
-
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}`);
|
|
6327
7632
|
}
|
|
6328
7633
|
async function installMcpServerFlow(tui, mcp) {
|
|
6329
7634
|
const query = await tui.prompt(
|