@standardagents/code 0.9.11 → 0.10.1
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 +522 -147
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import os8, { homedir } from 'os';
|
|
3
|
+
import path4 from 'path';
|
|
4
4
|
import readline2 from 'readline/promises';
|
|
5
5
|
import { stdout, stdin } from 'process';
|
|
6
|
-
import
|
|
6
|
+
import net from 'net';
|
|
7
|
+
import fs5 from 'fs';
|
|
7
8
|
import fsp from 'fs/promises';
|
|
8
9
|
import crypto from 'crypto';
|
|
9
10
|
import { spawn, execFileSync, spawnSync, execFile } from 'child_process';
|
|
@@ -365,6 +366,25 @@ var ApiClient = class {
|
|
|
365
366
|
user_id: t.user_id ?? null
|
|
366
367
|
})).filter((t) => !me || !t.user_id || t.user_id === me).filter((t) => requireTags.every((tag) => t.tags.includes(tag)));
|
|
367
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Fetch one thread's summary (id, tags, created_at, terminated). Used by the
|
|
371
|
+
* daemon's hub wake handler to resolve a cold thread's runner tag + project
|
|
372
|
+
* before attaching. Returns null if the thread is gone or unreadable.
|
|
373
|
+
*/
|
|
374
|
+
async getThread(threadId) {
|
|
375
|
+
try {
|
|
376
|
+
const t = await this.json(`/api/threads/${threadId}`);
|
|
377
|
+
if (!t || !t.id) return null;
|
|
378
|
+
return {
|
|
379
|
+
id: t.id,
|
|
380
|
+
tags: Array.isArray(t.tags) ? t.tags : [],
|
|
381
|
+
created_at: t.created_at,
|
|
382
|
+
terminated: t.terminated ?? null
|
|
383
|
+
};
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
368
388
|
/** The authenticated user's id (cached). Null when the instance doesn't
|
|
369
389
|
* report one (super-admin sessions, very old instances). */
|
|
370
390
|
meUserId;
|
|
@@ -805,6 +825,185 @@ var Heartbeat = class {
|
|
|
805
825
|
this.onDead();
|
|
806
826
|
}
|
|
807
827
|
};
|
|
828
|
+
var ALLOWED_HOST = "chatgpt.com";
|
|
829
|
+
var ALLOWED_PORT = 443;
|
|
830
|
+
var CHUNK_BYTES = 256 * 1024;
|
|
831
|
+
var TunnelManager = class {
|
|
832
|
+
constructor(send) {
|
|
833
|
+
this.send = send;
|
|
834
|
+
}
|
|
835
|
+
send;
|
|
836
|
+
tunnels = /* @__PURE__ */ new Map();
|
|
837
|
+
/** Whether a stream_* frame belongs to this manager. */
|
|
838
|
+
handles(type) {
|
|
839
|
+
return type === "stream_open" || type === "stream_data" || type === "stream_end" || type === "stream_abort";
|
|
840
|
+
}
|
|
841
|
+
/** Dispatch one decoded stream_* frame. */
|
|
842
|
+
handleFrame(msg) {
|
|
843
|
+
const id = typeof msg.id === "string" ? msg.id : null;
|
|
844
|
+
if (!id) return;
|
|
845
|
+
switch (msg.type) {
|
|
846
|
+
case "stream_open":
|
|
847
|
+
this.open(msg);
|
|
848
|
+
return;
|
|
849
|
+
case "stream_data": {
|
|
850
|
+
const tunnel = this.tunnels.get(id);
|
|
851
|
+
if (!tunnel) return;
|
|
852
|
+
if (typeof msg.data !== "string") return;
|
|
853
|
+
try {
|
|
854
|
+
tunnel.socket.write(Buffer.from(msg.data, "base64"));
|
|
855
|
+
} catch {
|
|
856
|
+
this.fail(id, "write to upstream failed");
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
case "stream_end": {
|
|
861
|
+
const tunnel = this.tunnels.get(id);
|
|
862
|
+
if (tunnel) {
|
|
863
|
+
try {
|
|
864
|
+
tunnel.socket.end();
|
|
865
|
+
} catch {
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
case "stream_abort": {
|
|
871
|
+
this.teardown(id);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
open(frame) {
|
|
877
|
+
const id = frame.id;
|
|
878
|
+
if (frame.tool !== "tcp_tunnel") {
|
|
879
|
+
this.send({ type: "stream_error", id, error: `Unsupported stream tool: ${frame.tool}` });
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const host = typeof frame.meta?.host === "string" ? frame.meta.host : "";
|
|
883
|
+
const port = typeof frame.meta?.port === "number" ? frame.meta.port : 0;
|
|
884
|
+
if (host !== ALLOWED_HOST || port !== ALLOWED_PORT) {
|
|
885
|
+
this.send({
|
|
886
|
+
type: "stream_error",
|
|
887
|
+
id,
|
|
888
|
+
error: `Tunnel refused: only ${ALLOWED_HOST}:${ALLOWED_PORT} is allowed (requested ${host}:${port}).`
|
|
889
|
+
});
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
let opened = false;
|
|
893
|
+
const socket = net.connect({ host, port });
|
|
894
|
+
socket.setNoDelay(true);
|
|
895
|
+
this.tunnels.set(id, { socket });
|
|
896
|
+
socket.on("connect", () => {
|
|
897
|
+
opened = true;
|
|
898
|
+
this.send({ type: "stream_opened", id });
|
|
899
|
+
});
|
|
900
|
+
socket.on("data", (data) => {
|
|
901
|
+
for (let i = 0; i < data.length; i += CHUNK_BYTES) {
|
|
902
|
+
const slice = data.subarray(i, i + CHUNK_BYTES);
|
|
903
|
+
this.send({ type: "stream_data", id, data: slice.toString("base64") });
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
socket.on("end", () => {
|
|
907
|
+
this.send({ type: "stream_end", id });
|
|
908
|
+
this.tunnels.delete(id);
|
|
909
|
+
});
|
|
910
|
+
socket.on("error", (error) => {
|
|
911
|
+
if (!opened) {
|
|
912
|
+
this.send({ type: "stream_error", id, error: `Tunnel connect failed: ${error.message}` });
|
|
913
|
+
} else {
|
|
914
|
+
this.send({ type: "stream_abort", id, error: error.message });
|
|
915
|
+
}
|
|
916
|
+
this.tunnels.delete(id);
|
|
917
|
+
});
|
|
918
|
+
socket.on("close", () => {
|
|
919
|
+
if (this.tunnels.has(id)) {
|
|
920
|
+
this.send({ type: "stream_end", id });
|
|
921
|
+
this.tunnels.delete(id);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
fail(id, error) {
|
|
926
|
+
this.send({ type: "stream_abort", id, error });
|
|
927
|
+
this.teardown(id);
|
|
928
|
+
}
|
|
929
|
+
teardown(id) {
|
|
930
|
+
const tunnel = this.tunnels.get(id);
|
|
931
|
+
if (!tunnel) return;
|
|
932
|
+
this.tunnels.delete(id);
|
|
933
|
+
try {
|
|
934
|
+
tunnel.socket.destroy();
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
/** Destroy every open tunnel (bridge teardown). */
|
|
939
|
+
closeAll() {
|
|
940
|
+
for (const id of Array.from(this.tunnels.keys())) {
|
|
941
|
+
this.teardown(id);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
var MAX_ENTRIES = 200;
|
|
946
|
+
var MAX_RESULT_BYTES = 256 * 1024;
|
|
947
|
+
function ledgerDir() {
|
|
948
|
+
return path4.join(os8.homedir(), ".standardagents", "tool-ledger");
|
|
949
|
+
}
|
|
950
|
+
var ToolLedger = class {
|
|
951
|
+
constructor(threadId) {
|
|
952
|
+
this.threadId = threadId;
|
|
953
|
+
const safe = encodeURIComponent(threadId);
|
|
954
|
+
this.file = path4.join(ledgerDir(), `${safe}.json`);
|
|
955
|
+
}
|
|
956
|
+
threadId;
|
|
957
|
+
entries = /* @__PURE__ */ new Map();
|
|
958
|
+
file;
|
|
959
|
+
loaded = false;
|
|
960
|
+
load() {
|
|
961
|
+
if (this.loaded) return;
|
|
962
|
+
this.loaded = true;
|
|
963
|
+
try {
|
|
964
|
+
const raw = fs5.readFileSync(this.file, "utf8");
|
|
965
|
+
const parsed = JSON.parse(raw);
|
|
966
|
+
for (const [id, entry] of Object.entries(parsed)) {
|
|
967
|
+
if (entry && typeof entry.ok === "boolean") this.entries.set(id, entry);
|
|
968
|
+
}
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
/** The cached result for a completed call, or null if we have not run it. */
|
|
973
|
+
get(toolCallId) {
|
|
974
|
+
this.load();
|
|
975
|
+
return this.entries.get(toolCallId) ?? null;
|
|
976
|
+
}
|
|
977
|
+
/** Record a completed call's result, then persist (best effort). */
|
|
978
|
+
record(toolCallId, result) {
|
|
979
|
+
this.load();
|
|
980
|
+
this.entries.set(toolCallId, { ...result, at: Date.now() });
|
|
981
|
+
this.evict();
|
|
982
|
+
this.persist();
|
|
983
|
+
}
|
|
984
|
+
evict() {
|
|
985
|
+
if (this.entries.size <= MAX_ENTRIES) return;
|
|
986
|
+
const overflow = this.entries.size - MAX_ENTRIES;
|
|
987
|
+
let dropped = 0;
|
|
988
|
+
for (const key of this.entries.keys()) {
|
|
989
|
+
if (dropped >= overflow) break;
|
|
990
|
+
this.entries.delete(key);
|
|
991
|
+
dropped++;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
persist() {
|
|
995
|
+
try {
|
|
996
|
+
fs5.mkdirSync(ledgerDir(), { recursive: true });
|
|
997
|
+
const out = {};
|
|
998
|
+
for (const [id, entry] of this.entries) {
|
|
999
|
+
const size = entry.result ? Buffer.byteLength(entry.result) : 0;
|
|
1000
|
+
out[id] = size > MAX_RESULT_BYTES ? { ...entry, result: void 0 } : entry;
|
|
1001
|
+
}
|
|
1002
|
+
fs5.writeFileSync(this.file, JSON.stringify(out), { mode: 384 });
|
|
1003
|
+
} catch {
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
808
1007
|
|
|
809
1008
|
// src/render.ts
|
|
810
1009
|
var RESET = "\x1B[0m";
|
|
@@ -1025,6 +1224,8 @@ var Bridge = class {
|
|
|
1025
1224
|
this.perm = perm;
|
|
1026
1225
|
this.hooks = hooks;
|
|
1027
1226
|
this.identity = identity;
|
|
1227
|
+
this.tunnels = new TunnelManager((payload) => this.send(payload));
|
|
1228
|
+
this.ledger = new ToolLedger(this.threadId);
|
|
1028
1229
|
}
|
|
1029
1230
|
api;
|
|
1030
1231
|
threadId;
|
|
@@ -1042,6 +1243,14 @@ var Bridge = class {
|
|
|
1042
1243
|
// Durable forwarded calls we've started handling, so a server re-send (after a
|
|
1043
1244
|
// reconnect) doesn't prompt or run them twice.
|
|
1044
1245
|
handledDurable = /* @__PURE__ */ new Set();
|
|
1246
|
+
// Raw TCP tunnels: the provider owns TLS over these runtime bytes; this
|
|
1247
|
+
// execution-owner bridge only shuttles ciphertext and tears it down on close.
|
|
1248
|
+
tunnels;
|
|
1249
|
+
// Durable, exactly-once ledger of completed forwarded calls. Survives a crash
|
|
1250
|
+
// AFTER execution but BEFORE delivery: on redelivery we return the cached
|
|
1251
|
+
// result instead of re-running the side-effecting operation. A cache — the
|
|
1252
|
+
// server's pending table stays the source of truth.
|
|
1253
|
+
ledger;
|
|
1045
1254
|
// Whether this client currently holds execution ownership. Starts true so an
|
|
1046
1255
|
// instance that predates the owner concept (no owner field in bridge_ready)
|
|
1047
1256
|
// behaves exactly as before; an owner-aware instance sets it on every connect.
|
|
@@ -1144,6 +1353,7 @@ var Bridge = class {
|
|
|
1144
1353
|
if (this.ws !== ws) return;
|
|
1145
1354
|
this.ws = null;
|
|
1146
1355
|
this.stopHeartbeat();
|
|
1356
|
+
this.tunnels.closeAll();
|
|
1147
1357
|
this.scheduleReconnect();
|
|
1148
1358
|
}
|
|
1149
1359
|
scheduleReconnect() {
|
|
@@ -1176,6 +1386,7 @@ var Bridge = class {
|
|
|
1176
1386
|
close() {
|
|
1177
1387
|
this.closed = true;
|
|
1178
1388
|
this.stopHeartbeat();
|
|
1389
|
+
this.tunnels.closeAll();
|
|
1179
1390
|
if (this.reconnectTimer) {
|
|
1180
1391
|
clearTimeout(this.reconnectTimer);
|
|
1181
1392
|
this.reconnectTimer = null;
|
|
@@ -1249,6 +1460,11 @@ var Bridge = class {
|
|
|
1249
1460
|
this.hooks.onSuperseded?.();
|
|
1250
1461
|
return;
|
|
1251
1462
|
}
|
|
1463
|
+
if (this.tunnels.handles(msg.type)) {
|
|
1464
|
+
if (!this.owner) return;
|
|
1465
|
+
this.tunnels.handleFrame(msg);
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1252
1468
|
if (msg.type !== "tool_request") return;
|
|
1253
1469
|
if (!this.owner) return;
|
|
1254
1470
|
const req = msg;
|
|
@@ -1281,6 +1497,11 @@ var Bridge = class {
|
|
|
1281
1497
|
if (req.durable && req.toolCallId) {
|
|
1282
1498
|
if (this.handledDurable.has(req.toolCallId)) return;
|
|
1283
1499
|
this.handledDurable.add(req.toolCallId);
|
|
1500
|
+
const cached = this.ledger.get(req.toolCallId);
|
|
1501
|
+
if (cached) {
|
|
1502
|
+
this.respond(req, cached.ok, cached.result, cached.error);
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1284
1505
|
}
|
|
1285
1506
|
const summary = describe(req);
|
|
1286
1507
|
let effectiveRisk = typeof req.risk === "number" ? req.risk : req.requestPermission ? 3 : 1;
|
|
@@ -1328,6 +1549,13 @@ var Bridge = class {
|
|
|
1328
1549
|
} finally {
|
|
1329
1550
|
this.hooks.onStatus?.(callKey, null);
|
|
1330
1551
|
}
|
|
1552
|
+
if (req.durable && req.toolCallId) {
|
|
1553
|
+
this.ledger.record(req.toolCallId, {
|
|
1554
|
+
ok: result.ok,
|
|
1555
|
+
result: result.ok ? result.result ?? "" : void 0,
|
|
1556
|
+
error: result.ok ? void 0 : result.error
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1331
1559
|
if (result.ok) {
|
|
1332
1560
|
const { display, detail } = activityPresentation(req, summary);
|
|
1333
1561
|
this.hooks.onActivity(`\u2713 ${display}${detailSuffix(req.tool, result.result)}`, detail);
|
|
@@ -1433,7 +1661,7 @@ function detailSuffix(tool, result) {
|
|
|
1433
1661
|
const lines = result.split("\n").length;
|
|
1434
1662
|
return ` (${lines} line${lines === 1 ? "" : "s"})`;
|
|
1435
1663
|
}
|
|
1436
|
-
var LOG_DIR =
|
|
1664
|
+
var LOG_DIR = path4.join(os8.homedir(), ".standardagents", "process-logs");
|
|
1437
1665
|
var KEY2 = "bg_processes";
|
|
1438
1666
|
function isAlive(pid) {
|
|
1439
1667
|
try {
|
|
@@ -1510,11 +1738,11 @@ var ProcessRegistry = class {
|
|
|
1510
1738
|
}
|
|
1511
1739
|
};
|
|
1512
1740
|
function configFile() {
|
|
1513
|
-
return process.env.STANDARDAGENTS_MCP_CONFIG ||
|
|
1741
|
+
return process.env.STANDARDAGENTS_MCP_CONFIG || path4.join(os8.homedir(), ".standardagents", "mcp.json");
|
|
1514
1742
|
}
|
|
1515
1743
|
function loadMcpConfig() {
|
|
1516
1744
|
try {
|
|
1517
|
-
const raw =
|
|
1745
|
+
const raw = fs5.readFileSync(configFile(), "utf8");
|
|
1518
1746
|
const parsed = JSON.parse(raw);
|
|
1519
1747
|
if (!parsed.servers || typeof parsed.servers !== "object") parsed.servers = {};
|
|
1520
1748
|
return parsed;
|
|
@@ -1545,8 +1773,8 @@ function setMcpServerEnabled(name, enabled) {
|
|
|
1545
1773
|
}
|
|
1546
1774
|
function write(cfg) {
|
|
1547
1775
|
const file2 = configFile();
|
|
1548
|
-
|
|
1549
|
-
|
|
1776
|
+
fs5.mkdirSync(path4.dirname(file2), { recursive: true });
|
|
1777
|
+
fs5.writeFileSync(file2, JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
1550
1778
|
}
|
|
1551
1779
|
function parseServerSpec(spec) {
|
|
1552
1780
|
const trimmed = spec.trim();
|
|
@@ -1629,13 +1857,13 @@ var HostTools = class {
|
|
|
1629
1857
|
/** Resolve a user/model-supplied path against the project directory. */
|
|
1630
1858
|
resolve(p) {
|
|
1631
1859
|
if (!p || p === ".") return this.projectDir;
|
|
1632
|
-
return
|
|
1860
|
+
return path4.resolve(this.projectDir, p);
|
|
1633
1861
|
}
|
|
1634
1862
|
/** True when the resolved path escapes the project directory. */
|
|
1635
1863
|
isOutsideProject(p) {
|
|
1636
1864
|
const abs = this.resolve(p);
|
|
1637
|
-
const rel =
|
|
1638
|
-
return rel.startsWith("..") ||
|
|
1865
|
+
const rel = path4.relative(this.projectDir, abs);
|
|
1866
|
+
return rel.startsWith("..") || path4.isAbsolute(rel);
|
|
1639
1867
|
}
|
|
1640
1868
|
async execute(tool, args) {
|
|
1641
1869
|
try {
|
|
@@ -1722,18 +1950,18 @@ var HostTools = class {
|
|
|
1722
1950
|
const matches = await this.walkGlob(base, pattern);
|
|
1723
1951
|
return { ok: true, result: matches.slice(0, 300).join("\n") || "(no files)" };
|
|
1724
1952
|
}
|
|
1725
|
-
const rel = rg.stdout.trim().split("\n").filter(Boolean).map((p) =>
|
|
1953
|
+
const rel = rg.stdout.trim().split("\n").filter(Boolean).map((p) => path4.relative(this.projectDir, p)).slice(0, 300);
|
|
1726
1954
|
return { ok: true, result: rel.join("\n") || "(no files)" };
|
|
1727
1955
|
}
|
|
1728
1956
|
async writeFile(args) {
|
|
1729
1957
|
const file2 = this.resolve(String(args.path || ""));
|
|
1730
1958
|
const content = String(args.content ?? "");
|
|
1731
|
-
await fsp.mkdir(
|
|
1732
|
-
const existed =
|
|
1959
|
+
await fsp.mkdir(path4.dirname(file2), { recursive: true });
|
|
1960
|
+
const existed = fs5.existsSync(file2);
|
|
1733
1961
|
await fsp.writeFile(file2, content, "utf8");
|
|
1734
1962
|
return {
|
|
1735
1963
|
ok: true,
|
|
1736
|
-
result: `${existed ? "Overwrote" : "Created"} ${
|
|
1964
|
+
result: `${existed ? "Overwrote" : "Created"} ${path4.relative(this.projectDir, file2)} (${Buffer.byteLength(content)} bytes)`
|
|
1737
1965
|
};
|
|
1738
1966
|
}
|
|
1739
1967
|
async editFile(args) {
|
|
@@ -1752,7 +1980,7 @@ var HostTools = class {
|
|
|
1752
1980
|
}
|
|
1753
1981
|
const updated = replaceAll ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
|
|
1754
1982
|
await fsp.writeFile(file2, updated, "utf8");
|
|
1755
|
-
return { ok: true, result: `Edited ${
|
|
1983
|
+
return { ok: true, result: `Edited ${path4.relative(this.projectDir, file2)} (${count} replacement${count === 1 ? "" : "s"})` };
|
|
1756
1984
|
}
|
|
1757
1985
|
/**
|
|
1758
1986
|
* Copy a file from the THREAD filesystem (e.g. a generated /attachments/*
|
|
@@ -1769,12 +1997,12 @@ var HostTools = class {
|
|
|
1769
1997
|
const bytes = await this.api.fetchFile(this.threadId, source);
|
|
1770
1998
|
if (!bytes) return { ok: false, error: `Could not download ${source} from the thread filesystem.` };
|
|
1771
1999
|
const dest = this.resolve(destArg);
|
|
1772
|
-
await fsp.mkdir(
|
|
1773
|
-
const existed =
|
|
2000
|
+
await fsp.mkdir(path4.dirname(dest), { recursive: true });
|
|
2001
|
+
const existed = fs5.existsSync(dest);
|
|
1774
2002
|
await fsp.writeFile(dest, bytes);
|
|
1775
2003
|
return {
|
|
1776
2004
|
ok: true,
|
|
1777
|
-
result: `${existed ? "Overwrote" : "Saved"} ${
|
|
2005
|
+
result: `${existed ? "Overwrote" : "Saved"} ${path4.relative(this.projectDir, dest)} (${bytes.length} bytes) from ${source}`
|
|
1778
2006
|
};
|
|
1779
2007
|
}
|
|
1780
2008
|
/**
|
|
@@ -1812,16 +2040,16 @@ var HostTools = class {
|
|
|
1812
2040
|
}
|
|
1813
2041
|
}
|
|
1814
2042
|
const hash = crypto.createHash("sha256").update(JSON.stringify(files)).digest("hex").slice(0, 12);
|
|
1815
|
-
const skillDir =
|
|
2043
|
+
const skillDir = path4.join(os8.tmpdir(), "standardcode-skills", `${skill}-${hash}`);
|
|
1816
2044
|
for (const f of files) {
|
|
1817
|
-
const dest =
|
|
1818
|
-
if (
|
|
2045
|
+
const dest = path4.resolve(skillDir, f.path);
|
|
2046
|
+
if (path4.relative(skillDir, dest).startsWith("..")) {
|
|
1819
2047
|
return { ok: false, error: `Skill file escapes its directory: ${f.path}` };
|
|
1820
2048
|
}
|
|
1821
|
-
await fsp.mkdir(
|
|
2049
|
+
await fsp.mkdir(path4.dirname(dest), { recursive: true });
|
|
1822
2050
|
await fsp.writeFile(dest, f.content, "utf8");
|
|
1823
2051
|
}
|
|
1824
|
-
const entryPath =
|
|
2052
|
+
const entryPath = path4.resolve(skillDir, entry);
|
|
1825
2053
|
const entryContent = files.find((f) => f.path === entry).content;
|
|
1826
2054
|
let cmd;
|
|
1827
2055
|
let argv;
|
|
@@ -1830,7 +2058,7 @@ var HostTools = class {
|
|
|
1830
2058
|
cmd = entryPath;
|
|
1831
2059
|
argv = scriptArgs;
|
|
1832
2060
|
} else {
|
|
1833
|
-
const ext =
|
|
2061
|
+
const ext = path4.extname(entry).toLowerCase();
|
|
1834
2062
|
const interp = {
|
|
1835
2063
|
".py": ["python3"],
|
|
1836
2064
|
".sh": ["bash"],
|
|
@@ -1900,15 +2128,15 @@ ${truncated}`
|
|
|
1900
2128
|
const command = String(args.command || "");
|
|
1901
2129
|
if (!command.trim()) return { ok: false, error: "command is required" };
|
|
1902
2130
|
const cwd = args.cwd ? this.resolve(String(args.cwd)) : this.projectDir;
|
|
1903
|
-
if (!
|
|
2131
|
+
if (!fs5.existsSync(cwd)) {
|
|
1904
2132
|
return { ok: false, error: `cwd does not exist: ${cwd}` };
|
|
1905
2133
|
}
|
|
1906
2134
|
const id = crypto.randomUUID().slice(0, 8);
|
|
1907
|
-
const logPath =
|
|
2135
|
+
const logPath = path4.join(LOG_DIR, `${id}.log`);
|
|
1908
2136
|
let out;
|
|
1909
2137
|
try {
|
|
1910
2138
|
await fsp.mkdir(LOG_DIR, { recursive: true });
|
|
1911
|
-
out =
|
|
2139
|
+
out = fs5.openSync(logPath, "a");
|
|
1912
2140
|
} catch (err) {
|
|
1913
2141
|
return { ok: false, error: `Could not open log file: ${err instanceof Error ? err.message : String(err)}` };
|
|
1914
2142
|
}
|
|
@@ -1916,14 +2144,14 @@ ${truncated}`
|
|
|
1916
2144
|
try {
|
|
1917
2145
|
child = spawn("bash", ["-lc", command], { cwd, detached: true, stdio: ["ignore", out, out] });
|
|
1918
2146
|
} catch (err) {
|
|
1919
|
-
|
|
2147
|
+
fs5.closeSync(out);
|
|
1920
2148
|
return { ok: false, error: `Failed to start: ${err instanceof Error ? err.message : String(err)}` };
|
|
1921
2149
|
}
|
|
1922
2150
|
let spawnError = null;
|
|
1923
2151
|
child.on("error", (err) => {
|
|
1924
2152
|
spawnError = err;
|
|
1925
2153
|
});
|
|
1926
|
-
|
|
2154
|
+
fs5.closeSync(out);
|
|
1927
2155
|
const pid = child.pid;
|
|
1928
2156
|
let earlyExit;
|
|
1929
2157
|
const onEarlyExit = (code) => {
|
|
@@ -2179,10 +2407,10 @@ ${tail}` : " No output was captured.")
|
|
|
2179
2407
|
const entries = await fsp.readdir(dir2, { withFileTypes: true }).catch(() => []);
|
|
2180
2408
|
for (const e of entries) {
|
|
2181
2409
|
if (e.name === ".git" || e.name === "node_modules") continue;
|
|
2182
|
-
const full =
|
|
2410
|
+
const full = path4.join(dir2, e.name);
|
|
2183
2411
|
if (e.isDirectory()) await walk(full);
|
|
2184
2412
|
else {
|
|
2185
|
-
const rel =
|
|
2413
|
+
const rel = path4.relative(this.projectDir, full);
|
|
2186
2414
|
if (re.test(rel) || re.test(e.name)) out.push(rel);
|
|
2187
2415
|
}
|
|
2188
2416
|
}
|
|
@@ -3053,14 +3281,37 @@ function parseToolCalls(message) {
|
|
|
3053
3281
|
});
|
|
3054
3282
|
}
|
|
3055
3283
|
var EMPTY_BOUNDARY_STALE_MS = 15e4;
|
|
3284
|
+
var WAITING_FOR_CLIENT_MARKER = "Waiting for your machine to reconnect";
|
|
3285
|
+
var PENDING_TAIL_STALE_MS = 10 * 6e4;
|
|
3286
|
+
var BUSY_TAIL_STALE_MS = 30 * 6e4;
|
|
3287
|
+
function toMs(raw) {
|
|
3288
|
+
return raw > 1e14 ? raw / 1e3 : raw < 1e12 ? raw * 1e3 : raw;
|
|
3289
|
+
}
|
|
3056
3290
|
function deriveSessionActivity(messages, nowMs = Date.now()) {
|
|
3057
3291
|
const visible = messages.filter((message) => message.silent !== true && message.metadata?.silent !== true).sort((a, b) => createdAt(a) - createdAt(b));
|
|
3058
|
-
if (visible.some((message) => message.status === "pending"))
|
|
3292
|
+
if (visible.some((message) => message.status === "pending")) {
|
|
3293
|
+
const lastPending = [...visible].reverse().find((m) => m.status === "pending");
|
|
3294
|
+
const pendingMs = lastPending ? toMs(createdAt(lastPending)) : 0;
|
|
3295
|
+
if (pendingMs > 0 && nowMs - pendingMs > PENDING_TAIL_STALE_MS) {
|
|
3296
|
+
return { busy: false, currentTool: null };
|
|
3297
|
+
}
|
|
3298
|
+
return { busy: true, currentTool: unresolvedTool(visible) };
|
|
3299
|
+
}
|
|
3059
3300
|
const last = visible.at(-1);
|
|
3060
3301
|
if (!last) return { busy: false, currentTool: null };
|
|
3302
|
+
if (last.role === "tool" && messageText(last.content).includes(WAITING_FOR_CLIENT_MARKER)) {
|
|
3303
|
+
const tool = unresolvedTool(visible);
|
|
3304
|
+
return { busy: true, currentTool: { ...tool ?? { id: last.tool_call_id ?? "", name: "forwarded", arguments: {} }, waitingForHost: true } };
|
|
3305
|
+
}
|
|
3061
3306
|
const currentTool = unresolvedTool(visible);
|
|
3062
3307
|
if (currentTool) return { busy: true, currentTool };
|
|
3063
|
-
if (last.role === "user" || last.role === "tool")
|
|
3308
|
+
if (last.role === "user" || last.role === "tool") {
|
|
3309
|
+
const lastMs = toMs(createdAt(last));
|
|
3310
|
+
if (lastMs > 0 && nowMs - lastMs > BUSY_TAIL_STALE_MS) {
|
|
3311
|
+
return { busy: false, currentTool: null };
|
|
3312
|
+
}
|
|
3313
|
+
return { busy: true, currentTool: null };
|
|
3314
|
+
}
|
|
3064
3315
|
if (last.role === "assistant" && last.status !== "failed" && !messageText(last.content).trim()) {
|
|
3065
3316
|
const raw = createdAt(last);
|
|
3066
3317
|
const lastMs = raw > 1e14 ? raw / 1e3 : raw < 1e12 ? raw * 1e3 : raw;
|
|
@@ -3809,18 +4060,18 @@ function run(cmd, args, maxBuffer = MAX_IMAGE_BYTES * 2) {
|
|
|
3809
4060
|
});
|
|
3810
4061
|
}
|
|
3811
4062
|
function fromFile(filePath) {
|
|
3812
|
-
const mime = FILE_MIMES[
|
|
4063
|
+
const mime = FILE_MIMES[path4.extname(filePath).toLowerCase()];
|
|
3813
4064
|
if (!mime) return null;
|
|
3814
4065
|
try {
|
|
3815
|
-
const stat =
|
|
4066
|
+
const stat = fs5.statSync(filePath);
|
|
3816
4067
|
if (!stat.isFile() || stat.size === 0 || stat.size > MAX_IMAGE_BYTES) return null;
|
|
3817
|
-
return { data:
|
|
4068
|
+
return { data: fs5.readFileSync(filePath).toString("base64"), mime };
|
|
3818
4069
|
} catch {
|
|
3819
4070
|
return null;
|
|
3820
4071
|
}
|
|
3821
4072
|
}
|
|
3822
4073
|
async function readDarwin() {
|
|
3823
|
-
const tmp =
|
|
4074
|
+
const tmp = path4.join(os8.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
|
|
3824
4075
|
const script = [
|
|
3825
4076
|
`set d to the clipboard as \xABclass PNGf\xBB`,
|
|
3826
4077
|
`set f to open for access POSIX file "${tmp}" with write permission`,
|
|
@@ -3832,7 +4083,7 @@ async function readDarwin() {
|
|
|
3832
4083
|
if (png.ok) {
|
|
3833
4084
|
const img = fromFile(tmp);
|
|
3834
4085
|
try {
|
|
3835
|
-
|
|
4086
|
+
fs5.unlinkSync(tmp);
|
|
3836
4087
|
} catch {
|
|
3837
4088
|
}
|
|
3838
4089
|
if (img) return img;
|
|
@@ -3857,7 +4108,7 @@ async function readLinux() {
|
|
|
3857
4108
|
return null;
|
|
3858
4109
|
}
|
|
3859
4110
|
async function readWindows() {
|
|
3860
|
-
const tmp =
|
|
4111
|
+
const tmp = path4.join(os8.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
|
|
3861
4112
|
const ps = [
|
|
3862
4113
|
"Add-Type -AssemblyName System.Windows.Forms;",
|
|
3863
4114
|
"$img = [System.Windows.Forms.Clipboard]::GetImage();",
|
|
@@ -3866,7 +4117,7 @@ async function readWindows() {
|
|
|
3866
4117
|
await run("powershell", ["-NoProfile", "-STA", "-Command", ps]);
|
|
3867
4118
|
const img = fromFile(tmp);
|
|
3868
4119
|
try {
|
|
3869
|
-
|
|
4120
|
+
fs5.unlinkSync(tmp);
|
|
3870
4121
|
} catch {
|
|
3871
4122
|
}
|
|
3872
4123
|
return img;
|
|
@@ -5803,10 +6054,10 @@ ${C.cyan}\u2503${C.reset} ${question}
|
|
|
5803
6054
|
|
|
5804
6055
|
// src/history.ts
|
|
5805
6056
|
var HISTORY_KEY = "input_history";
|
|
5806
|
-
var
|
|
6057
|
+
var MAX_ENTRIES2 = 100;
|
|
5807
6058
|
function clean(value) {
|
|
5808
6059
|
if (!Array.isArray(value)) return [];
|
|
5809
|
-
return value.filter((e) => typeof e === "string" && e.trim() !== "").slice(-
|
|
6060
|
+
return value.filter((e) => typeof e === "string" && e.trim() !== "").slice(-MAX_ENTRIES2);
|
|
5810
6061
|
}
|
|
5811
6062
|
async function loadHistory(store, threadId, seedThreadId) {
|
|
5812
6063
|
const own = clean(await store.kvGet(threadId, HISTORY_KEY));
|
|
@@ -5824,12 +6075,12 @@ function appendHistory(store, threadId, history, text) {
|
|
|
5824
6075
|
const t = text.trim();
|
|
5825
6076
|
if (!t || history[history.length - 1] === t) return history;
|
|
5826
6077
|
history.push(t);
|
|
5827
|
-
if (history.length >
|
|
6078
|
+
if (history.length > MAX_ENTRIES2) history.splice(0, history.length - MAX_ENTRIES2);
|
|
5828
6079
|
void store.kvSet(threadId, HISTORY_KEY, [...history]);
|
|
5829
6080
|
return history;
|
|
5830
6081
|
}
|
|
5831
|
-
var DIR =
|
|
5832
|
-
var FILE =
|
|
6082
|
+
var DIR = path4.join(os8.homedir(), ".standardagents");
|
|
6083
|
+
var FILE = path4.join(DIR, "credentials");
|
|
5833
6084
|
function normalizeEndpoint(endpoint) {
|
|
5834
6085
|
let e = endpoint.trim();
|
|
5835
6086
|
if (!/^https?:\/\//i.test(e)) e = "http://" + e;
|
|
@@ -5837,7 +6088,7 @@ function normalizeEndpoint(endpoint) {
|
|
|
5837
6088
|
}
|
|
5838
6089
|
function loadCredentials() {
|
|
5839
6090
|
try {
|
|
5840
|
-
const raw =
|
|
6091
|
+
const raw = fs5.readFileSync(FILE, "utf8");
|
|
5841
6092
|
const parsed = JSON.parse(raw);
|
|
5842
6093
|
if (!parsed.instances) parsed.instances = {};
|
|
5843
6094
|
return parsed;
|
|
@@ -5856,20 +6107,20 @@ function saveCredential(cred, options = {}) {
|
|
|
5856
6107
|
if (options.updateDefault ?? true) {
|
|
5857
6108
|
creds.default_endpoint = endpoint;
|
|
5858
6109
|
}
|
|
5859
|
-
|
|
5860
|
-
|
|
6110
|
+
fs5.mkdirSync(DIR, { recursive: true });
|
|
6111
|
+
fs5.writeFileSync(FILE, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
5861
6112
|
try {
|
|
5862
|
-
|
|
6113
|
+
fs5.chmodSync(FILE, 384);
|
|
5863
6114
|
} catch {
|
|
5864
6115
|
}
|
|
5865
6116
|
}
|
|
5866
6117
|
function deleteCredential(endpoint) {
|
|
5867
6118
|
const creds = loadCredentials();
|
|
5868
6119
|
delete creds.instances[normalizeEndpoint(endpoint)];
|
|
5869
|
-
|
|
5870
|
-
|
|
6120
|
+
fs5.mkdirSync(DIR, { recursive: true });
|
|
6121
|
+
fs5.writeFileSync(FILE, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
5871
6122
|
try {
|
|
5872
|
-
|
|
6123
|
+
fs5.chmodSync(FILE, 384);
|
|
5873
6124
|
} catch {
|
|
5874
6125
|
}
|
|
5875
6126
|
}
|
|
@@ -5879,10 +6130,10 @@ function defaultEndpoint() {
|
|
|
5879
6130
|
function saveDefaultEndpoint(endpoint) {
|
|
5880
6131
|
const creds = loadCredentials();
|
|
5881
6132
|
creds.default_endpoint = normalizeEndpoint(endpoint);
|
|
5882
|
-
|
|
5883
|
-
|
|
6133
|
+
fs5.mkdirSync(DIR, { recursive: true });
|
|
6134
|
+
fs5.writeFileSync(FILE, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
5884
6135
|
try {
|
|
5885
|
-
|
|
6136
|
+
fs5.chmodSync(FILE, 384);
|
|
5886
6137
|
} catch {
|
|
5887
6138
|
}
|
|
5888
6139
|
}
|
|
@@ -5943,7 +6194,7 @@ var AGENT_CHOICES = [
|
|
|
5943
6194
|
var PRODUCTION_ENDPOINT = "https://api.standardcode.ai";
|
|
5944
6195
|
function readVersion() {
|
|
5945
6196
|
try {
|
|
5946
|
-
const pkg = JSON.parse(
|
|
6197
|
+
const pkg = JSON.parse(fs5.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
5947
6198
|
return typeof pkg.version === "string" ? pkg.version : "";
|
|
5948
6199
|
} catch {
|
|
5949
6200
|
return "";
|
|
@@ -5969,11 +6220,11 @@ function relaxTlsForLocalEndpoint(endpoint) {
|
|
|
5969
6220
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
5970
6221
|
return true;
|
|
5971
6222
|
}
|
|
5972
|
-
var dir = () =>
|
|
5973
|
-
var file = () =>
|
|
6223
|
+
var dir = () => path4.join(os8.homedir(), ".standardagents");
|
|
6224
|
+
var file = () => path4.join(dir(), "machine.json");
|
|
5974
6225
|
function loadMachineIdentity() {
|
|
5975
6226
|
try {
|
|
5976
|
-
const parsed = JSON.parse(
|
|
6227
|
+
const parsed = JSON.parse(fs5.readFileSync(file(), "utf8"));
|
|
5977
6228
|
if (typeof parsed.machine_id === "string" && parsed.machine_id.length > 0) {
|
|
5978
6229
|
return { machine_id: parsed.machine_id, created_at: parsed.created_at ?? Date.now() };
|
|
5979
6230
|
}
|
|
@@ -5987,8 +6238,8 @@ function loadMachineIdentity() {
|
|
|
5987
6238
|
return identity;
|
|
5988
6239
|
}
|
|
5989
6240
|
function saveMachineIdentity(identity) {
|
|
5990
|
-
|
|
5991
|
-
|
|
6241
|
+
fs5.mkdirSync(dir(), { recursive: true });
|
|
6242
|
+
fs5.writeFileSync(file(), JSON.stringify(identity, null, 2), { mode: 384 });
|
|
5992
6243
|
}
|
|
5993
6244
|
function daemonClientId(identity) {
|
|
5994
6245
|
return `daemon:${identity.machine_id}`;
|
|
@@ -6169,8 +6420,8 @@ function newRecord(identity) {
|
|
|
6169
6420
|
const now = Date.now();
|
|
6170
6421
|
return {
|
|
6171
6422
|
id: identity.machine_id,
|
|
6172
|
-
name:
|
|
6173
|
-
hostname:
|
|
6423
|
+
name: os8.hostname(),
|
|
6424
|
+
hostname: os8.hostname(),
|
|
6174
6425
|
platform: process.platform,
|
|
6175
6426
|
arch: process.arch,
|
|
6176
6427
|
version: readVersion() || void 0,
|
|
@@ -6183,7 +6434,7 @@ function newRecord(identity) {
|
|
|
6183
6434
|
async function updateOwnMachineRecord(api, identity, mutate) {
|
|
6184
6435
|
const existing = await loadRawMachine(api, identity.machine_id);
|
|
6185
6436
|
const record2 = existing ?? newRecord(identity);
|
|
6186
|
-
record2.hostname =
|
|
6437
|
+
record2.hostname = os8.hostname();
|
|
6187
6438
|
record2.platform = process.platform;
|
|
6188
6439
|
record2.arch = process.arch;
|
|
6189
6440
|
record2.version = readVersion() || record2.version;
|
|
@@ -6207,9 +6458,9 @@ function projectRepository(projectDir) {
|
|
|
6207
6458
|
function normalizeProjectDir(projectDir) {
|
|
6208
6459
|
let p = projectDir.trim();
|
|
6209
6460
|
if (!p) return p;
|
|
6210
|
-
if (p === "~") p =
|
|
6211
|
-
else if (p.startsWith("~/")) p =
|
|
6212
|
-
return
|
|
6461
|
+
if (p === "~") p = os8.homedir();
|
|
6462
|
+
else if (p.startsWith("~/")) p = path4.join(os8.homedir(), p.slice(2));
|
|
6463
|
+
return path4.resolve(p);
|
|
6213
6464
|
}
|
|
6214
6465
|
async function registerProject(api, identity, projectDir) {
|
|
6215
6466
|
const dir2 = normalizeProjectDir(projectDir);
|
|
@@ -6287,16 +6538,16 @@ async function clearMachineCommands(api, machineId, appliedIds) {
|
|
|
6287
6538
|
async function applyMachineCommand(api, identity, cmd) {
|
|
6288
6539
|
switch (cmd.kind) {
|
|
6289
6540
|
case "add_project": {
|
|
6290
|
-
const
|
|
6291
|
-
if (!
|
|
6292
|
-
await registerProject(api, identity,
|
|
6293
|
-
return `added project ${
|
|
6541
|
+
const path15 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
|
|
6542
|
+
if (!path15) return "add_project: ignored (no path)";
|
|
6543
|
+
await registerProject(api, identity, path15);
|
|
6544
|
+
return `added project ${path15}`;
|
|
6294
6545
|
}
|
|
6295
6546
|
case "remove_project": {
|
|
6296
|
-
const
|
|
6297
|
-
if (!
|
|
6298
|
-
await unregisterProject(api, identity,
|
|
6299
|
-
return `removed project ${
|
|
6547
|
+
const path15 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
|
|
6548
|
+
if (!path15) return "remove_project: ignored (no path)";
|
|
6549
|
+
await unregisterProject(api, identity, path15);
|
|
6550
|
+
return `removed project ${path15}`;
|
|
6300
6551
|
}
|
|
6301
6552
|
case "update":
|
|
6302
6553
|
return "update requested";
|
|
@@ -6358,6 +6609,101 @@ async function awaitApprovalViaRelay(api, threadId, request, options = {}) {
|
|
|
6358
6609
|
});
|
|
6359
6610
|
}
|
|
6360
6611
|
}
|
|
6612
|
+
|
|
6613
|
+
// src/hub.ts
|
|
6614
|
+
var HubSocket = class {
|
|
6615
|
+
constructor(api, clientId, hooks, clientName) {
|
|
6616
|
+
this.api = api;
|
|
6617
|
+
this.clientId = clientId;
|
|
6618
|
+
this.hooks = hooks;
|
|
6619
|
+
this.clientName = clientName;
|
|
6620
|
+
}
|
|
6621
|
+
api;
|
|
6622
|
+
clientId;
|
|
6623
|
+
hooks;
|
|
6624
|
+
clientName;
|
|
6625
|
+
ws = null;
|
|
6626
|
+
closed = false;
|
|
6627
|
+
heartbeat = null;
|
|
6628
|
+
reconnectAttempt = 0;
|
|
6629
|
+
reconnectTimer = null;
|
|
6630
|
+
/** Open the socket and keep it connected (reconnect forever on drop). */
|
|
6631
|
+
start() {
|
|
6632
|
+
this.openSocket();
|
|
6633
|
+
}
|
|
6634
|
+
openSocket() {
|
|
6635
|
+
if (this.closed) return;
|
|
6636
|
+
let url = `${this.api.wsEndpoint}/api/users/me/hub?token=${encodeURIComponent(this.api.bearer)}&client_id=${encodeURIComponent(this.clientId)}&client_kind=daemon`;
|
|
6637
|
+
if (this.clientName) url += `&client_name=${encodeURIComponent(this.clientName)}`;
|
|
6638
|
+
let ws;
|
|
6639
|
+
try {
|
|
6640
|
+
ws = new WebSocket(url);
|
|
6641
|
+
} catch {
|
|
6642
|
+
this.scheduleReconnect();
|
|
6643
|
+
return;
|
|
6644
|
+
}
|
|
6645
|
+
this.ws = ws;
|
|
6646
|
+
ws.addEventListener("open", () => {
|
|
6647
|
+
this.reconnectAttempt = 0;
|
|
6648
|
+
this.startHeartbeat(ws);
|
|
6649
|
+
this.hooks.onConnection?.("connected", 0);
|
|
6650
|
+
});
|
|
6651
|
+
ws.addEventListener("message", (ev) => {
|
|
6652
|
+
if (this.ws === ws) this.heartbeat?.markAlive();
|
|
6653
|
+
this.onMessage(String(ev.data));
|
|
6654
|
+
});
|
|
6655
|
+
ws.addEventListener("error", () => this.handleDrop(ws));
|
|
6656
|
+
ws.addEventListener("close", () => this.handleDrop(ws));
|
|
6657
|
+
}
|
|
6658
|
+
onMessage(raw) {
|
|
6659
|
+
let msg;
|
|
6660
|
+
try {
|
|
6661
|
+
msg = JSON.parse(raw);
|
|
6662
|
+
} catch {
|
|
6663
|
+
return;
|
|
6664
|
+
}
|
|
6665
|
+
if (msg && typeof msg === "object" && msg.type === "wake" && typeof msg.threadId === "string") {
|
|
6666
|
+
this.hooks.onWake(msg.threadId);
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6669
|
+
handleDrop(ws) {
|
|
6670
|
+
if (this.ws !== ws) return;
|
|
6671
|
+
this.ws = null;
|
|
6672
|
+
this.stopHeartbeat();
|
|
6673
|
+
this.scheduleReconnect();
|
|
6674
|
+
}
|
|
6675
|
+
scheduleReconnect() {
|
|
6676
|
+
if (this.closed || this.reconnectTimer) return;
|
|
6677
|
+
this.reconnectAttempt++;
|
|
6678
|
+
this.hooks.onConnection?.("reconnecting", this.reconnectAttempt);
|
|
6679
|
+
const base = Math.min(500 * 2 ** (this.reconnectAttempt - 1), 15e3);
|
|
6680
|
+
const delay = base + Math.floor(Math.random() * 400);
|
|
6681
|
+
this.reconnectTimer = setTimeout(() => {
|
|
6682
|
+
this.reconnectTimer = null;
|
|
6683
|
+
this.openSocket();
|
|
6684
|
+
}, delay);
|
|
6685
|
+
}
|
|
6686
|
+
startHeartbeat(ws) {
|
|
6687
|
+
this.stopHeartbeat();
|
|
6688
|
+
this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
|
|
6689
|
+
this.heartbeat.start();
|
|
6690
|
+
}
|
|
6691
|
+
stopHeartbeat() {
|
|
6692
|
+
if (this.heartbeat) {
|
|
6693
|
+
this.heartbeat.stop();
|
|
6694
|
+
this.heartbeat = null;
|
|
6695
|
+
}
|
|
6696
|
+
}
|
|
6697
|
+
close() {
|
|
6698
|
+
this.closed = true;
|
|
6699
|
+
this.stopHeartbeat();
|
|
6700
|
+
if (this.reconnectTimer) {
|
|
6701
|
+
clearTimeout(this.reconnectTimer);
|
|
6702
|
+
this.reconnectTimer = null;
|
|
6703
|
+
}
|
|
6704
|
+
this.ws?.close();
|
|
6705
|
+
}
|
|
6706
|
+
};
|
|
6361
6707
|
var PROJECT_MARKERS = [
|
|
6362
6708
|
".git",
|
|
6363
6709
|
"package.json",
|
|
@@ -6371,14 +6717,14 @@ var PROJECT_MARKERS = [
|
|
|
6371
6717
|
"requirements.txt",
|
|
6372
6718
|
".standardagents"
|
|
6373
6719
|
];
|
|
6374
|
-
var
|
|
6720
|
+
var MAX_ENTRIES3 = 500;
|
|
6375
6721
|
function resolveBrowsePath(input3) {
|
|
6376
|
-
const home =
|
|
6722
|
+
const home = os8.homedir();
|
|
6377
6723
|
let p = (input3 ?? "").trim();
|
|
6378
6724
|
if (!p) return home;
|
|
6379
6725
|
if (p === "~") return home;
|
|
6380
|
-
if (p.startsWith("~/")) p =
|
|
6381
|
-
return
|
|
6726
|
+
if (p.startsWith("~/")) p = path4.join(home, p.slice(2));
|
|
6727
|
+
return path4.resolve(p);
|
|
6382
6728
|
}
|
|
6383
6729
|
function markers(dirPath) {
|
|
6384
6730
|
let repo = false;
|
|
@@ -6386,7 +6732,7 @@ function markers(dirPath) {
|
|
|
6386
6732
|
for (const marker of PROJECT_MARKERS) {
|
|
6387
6733
|
let hit = false;
|
|
6388
6734
|
try {
|
|
6389
|
-
hit =
|
|
6735
|
+
hit = fs5.existsSync(path4.join(dirPath, marker));
|
|
6390
6736
|
} catch {
|
|
6391
6737
|
hit = false;
|
|
6392
6738
|
}
|
|
@@ -6398,9 +6744,9 @@ function markers(dirPath) {
|
|
|
6398
6744
|
return { project, repo };
|
|
6399
6745
|
}
|
|
6400
6746
|
function browseDirectory(input3, opts = {}) {
|
|
6401
|
-
const home =
|
|
6747
|
+
const home = os8.homedir();
|
|
6402
6748
|
const abs = resolveBrowsePath(input3);
|
|
6403
|
-
const parent =
|
|
6749
|
+
const parent = path4.dirname(abs);
|
|
6404
6750
|
const base = {
|
|
6405
6751
|
path: abs,
|
|
6406
6752
|
parent: parent === abs ? null : parent,
|
|
@@ -6408,11 +6754,11 @@ function browseDirectory(input3, opts = {}) {
|
|
|
6408
6754
|
};
|
|
6409
6755
|
let dirents;
|
|
6410
6756
|
try {
|
|
6411
|
-
const stat =
|
|
6757
|
+
const stat = fs5.statSync(abs);
|
|
6412
6758
|
if (!stat.isDirectory()) {
|
|
6413
6759
|
return { ...base, entries: [], truncated: false, error: "Not a directory" };
|
|
6414
6760
|
}
|
|
6415
|
-
dirents =
|
|
6761
|
+
dirents = fs5.readdirSync(abs, { withFileTypes: true });
|
|
6416
6762
|
} catch (err) {
|
|
6417
6763
|
const code = err?.code;
|
|
6418
6764
|
const message = code === "EACCES" || code === "EPERM" ? "Permission denied" : code === "ENOENT" ? "Folder not found" : "Could not read this folder";
|
|
@@ -6427,13 +6773,13 @@ function browseDirectory(input3, opts = {}) {
|
|
|
6427
6773
|
let isDir = d.isDirectory();
|
|
6428
6774
|
if (d.isSymbolicLink()) {
|
|
6429
6775
|
try {
|
|
6430
|
-
isDir =
|
|
6776
|
+
isDir = fs5.statSync(path4.join(abs, name)).isDirectory();
|
|
6431
6777
|
} catch {
|
|
6432
6778
|
isDir = false;
|
|
6433
6779
|
}
|
|
6434
6780
|
}
|
|
6435
6781
|
if (isDir) {
|
|
6436
|
-
const { project, repo } = markers(
|
|
6782
|
+
const { project, repo } = markers(path4.join(abs, name));
|
|
6437
6783
|
rawDirs.push({ name, dir: true, project: project || void 0, repo: repo || void 0 });
|
|
6438
6784
|
} else {
|
|
6439
6785
|
rawFiles.push({ name, dir: false });
|
|
@@ -6443,8 +6789,8 @@ function browseDirectory(input3, opts = {}) {
|
|
|
6443
6789
|
rawDirs.sort(cmp);
|
|
6444
6790
|
rawFiles.sort(cmp);
|
|
6445
6791
|
const all = [...rawDirs, ...rawFiles];
|
|
6446
|
-
const truncated = all.length >
|
|
6447
|
-
return { ...base, entries: truncated ? all.slice(0,
|
|
6792
|
+
const truncated = all.length > MAX_ENTRIES3;
|
|
6793
|
+
return { ...base, entries: truncated ? all.slice(0, MAX_ENTRIES3) : all, truncated };
|
|
6448
6794
|
}
|
|
6449
6795
|
function mkdirDirectory(parentInput, name, opts = {}) {
|
|
6450
6796
|
const parent = resolveBrowsePath(parentInput);
|
|
@@ -6453,12 +6799,12 @@ function mkdirDirectory(parentInput, name, opts = {}) {
|
|
|
6453
6799
|
if (!clean2 || clean2 === "." || clean2 === ".." || clean2.includes("/") || clean2.includes("\\") || clean2.includes("\0")) {
|
|
6454
6800
|
return { ...listParent(), error: "Invalid folder name." };
|
|
6455
6801
|
}
|
|
6456
|
-
const target =
|
|
6457
|
-
if (
|
|
6802
|
+
const target = path4.join(parent, clean2);
|
|
6803
|
+
if (path4.dirname(target) !== parent) {
|
|
6458
6804
|
return { ...listParent(), error: "Invalid folder name." };
|
|
6459
6805
|
}
|
|
6460
6806
|
try {
|
|
6461
|
-
|
|
6807
|
+
fs5.mkdirSync(target, { recursive: false });
|
|
6462
6808
|
} catch (err) {
|
|
6463
6809
|
const code = err?.code;
|
|
6464
6810
|
const message = code === "EEXIST" ? "A folder with that name already exists." : code === "EACCES" || code === "EPERM" ? "Permission denied." : code === "ENOENT" ? "The parent folder no longer exists." : "Could not create the folder.";
|
|
@@ -6475,14 +6821,14 @@ var CACHE_TTL_MS = 1e3 * 60 * 60 * 6;
|
|
|
6475
6821
|
var CHECK_TIMEOUT_MS = 4e3;
|
|
6476
6822
|
var IN_FLIGHT_TTL_MS = 15 * 60 * 1e3;
|
|
6477
6823
|
function cacheDir() {
|
|
6478
|
-
return
|
|
6824
|
+
return path4.join(homedir(), CACHE_REL_DIR);
|
|
6479
6825
|
}
|
|
6480
6826
|
function cachePath() {
|
|
6481
|
-
return
|
|
6827
|
+
return path4.join(cacheDir(), CACHE_FILE);
|
|
6482
6828
|
}
|
|
6483
6829
|
function readCache() {
|
|
6484
6830
|
try {
|
|
6485
|
-
const raw =
|
|
6831
|
+
const raw = fs5.readFileSync(cachePath(), "utf-8");
|
|
6486
6832
|
return JSON.parse(raw);
|
|
6487
6833
|
} catch {
|
|
6488
6834
|
return null;
|
|
@@ -6491,14 +6837,14 @@ function readCache() {
|
|
|
6491
6837
|
function writeCache(latest) {
|
|
6492
6838
|
try {
|
|
6493
6839
|
const dir2 = cacheDir();
|
|
6494
|
-
if (!
|
|
6495
|
-
|
|
6840
|
+
if (!fs5.existsSync(dir2)) fs5.mkdirSync(dir2, { recursive: true });
|
|
6841
|
+
fs5.writeFileSync(cachePath(), JSON.stringify({ latest, timestamp: Date.now() }));
|
|
6496
6842
|
} catch {
|
|
6497
6843
|
}
|
|
6498
6844
|
}
|
|
6499
6845
|
function readAutoUpdateState(dir2 = cacheDir()) {
|
|
6500
6846
|
try {
|
|
6501
|
-
const raw =
|
|
6847
|
+
const raw = fs5.readFileSync(path4.join(dir2, STATE_FILE), "utf-8");
|
|
6502
6848
|
const state = JSON.parse(raw);
|
|
6503
6849
|
return typeof state?.version === "string" ? state : null;
|
|
6504
6850
|
} catch {
|
|
@@ -6507,14 +6853,14 @@ function readAutoUpdateState(dir2 = cacheDir()) {
|
|
|
6507
6853
|
}
|
|
6508
6854
|
function writeAutoUpdateState(state, dir2 = cacheDir()) {
|
|
6509
6855
|
try {
|
|
6510
|
-
if (!
|
|
6511
|
-
|
|
6856
|
+
if (!fs5.existsSync(dir2)) fs5.mkdirSync(dir2, { recursive: true });
|
|
6857
|
+
fs5.writeFileSync(path4.join(dir2, STATE_FILE), JSON.stringify(state));
|
|
6512
6858
|
} catch {
|
|
6513
6859
|
}
|
|
6514
6860
|
}
|
|
6515
6861
|
function clearAutoUpdateState(dir2 = cacheDir()) {
|
|
6516
6862
|
try {
|
|
6517
|
-
|
|
6863
|
+
fs5.unlinkSync(path4.join(dir2, STATE_FILE));
|
|
6518
6864
|
} catch {
|
|
6519
6865
|
}
|
|
6520
6866
|
}
|
|
@@ -6557,7 +6903,7 @@ function decideAutoUpdate(info, opts) {
|
|
|
6557
6903
|
function startBackgroundUpdate(latest, pm, dir2 = cacheDir()) {
|
|
6558
6904
|
const startedAt = Date.now();
|
|
6559
6905
|
writeAutoUpdateState({ version: latest, startedAt, exitCode: null }, dir2);
|
|
6560
|
-
const stateFile =
|
|
6906
|
+
const stateFile = path4.join(dir2, STATE_FILE);
|
|
6561
6907
|
const { cmd, args } = updateCommand(pm);
|
|
6562
6908
|
const script = `const cp=require('child_process');const fs=require('fs');const r=cp.spawnSync(${JSON.stringify(cmd)},${JSON.stringify(args)},{shell:process.platform==='win32',encoding:'utf8'});const out=((r.stdout||'')+(r.stderr||'')).slice(-2000);fs.writeFileSync(${JSON.stringify(stateFile)},JSON.stringify({version:${JSON.stringify(latest)},startedAt:${startedAt},exitCode:r.status==null?-1:r.status,finishedAt:Date.now(),output:out}));`;
|
|
6563
6909
|
try {
|
|
@@ -6652,17 +6998,17 @@ var UPDATE_CHECK_MS = 6 * 60 * 6e4;
|
|
|
6652
6998
|
var SWEEP_MS = 10 * 6e4;
|
|
6653
6999
|
var MAX_WORKERS = 30;
|
|
6654
7000
|
var LOG_MAX_BYTES = 1e6;
|
|
6655
|
-
var LOG_FILE =
|
|
7001
|
+
var LOG_FILE = path4.join(os8.homedir(), ".standardagents", "daemon.log");
|
|
6656
7002
|
function daemonLog(line) {
|
|
6657
7003
|
try {
|
|
6658
|
-
|
|
7004
|
+
fs5.mkdirSync(path4.dirname(LOG_FILE), { recursive: true });
|
|
6659
7005
|
try {
|
|
6660
|
-
if (
|
|
6661
|
-
|
|
7006
|
+
if (fs5.statSync(LOG_FILE).size > LOG_MAX_BYTES) {
|
|
7007
|
+
fs5.renameSync(LOG_FILE, `${LOG_FILE}.old`);
|
|
6662
7008
|
}
|
|
6663
7009
|
} catch {
|
|
6664
7010
|
}
|
|
6665
|
-
|
|
7011
|
+
fs5.appendFileSync(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
6666
7012
|
`);
|
|
6667
7013
|
} catch {
|
|
6668
7014
|
}
|
|
@@ -6671,7 +7017,7 @@ function pathFromTags(tags) {
|
|
|
6671
7017
|
const tag = tags.find((t) => t.startsWith("path:"));
|
|
6672
7018
|
if (!tag) return null;
|
|
6673
7019
|
const raw = tag.slice("path:".length);
|
|
6674
|
-
return raw.replace(/^~(?=\/|$)/,
|
|
7020
|
+
return raw.replace(/^~(?=\/|$)/, os8.homedir());
|
|
6675
7021
|
}
|
|
6676
7022
|
var ThreadWorker = class {
|
|
6677
7023
|
constructor(api, identity, machineName, threadId, projectDir, createdAt2) {
|
|
@@ -6803,7 +7149,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
6803
7149
|
process.exit(1);
|
|
6804
7150
|
}
|
|
6805
7151
|
let displayName = machineDisplayName(await loadMachine(api, identity.machine_id).catch(() => null) ?? {
|
|
6806
|
-
hostname:
|
|
7152
|
+
hostname: os8.hostname(),
|
|
6807
7153
|
id: identity.machine_id
|
|
6808
7154
|
});
|
|
6809
7155
|
const applied = consumeAppliedUpdate(version);
|
|
@@ -6841,7 +7187,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
6841
7187
|
daemonLog(`detached ${oldest.threadId.slice(0, 8)} (worker cap)`);
|
|
6842
7188
|
}
|
|
6843
7189
|
try {
|
|
6844
|
-
|
|
7190
|
+
fs5.mkdirSync(projectDir, { recursive: true });
|
|
6845
7191
|
} catch (e) {
|
|
6846
7192
|
daemonLog(`cannot prepare project dir ${projectDir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
6847
7193
|
return;
|
|
@@ -6890,6 +7236,31 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
6890
7236
|
onThreadDeleted: (id) => detach(id)
|
|
6891
7237
|
});
|
|
6892
7238
|
events.connect();
|
|
7239
|
+
const hub = new HubSocket(
|
|
7240
|
+
api,
|
|
7241
|
+
daemonClientId(identity),
|
|
7242
|
+
{
|
|
7243
|
+
onWake: (threadId) => {
|
|
7244
|
+
if (workers.has(threadId)) return;
|
|
7245
|
+
void (async () => {
|
|
7246
|
+
try {
|
|
7247
|
+
const thread = await api.getThread(threadId);
|
|
7248
|
+
if (!thread || thread.terminated) return;
|
|
7249
|
+
if (!thread.tags?.includes(runnerTag)) return;
|
|
7250
|
+
daemonLog(`wake: attaching ${threadId.slice(0, 8)} on hub signal`);
|
|
7251
|
+
await attach(threadId, thread.tags ?? [], (thread.created_at ?? 0) * 1e3);
|
|
7252
|
+
} catch (e) {
|
|
7253
|
+
daemonLog(`wake attach failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
7254
|
+
}
|
|
7255
|
+
})();
|
|
7256
|
+
},
|
|
7257
|
+
onConnection: (state, attempt) => {
|
|
7258
|
+
if (state === "reconnecting" && attempt === 1) daemonLog("hub: reconnecting");
|
|
7259
|
+
}
|
|
7260
|
+
},
|
|
7261
|
+
displayName
|
|
7262
|
+
);
|
|
7263
|
+
hub.start();
|
|
6893
7264
|
await sweep();
|
|
6894
7265
|
const reclaim = setInterval(() => {
|
|
6895
7266
|
for (const worker of workers.values()) {
|
|
@@ -7013,6 +7384,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
7013
7384
|
clearInterval(sweeper);
|
|
7014
7385
|
for (const worker of workers.values()) worker.stop();
|
|
7015
7386
|
events.close();
|
|
7387
|
+
hub.close();
|
|
7016
7388
|
daemonLog(`daemon exiting (code ${code})`);
|
|
7017
7389
|
process.exit(code);
|
|
7018
7390
|
};
|
|
@@ -7025,19 +7397,19 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
7025
7397
|
var SERVICE_LABEL = "ai.standardcode.daemon";
|
|
7026
7398
|
var SYSTEMD_UNIT = "standardcode-daemon.service";
|
|
7027
7399
|
function resolveDaemonCommand(extraArgs = []) {
|
|
7028
|
-
const entry =
|
|
7400
|
+
const entry = path4.resolve(process.argv[1] ?? "");
|
|
7029
7401
|
if (!entry) throw new Error("Cannot determine how this CLI was launched.");
|
|
7030
7402
|
const argv = [process.execPath];
|
|
7031
7403
|
if (entry.endsWith(".ts")) {
|
|
7032
|
-
let dir2 =
|
|
7404
|
+
let dir2 = path4.dirname(entry);
|
|
7033
7405
|
let tsx = null;
|
|
7034
|
-
for (let i = 0; i < 6 && dir2 !==
|
|
7035
|
-
const candidate =
|
|
7036
|
-
if (
|
|
7406
|
+
for (let i = 0; i < 6 && dir2 !== path4.dirname(dir2); i++) {
|
|
7407
|
+
const candidate = path4.join(dir2, "node_modules", "tsx", "dist", "cli.mjs");
|
|
7408
|
+
if (fs5.existsSync(candidate)) {
|
|
7037
7409
|
tsx = candidate;
|
|
7038
7410
|
break;
|
|
7039
7411
|
}
|
|
7040
|
-
dir2 =
|
|
7412
|
+
dir2 = path4.dirname(dir2);
|
|
7041
7413
|
}
|
|
7042
7414
|
if (!tsx) {
|
|
7043
7415
|
throw new Error(
|
|
@@ -7051,7 +7423,7 @@ function resolveDaemonCommand(extraArgs = []) {
|
|
|
7051
7423
|
}
|
|
7052
7424
|
function servicePath() {
|
|
7053
7425
|
const parts = [
|
|
7054
|
-
|
|
7426
|
+
path4.dirname(process.execPath),
|
|
7055
7427
|
"/opt/homebrew/bin",
|
|
7056
7428
|
"/usr/local/bin",
|
|
7057
7429
|
"/usr/bin",
|
|
@@ -7067,8 +7439,8 @@ function run2(cmd, args) {
|
|
|
7067
7439
|
const output4 = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
|
|
7068
7440
|
return { ok: res.status === 0, output: output4 };
|
|
7069
7441
|
}
|
|
7070
|
-
var plistPath = () =>
|
|
7071
|
-
var unitPath = () =>
|
|
7442
|
+
var plistPath = () => path4.join(os8.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
7443
|
+
var unitPath = () => path4.join(os8.homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
7072
7444
|
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
7073
7445
|
function installService(command, endpoint) {
|
|
7074
7446
|
if (process.platform === "darwin") return installLaunchd(command, endpoint);
|
|
@@ -7080,9 +7452,9 @@ function installService(command, endpoint) {
|
|
|
7080
7452
|
};
|
|
7081
7453
|
}
|
|
7082
7454
|
function installLaunchd(command, endpoint) {
|
|
7083
|
-
const logDir =
|
|
7084
|
-
|
|
7085
|
-
|
|
7455
|
+
const logDir = path4.join(os8.homedir(), ".standardagents");
|
|
7456
|
+
fs5.mkdirSync(logDir, { recursive: true });
|
|
7457
|
+
fs5.mkdirSync(path4.dirname(plistPath()), { recursive: true });
|
|
7086
7458
|
const envEntries = [
|
|
7087
7459
|
` <key>PATH</key><string>${xmlEscape(servicePath())}</string>`,
|
|
7088
7460
|
...endpoint ? [` <key>STANDARD_CODE_DAEMON_ENDPOINT</key><string>${xmlEscape(endpoint)}</string>`] : []
|
|
@@ -7099,8 +7471,8 @@ ${command.argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n")}
|
|
|
7099
7471
|
<key>RunAtLoad</key><true/>
|
|
7100
7472
|
<key>KeepAlive</key><true/>
|
|
7101
7473
|
<key>ThrottleInterval</key><integer>5</integer>
|
|
7102
|
-
<key>StandardOutPath</key><string>${xmlEscape(
|
|
7103
|
-
<key>StandardErrorPath</key><string>${xmlEscape(
|
|
7474
|
+
<key>StandardOutPath</key><string>${xmlEscape(path4.join(logDir, "daemon.out.log"))}</string>
|
|
7475
|
+
<key>StandardErrorPath</key><string>${xmlEscape(path4.join(logDir, "daemon.err.log"))}</string>
|
|
7104
7476
|
<key>EnvironmentVariables</key>
|
|
7105
7477
|
<dict>
|
|
7106
7478
|
${envEntries}
|
|
@@ -7108,7 +7480,7 @@ ${envEntries}
|
|
|
7108
7480
|
</dict>
|
|
7109
7481
|
</plist>
|
|
7110
7482
|
`;
|
|
7111
|
-
|
|
7483
|
+
fs5.writeFileSync(plistPath(), plist);
|
|
7112
7484
|
const uid = typeof process.getuid === "function" ? process.getuid() : 501;
|
|
7113
7485
|
run2("launchctl", ["bootout", `gui/${uid}`, plistPath()]);
|
|
7114
7486
|
const boot = run2("launchctl", ["bootstrap", `gui/${uid}`, plistPath()]);
|
|
@@ -7125,7 +7497,7 @@ ${envEntries}
|
|
|
7125
7497
|
return { ok: true, detail: `LaunchAgent installed (${plistPath()})` };
|
|
7126
7498
|
}
|
|
7127
7499
|
function installSystemd(command, endpoint) {
|
|
7128
|
-
|
|
7500
|
+
fs5.mkdirSync(path4.dirname(unitPath()), { recursive: true });
|
|
7129
7501
|
const unit = `[Unit]
|
|
7130
7502
|
Description=Standard Code daemon (headless coding-agent execution client)
|
|
7131
7503
|
After=network-online.target
|
|
@@ -7140,7 +7512,7 @@ ${endpoint ? `Environment=STANDARD_CODE_DAEMON_ENDPOINT=${endpoint}
|
|
|
7140
7512
|
[Install]
|
|
7141
7513
|
WantedBy=default.target
|
|
7142
7514
|
`;
|
|
7143
|
-
|
|
7515
|
+
fs5.writeFileSync(unitPath(), unit);
|
|
7144
7516
|
const reload = run2("systemctl", ["--user", "daemon-reload"]);
|
|
7145
7517
|
if (!reload.ok) {
|
|
7146
7518
|
return {
|
|
@@ -7153,10 +7525,10 @@ WantedBy=default.target
|
|
|
7153
7525
|
if (!enable.ok) {
|
|
7154
7526
|
return { ok: false, detail: `systemctl enable failed: ${enable.output}` };
|
|
7155
7527
|
}
|
|
7156
|
-
const linger = run2("loginctl", ["enable-linger",
|
|
7528
|
+
const linger = run2("loginctl", ["enable-linger", os8.userInfo().username]);
|
|
7157
7529
|
return {
|
|
7158
7530
|
ok: true,
|
|
7159
|
-
detail: `systemd user unit installed (${unitPath()})` + (linger.ok ? ", lingering enabled" : ` \u2014 enable lingering manually: sudo loginctl enable-linger ${
|
|
7531
|
+
detail: `systemd user unit installed (${unitPath()})` + (linger.ok ? ", lingering enabled" : ` \u2014 enable lingering manually: sudo loginctl enable-linger ${os8.userInfo().username}`)
|
|
7160
7532
|
};
|
|
7161
7533
|
}
|
|
7162
7534
|
function uninstallService() {
|
|
@@ -7164,7 +7536,7 @@ function uninstallService() {
|
|
|
7164
7536
|
const uid = typeof process.getuid === "function" ? process.getuid() : 501;
|
|
7165
7537
|
run2("launchctl", ["bootout", `gui/${uid}`, plistPath()]);
|
|
7166
7538
|
try {
|
|
7167
|
-
|
|
7539
|
+
fs5.unlinkSync(plistPath());
|
|
7168
7540
|
} catch {
|
|
7169
7541
|
}
|
|
7170
7542
|
return { ok: true, detail: "LaunchAgent removed" };
|
|
@@ -7172,7 +7544,7 @@ function uninstallService() {
|
|
|
7172
7544
|
if (process.platform === "linux") {
|
|
7173
7545
|
run2("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
|
|
7174
7546
|
try {
|
|
7175
|
-
|
|
7547
|
+
fs5.unlinkSync(unitPath());
|
|
7176
7548
|
} catch {
|
|
7177
7549
|
}
|
|
7178
7550
|
run2("systemctl", ["--user", "daemon-reload"]);
|
|
@@ -7182,7 +7554,7 @@ function uninstallService() {
|
|
|
7182
7554
|
}
|
|
7183
7555
|
function serviceStatus() {
|
|
7184
7556
|
if (process.platform === "darwin") {
|
|
7185
|
-
const installed =
|
|
7557
|
+
const installed = fs5.existsSync(plistPath());
|
|
7186
7558
|
const list = run2("launchctl", ["list", SERVICE_LABEL]);
|
|
7187
7559
|
const pidMatch = list.output.match(/"PID"\s*=\s*(\d+)/);
|
|
7188
7560
|
return {
|
|
@@ -7192,7 +7564,7 @@ function serviceStatus() {
|
|
|
7192
7564
|
};
|
|
7193
7565
|
}
|
|
7194
7566
|
if (process.platform === "linux") {
|
|
7195
|
-
const installed =
|
|
7567
|
+
const installed = fs5.existsSync(unitPath());
|
|
7196
7568
|
const active = run2("systemctl", ["--user", "is-active", SYSTEMD_UNIT]);
|
|
7197
7569
|
return {
|
|
7198
7570
|
installed,
|
|
@@ -7284,7 +7656,7 @@ async function installCommand(endpointFlag) {
|
|
|
7284
7656
|
const api = await ensureSignedIn(endpoint);
|
|
7285
7657
|
const identity = loadMachineIdentity();
|
|
7286
7658
|
const existing = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
7287
|
-
const suggested = machineDisplayName(existing ?? { hostname:
|
|
7659
|
+
const suggested = machineDisplayName(existing ?? { hostname: os8.hostname(), id: identity.machine_id });
|
|
7288
7660
|
const rl = readline2.createInterface({ input: stdin, output: stdout });
|
|
7289
7661
|
const answer = (await rl.question(
|
|
7290
7662
|
`${c2.bold}Machine name${c2.reset} ${c2.dim}(shown in the session picker)${c2.reset} [${suggested}]: `
|
|
@@ -7344,7 +7716,7 @@ async function statusCommand() {
|
|
|
7344
7716
|
const cred = getCredential(endpoint);
|
|
7345
7717
|
if (!cred) {
|
|
7346
7718
|
stdout.write(
|
|
7347
|
-
`${c2.bold}Machine:${c2.reset} ${
|
|
7719
|
+
`${c2.bold}Machine:${c2.reset} ${os8.hostname()} ${c2.dim}(${identity.machine_id})${c2.reset}
|
|
7348
7720
|
`
|
|
7349
7721
|
);
|
|
7350
7722
|
stdout.write(`${c2.bold}Account:${c2.reset} ${c2.yellow}not signed in to ${endpoint}${c2.reset}
|
|
@@ -7355,7 +7727,7 @@ async function statusCommand() {
|
|
|
7355
7727
|
const api = new ApiClient(endpoint, cred.access_token);
|
|
7356
7728
|
const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
7357
7729
|
stdout.write(
|
|
7358
|
-
`${c2.bold}Machine:${c2.reset} ${machineDisplayName(record2 ?? { hostname:
|
|
7730
|
+
`${c2.bold}Machine:${c2.reset} ${machineDisplayName(record2 ?? { hostname: os8.hostname(), id: identity.machine_id })} ${c2.dim}(${identity.machine_id})${c2.reset}
|
|
7359
7731
|
`
|
|
7360
7732
|
);
|
|
7361
7733
|
if (!record2) {
|
|
@@ -7379,8 +7751,8 @@ async function projectCommand(action, target) {
|
|
|
7379
7751
|
`);
|
|
7380
7752
|
process.exit(1);
|
|
7381
7753
|
}
|
|
7382
|
-
const dir2 =
|
|
7383
|
-
if (action === "add" && !
|
|
7754
|
+
const dir2 = path4.resolve(target);
|
|
7755
|
+
if (action === "add" && !fs5.existsSync(dir2)) {
|
|
7384
7756
|
stdout.write(`${c2.red}\u2717${c2.reset} ${dir2} does not exist on this machine.
|
|
7385
7757
|
`);
|
|
7386
7758
|
process.exit(1);
|
|
@@ -7389,7 +7761,7 @@ async function projectCommand(action, target) {
|
|
|
7389
7761
|
const api = await ensureSignedIn(endpoint);
|
|
7390
7762
|
const identity = loadMachineIdentity();
|
|
7391
7763
|
const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
7392
|
-
const displayName = machineDisplayName(record2 ?? { hostname:
|
|
7764
|
+
const displayName = machineDisplayName(record2 ?? { hostname: os8.hostname(), id: identity.machine_id });
|
|
7393
7765
|
if (action === "add") {
|
|
7394
7766
|
await registerProject(api, identity, dir2);
|
|
7395
7767
|
stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir2} for remote sessions on ${displayName}.
|
|
@@ -7604,7 +7976,7 @@ ${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.rese
|
|
|
7604
7976
|
`);
|
|
7605
7977
|
}
|
|
7606
7978
|
function printWelcome(endpoint, projectDir) {
|
|
7607
|
-
const home =
|
|
7979
|
+
const home = os8.homedir();
|
|
7608
7980
|
const dir2 = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
7609
7981
|
const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
7610
7982
|
const version = readVersion();
|
|
@@ -7674,8 +8046,8 @@ async function main() {
|
|
|
7674
8046
|
const endpointArg = cliArgs.endpoint;
|
|
7675
8047
|
const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
|
|
7676
8048
|
const dirArg = cliArgs.dir;
|
|
7677
|
-
const projectDir =
|
|
7678
|
-
const machine =
|
|
8049
|
+
const projectDir = path4.resolve(dirArg || process.cwd());
|
|
8050
|
+
const machine = os8.hostname();
|
|
7679
8051
|
const reader = { rl: null };
|
|
7680
8052
|
let handoffClosing = false;
|
|
7681
8053
|
let preflightArmed = false;
|
|
@@ -7889,7 +8261,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
7889
8261
|
const ok = await ensureSamaAuth(tui, api);
|
|
7890
8262
|
if (!ok) process.exit(0);
|
|
7891
8263
|
}
|
|
7892
|
-
const home =
|
|
8264
|
+
const home = os8.homedir();
|
|
7893
8265
|
const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
7894
8266
|
const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
|
|
7895
8267
|
const session = { mode: "local", identity };
|
|
@@ -9063,7 +9435,10 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
9063
9435
|
}
|
|
9064
9436
|
const polledBusy = (serverBusy ?? false) || threadBusy(msgs);
|
|
9065
9437
|
if (serverTool && !activeSteps.has(serverTool.id)) {
|
|
9066
|
-
activeSteps.set(
|
|
9438
|
+
activeSteps.set(
|
|
9439
|
+
serverTool.id,
|
|
9440
|
+
serverTool.waitingForHost ? "waiting for your machine to reconnect" : serverTool.name || "working"
|
|
9441
|
+
);
|
|
9067
9442
|
refreshStatus();
|
|
9068
9443
|
} else if (serverBusy !== null && !serverTool && activeSteps.size) {
|
|
9069
9444
|
activeSteps.clear();
|
|
@@ -9320,12 +9695,12 @@ async function manageMachineProjects(tui, api, self, machine, dispatch, applyNot
|
|
|
9320
9695
|
);
|
|
9321
9696
|
if (!picked) return;
|
|
9322
9697
|
if (picked === ADD) {
|
|
9323
|
-
const
|
|
9698
|
+
const path15 = await tui.prompt(
|
|
9324
9699
|
`Absolute project path on ${machine.name}`,
|
|
9325
9700
|
machine.id === self.machine_id ? process.cwd() : "/home/you/project"
|
|
9326
9701
|
);
|
|
9327
|
-
if (!
|
|
9328
|
-
const trimmed =
|
|
9702
|
+
if (!path15 || !path15.trim()) return;
|
|
9703
|
+
const trimmed = path15.trim();
|
|
9329
9704
|
if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
|
|
9330
9705
|
tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
|
|
9331
9706
|
return;
|