@standardagents/code 0.9.10 → 0.10.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 +529 -148
- 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 (Sama One's direct egress): the Worker terminates TLS over
|
|
1247
|
+
// these; we only shuttle ciphertext. Bound to this socket, torn down with it.
|
|
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
|
}
|
|
@@ -3052,15 +3280,44 @@ function parseToolCalls(message) {
|
|
|
3052
3280
|
return [{ id, name, arguments: parseObject(item?.function?.arguments ?? item?.arguments ?? item?.args) }];
|
|
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
|
+
}
|
|
3290
|
+
function deriveSessionActivity(messages, nowMs = Date.now()) {
|
|
3056
3291
|
const visible = messages.filter((message) => message.silent !== true && message.metadata?.silent !== true).sort((a, b) => createdAt(a) - createdAt(b));
|
|
3057
|
-
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
|
+
}
|
|
3058
3300
|
const last = visible.at(-1);
|
|
3059
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
|
+
}
|
|
3060
3306
|
const currentTool = unresolvedTool(visible);
|
|
3061
3307
|
if (currentTool) return { busy: true, currentTool };
|
|
3062
|
-
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
|
+
}
|
|
3063
3315
|
if (last.role === "assistant" && last.status !== "failed" && !messageText(last.content).trim()) {
|
|
3316
|
+
const raw = createdAt(last);
|
|
3317
|
+
const lastMs = raw > 1e14 ? raw / 1e3 : raw < 1e12 ? raw * 1e3 : raw;
|
|
3318
|
+
if (lastMs > 0 && nowMs - lastMs > EMPTY_BOUNDARY_STALE_MS) {
|
|
3319
|
+
return { busy: false, currentTool: null };
|
|
3320
|
+
}
|
|
3064
3321
|
return { busy: true, currentTool: null };
|
|
3065
3322
|
}
|
|
3066
3323
|
return { busy: false, currentTool: null };
|
|
@@ -3803,18 +4060,18 @@ function run(cmd, args, maxBuffer = MAX_IMAGE_BYTES * 2) {
|
|
|
3803
4060
|
});
|
|
3804
4061
|
}
|
|
3805
4062
|
function fromFile(filePath) {
|
|
3806
|
-
const mime = FILE_MIMES[
|
|
4063
|
+
const mime = FILE_MIMES[path4.extname(filePath).toLowerCase()];
|
|
3807
4064
|
if (!mime) return null;
|
|
3808
4065
|
try {
|
|
3809
|
-
const stat =
|
|
4066
|
+
const stat = fs5.statSync(filePath);
|
|
3810
4067
|
if (!stat.isFile() || stat.size === 0 || stat.size > MAX_IMAGE_BYTES) return null;
|
|
3811
|
-
return { data:
|
|
4068
|
+
return { data: fs5.readFileSync(filePath).toString("base64"), mime };
|
|
3812
4069
|
} catch {
|
|
3813
4070
|
return null;
|
|
3814
4071
|
}
|
|
3815
4072
|
}
|
|
3816
4073
|
async function readDarwin() {
|
|
3817
|
-
const tmp =
|
|
4074
|
+
const tmp = path4.join(os8.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
|
|
3818
4075
|
const script = [
|
|
3819
4076
|
`set d to the clipboard as \xABclass PNGf\xBB`,
|
|
3820
4077
|
`set f to open for access POSIX file "${tmp}" with write permission`,
|
|
@@ -3826,7 +4083,7 @@ async function readDarwin() {
|
|
|
3826
4083
|
if (png.ok) {
|
|
3827
4084
|
const img = fromFile(tmp);
|
|
3828
4085
|
try {
|
|
3829
|
-
|
|
4086
|
+
fs5.unlinkSync(tmp);
|
|
3830
4087
|
} catch {
|
|
3831
4088
|
}
|
|
3832
4089
|
if (img) return img;
|
|
@@ -3851,7 +4108,7 @@ async function readLinux() {
|
|
|
3851
4108
|
return null;
|
|
3852
4109
|
}
|
|
3853
4110
|
async function readWindows() {
|
|
3854
|
-
const tmp =
|
|
4111
|
+
const tmp = path4.join(os8.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
|
|
3855
4112
|
const ps = [
|
|
3856
4113
|
"Add-Type -AssemblyName System.Windows.Forms;",
|
|
3857
4114
|
"$img = [System.Windows.Forms.Clipboard]::GetImage();",
|
|
@@ -3860,7 +4117,7 @@ async function readWindows() {
|
|
|
3860
4117
|
await run("powershell", ["-NoProfile", "-STA", "-Command", ps]);
|
|
3861
4118
|
const img = fromFile(tmp);
|
|
3862
4119
|
try {
|
|
3863
|
-
|
|
4120
|
+
fs5.unlinkSync(tmp);
|
|
3864
4121
|
} catch {
|
|
3865
4122
|
}
|
|
3866
4123
|
return img;
|
|
@@ -5797,10 +6054,10 @@ ${C.cyan}\u2503${C.reset} ${question}
|
|
|
5797
6054
|
|
|
5798
6055
|
// src/history.ts
|
|
5799
6056
|
var HISTORY_KEY = "input_history";
|
|
5800
|
-
var
|
|
6057
|
+
var MAX_ENTRIES2 = 100;
|
|
5801
6058
|
function clean(value) {
|
|
5802
6059
|
if (!Array.isArray(value)) return [];
|
|
5803
|
-
return value.filter((e) => typeof e === "string" && e.trim() !== "").slice(-
|
|
6060
|
+
return value.filter((e) => typeof e === "string" && e.trim() !== "").slice(-MAX_ENTRIES2);
|
|
5804
6061
|
}
|
|
5805
6062
|
async function loadHistory(store, threadId, seedThreadId) {
|
|
5806
6063
|
const own = clean(await store.kvGet(threadId, HISTORY_KEY));
|
|
@@ -5818,12 +6075,12 @@ function appendHistory(store, threadId, history, text) {
|
|
|
5818
6075
|
const t = text.trim();
|
|
5819
6076
|
if (!t || history[history.length - 1] === t) return history;
|
|
5820
6077
|
history.push(t);
|
|
5821
|
-
if (history.length >
|
|
6078
|
+
if (history.length > MAX_ENTRIES2) history.splice(0, history.length - MAX_ENTRIES2);
|
|
5822
6079
|
void store.kvSet(threadId, HISTORY_KEY, [...history]);
|
|
5823
6080
|
return history;
|
|
5824
6081
|
}
|
|
5825
|
-
var DIR =
|
|
5826
|
-
var FILE =
|
|
6082
|
+
var DIR = path4.join(os8.homedir(), ".standardagents");
|
|
6083
|
+
var FILE = path4.join(DIR, "credentials");
|
|
5827
6084
|
function normalizeEndpoint(endpoint) {
|
|
5828
6085
|
let e = endpoint.trim();
|
|
5829
6086
|
if (!/^https?:\/\//i.test(e)) e = "http://" + e;
|
|
@@ -5831,7 +6088,7 @@ function normalizeEndpoint(endpoint) {
|
|
|
5831
6088
|
}
|
|
5832
6089
|
function loadCredentials() {
|
|
5833
6090
|
try {
|
|
5834
|
-
const raw =
|
|
6091
|
+
const raw = fs5.readFileSync(FILE, "utf8");
|
|
5835
6092
|
const parsed = JSON.parse(raw);
|
|
5836
6093
|
if (!parsed.instances) parsed.instances = {};
|
|
5837
6094
|
return parsed;
|
|
@@ -5850,20 +6107,20 @@ function saveCredential(cred, options = {}) {
|
|
|
5850
6107
|
if (options.updateDefault ?? true) {
|
|
5851
6108
|
creds.default_endpoint = endpoint;
|
|
5852
6109
|
}
|
|
5853
|
-
|
|
5854
|
-
|
|
6110
|
+
fs5.mkdirSync(DIR, { recursive: true });
|
|
6111
|
+
fs5.writeFileSync(FILE, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
5855
6112
|
try {
|
|
5856
|
-
|
|
6113
|
+
fs5.chmodSync(FILE, 384);
|
|
5857
6114
|
} catch {
|
|
5858
6115
|
}
|
|
5859
6116
|
}
|
|
5860
6117
|
function deleteCredential(endpoint) {
|
|
5861
6118
|
const creds = loadCredentials();
|
|
5862
6119
|
delete creds.instances[normalizeEndpoint(endpoint)];
|
|
5863
|
-
|
|
5864
|
-
|
|
6120
|
+
fs5.mkdirSync(DIR, { recursive: true });
|
|
6121
|
+
fs5.writeFileSync(FILE, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
5865
6122
|
try {
|
|
5866
|
-
|
|
6123
|
+
fs5.chmodSync(FILE, 384);
|
|
5867
6124
|
} catch {
|
|
5868
6125
|
}
|
|
5869
6126
|
}
|
|
@@ -5873,10 +6130,10 @@ function defaultEndpoint() {
|
|
|
5873
6130
|
function saveDefaultEndpoint(endpoint) {
|
|
5874
6131
|
const creds = loadCredentials();
|
|
5875
6132
|
creds.default_endpoint = normalizeEndpoint(endpoint);
|
|
5876
|
-
|
|
5877
|
-
|
|
6133
|
+
fs5.mkdirSync(DIR, { recursive: true });
|
|
6134
|
+
fs5.writeFileSync(FILE, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
5878
6135
|
try {
|
|
5879
|
-
|
|
6136
|
+
fs5.chmodSync(FILE, 384);
|
|
5880
6137
|
} catch {
|
|
5881
6138
|
}
|
|
5882
6139
|
}
|
|
@@ -5937,7 +6194,7 @@ var AGENT_CHOICES = [
|
|
|
5937
6194
|
var PRODUCTION_ENDPOINT = "https://api.standardcode.ai";
|
|
5938
6195
|
function readVersion() {
|
|
5939
6196
|
try {
|
|
5940
|
-
const pkg = JSON.parse(
|
|
6197
|
+
const pkg = JSON.parse(fs5.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
5941
6198
|
return typeof pkg.version === "string" ? pkg.version : "";
|
|
5942
6199
|
} catch {
|
|
5943
6200
|
return "";
|
|
@@ -5963,11 +6220,11 @@ function relaxTlsForLocalEndpoint(endpoint) {
|
|
|
5963
6220
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
5964
6221
|
return true;
|
|
5965
6222
|
}
|
|
5966
|
-
var dir = () =>
|
|
5967
|
-
var file = () =>
|
|
6223
|
+
var dir = () => path4.join(os8.homedir(), ".standardagents");
|
|
6224
|
+
var file = () => path4.join(dir(), "machine.json");
|
|
5968
6225
|
function loadMachineIdentity() {
|
|
5969
6226
|
try {
|
|
5970
|
-
const parsed = JSON.parse(
|
|
6227
|
+
const parsed = JSON.parse(fs5.readFileSync(file(), "utf8"));
|
|
5971
6228
|
if (typeof parsed.machine_id === "string" && parsed.machine_id.length > 0) {
|
|
5972
6229
|
return { machine_id: parsed.machine_id, created_at: parsed.created_at ?? Date.now() };
|
|
5973
6230
|
}
|
|
@@ -5981,8 +6238,8 @@ function loadMachineIdentity() {
|
|
|
5981
6238
|
return identity;
|
|
5982
6239
|
}
|
|
5983
6240
|
function saveMachineIdentity(identity) {
|
|
5984
|
-
|
|
5985
|
-
|
|
6241
|
+
fs5.mkdirSync(dir(), { recursive: true });
|
|
6242
|
+
fs5.writeFileSync(file(), JSON.stringify(identity, null, 2), { mode: 384 });
|
|
5986
6243
|
}
|
|
5987
6244
|
function daemonClientId(identity) {
|
|
5988
6245
|
return `daemon:${identity.machine_id}`;
|
|
@@ -6163,8 +6420,8 @@ function newRecord(identity) {
|
|
|
6163
6420
|
const now = Date.now();
|
|
6164
6421
|
return {
|
|
6165
6422
|
id: identity.machine_id,
|
|
6166
|
-
name:
|
|
6167
|
-
hostname:
|
|
6423
|
+
name: os8.hostname(),
|
|
6424
|
+
hostname: os8.hostname(),
|
|
6168
6425
|
platform: process.platform,
|
|
6169
6426
|
arch: process.arch,
|
|
6170
6427
|
version: readVersion() || void 0,
|
|
@@ -6177,7 +6434,7 @@ function newRecord(identity) {
|
|
|
6177
6434
|
async function updateOwnMachineRecord(api, identity, mutate) {
|
|
6178
6435
|
const existing = await loadRawMachine(api, identity.machine_id);
|
|
6179
6436
|
const record2 = existing ?? newRecord(identity);
|
|
6180
|
-
record2.hostname =
|
|
6437
|
+
record2.hostname = os8.hostname();
|
|
6181
6438
|
record2.platform = process.platform;
|
|
6182
6439
|
record2.arch = process.arch;
|
|
6183
6440
|
record2.version = readVersion() || record2.version;
|
|
@@ -6201,9 +6458,9 @@ function projectRepository(projectDir) {
|
|
|
6201
6458
|
function normalizeProjectDir(projectDir) {
|
|
6202
6459
|
let p = projectDir.trim();
|
|
6203
6460
|
if (!p) return p;
|
|
6204
|
-
if (p === "~") p =
|
|
6205
|
-
else if (p.startsWith("~/")) p =
|
|
6206
|
-
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);
|
|
6207
6464
|
}
|
|
6208
6465
|
async function registerProject(api, identity, projectDir) {
|
|
6209
6466
|
const dir2 = normalizeProjectDir(projectDir);
|
|
@@ -6281,16 +6538,16 @@ async function clearMachineCommands(api, machineId, appliedIds) {
|
|
|
6281
6538
|
async function applyMachineCommand(api, identity, cmd) {
|
|
6282
6539
|
switch (cmd.kind) {
|
|
6283
6540
|
case "add_project": {
|
|
6284
|
-
const
|
|
6285
|
-
if (!
|
|
6286
|
-
await registerProject(api, identity,
|
|
6287
|
-
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}`;
|
|
6288
6545
|
}
|
|
6289
6546
|
case "remove_project": {
|
|
6290
|
-
const
|
|
6291
|
-
if (!
|
|
6292
|
-
await unregisterProject(api, identity,
|
|
6293
|
-
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}`;
|
|
6294
6551
|
}
|
|
6295
6552
|
case "update":
|
|
6296
6553
|
return "update requested";
|
|
@@ -6352,6 +6609,101 @@ async function awaitApprovalViaRelay(api, threadId, request, options = {}) {
|
|
|
6352
6609
|
});
|
|
6353
6610
|
}
|
|
6354
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
|
+
};
|
|
6355
6707
|
var PROJECT_MARKERS = [
|
|
6356
6708
|
".git",
|
|
6357
6709
|
"package.json",
|
|
@@ -6365,14 +6717,14 @@ var PROJECT_MARKERS = [
|
|
|
6365
6717
|
"requirements.txt",
|
|
6366
6718
|
".standardagents"
|
|
6367
6719
|
];
|
|
6368
|
-
var
|
|
6720
|
+
var MAX_ENTRIES3 = 500;
|
|
6369
6721
|
function resolveBrowsePath(input3) {
|
|
6370
|
-
const home =
|
|
6722
|
+
const home = os8.homedir();
|
|
6371
6723
|
let p = (input3 ?? "").trim();
|
|
6372
6724
|
if (!p) return home;
|
|
6373
6725
|
if (p === "~") return home;
|
|
6374
|
-
if (p.startsWith("~/")) p =
|
|
6375
|
-
return
|
|
6726
|
+
if (p.startsWith("~/")) p = path4.join(home, p.slice(2));
|
|
6727
|
+
return path4.resolve(p);
|
|
6376
6728
|
}
|
|
6377
6729
|
function markers(dirPath) {
|
|
6378
6730
|
let repo = false;
|
|
@@ -6380,7 +6732,7 @@ function markers(dirPath) {
|
|
|
6380
6732
|
for (const marker of PROJECT_MARKERS) {
|
|
6381
6733
|
let hit = false;
|
|
6382
6734
|
try {
|
|
6383
|
-
hit =
|
|
6735
|
+
hit = fs5.existsSync(path4.join(dirPath, marker));
|
|
6384
6736
|
} catch {
|
|
6385
6737
|
hit = false;
|
|
6386
6738
|
}
|
|
@@ -6392,9 +6744,9 @@ function markers(dirPath) {
|
|
|
6392
6744
|
return { project, repo };
|
|
6393
6745
|
}
|
|
6394
6746
|
function browseDirectory(input3, opts = {}) {
|
|
6395
|
-
const home =
|
|
6747
|
+
const home = os8.homedir();
|
|
6396
6748
|
const abs = resolveBrowsePath(input3);
|
|
6397
|
-
const parent =
|
|
6749
|
+
const parent = path4.dirname(abs);
|
|
6398
6750
|
const base = {
|
|
6399
6751
|
path: abs,
|
|
6400
6752
|
parent: parent === abs ? null : parent,
|
|
@@ -6402,11 +6754,11 @@ function browseDirectory(input3, opts = {}) {
|
|
|
6402
6754
|
};
|
|
6403
6755
|
let dirents;
|
|
6404
6756
|
try {
|
|
6405
|
-
const stat =
|
|
6757
|
+
const stat = fs5.statSync(abs);
|
|
6406
6758
|
if (!stat.isDirectory()) {
|
|
6407
6759
|
return { ...base, entries: [], truncated: false, error: "Not a directory" };
|
|
6408
6760
|
}
|
|
6409
|
-
dirents =
|
|
6761
|
+
dirents = fs5.readdirSync(abs, { withFileTypes: true });
|
|
6410
6762
|
} catch (err) {
|
|
6411
6763
|
const code = err?.code;
|
|
6412
6764
|
const message = code === "EACCES" || code === "EPERM" ? "Permission denied" : code === "ENOENT" ? "Folder not found" : "Could not read this folder";
|
|
@@ -6421,13 +6773,13 @@ function browseDirectory(input3, opts = {}) {
|
|
|
6421
6773
|
let isDir = d.isDirectory();
|
|
6422
6774
|
if (d.isSymbolicLink()) {
|
|
6423
6775
|
try {
|
|
6424
|
-
isDir =
|
|
6776
|
+
isDir = fs5.statSync(path4.join(abs, name)).isDirectory();
|
|
6425
6777
|
} catch {
|
|
6426
6778
|
isDir = false;
|
|
6427
6779
|
}
|
|
6428
6780
|
}
|
|
6429
6781
|
if (isDir) {
|
|
6430
|
-
const { project, repo } = markers(
|
|
6782
|
+
const { project, repo } = markers(path4.join(abs, name));
|
|
6431
6783
|
rawDirs.push({ name, dir: true, project: project || void 0, repo: repo || void 0 });
|
|
6432
6784
|
} else {
|
|
6433
6785
|
rawFiles.push({ name, dir: false });
|
|
@@ -6437,8 +6789,8 @@ function browseDirectory(input3, opts = {}) {
|
|
|
6437
6789
|
rawDirs.sort(cmp);
|
|
6438
6790
|
rawFiles.sort(cmp);
|
|
6439
6791
|
const all = [...rawDirs, ...rawFiles];
|
|
6440
|
-
const truncated = all.length >
|
|
6441
|
-
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 };
|
|
6442
6794
|
}
|
|
6443
6795
|
function mkdirDirectory(parentInput, name, opts = {}) {
|
|
6444
6796
|
const parent = resolveBrowsePath(parentInput);
|
|
@@ -6447,12 +6799,12 @@ function mkdirDirectory(parentInput, name, opts = {}) {
|
|
|
6447
6799
|
if (!clean2 || clean2 === "." || clean2 === ".." || clean2.includes("/") || clean2.includes("\\") || clean2.includes("\0")) {
|
|
6448
6800
|
return { ...listParent(), error: "Invalid folder name." };
|
|
6449
6801
|
}
|
|
6450
|
-
const target =
|
|
6451
|
-
if (
|
|
6802
|
+
const target = path4.join(parent, clean2);
|
|
6803
|
+
if (path4.dirname(target) !== parent) {
|
|
6452
6804
|
return { ...listParent(), error: "Invalid folder name." };
|
|
6453
6805
|
}
|
|
6454
6806
|
try {
|
|
6455
|
-
|
|
6807
|
+
fs5.mkdirSync(target, { recursive: false });
|
|
6456
6808
|
} catch (err) {
|
|
6457
6809
|
const code = err?.code;
|
|
6458
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.";
|
|
@@ -6469,14 +6821,14 @@ var CACHE_TTL_MS = 1e3 * 60 * 60 * 6;
|
|
|
6469
6821
|
var CHECK_TIMEOUT_MS = 4e3;
|
|
6470
6822
|
var IN_FLIGHT_TTL_MS = 15 * 60 * 1e3;
|
|
6471
6823
|
function cacheDir() {
|
|
6472
|
-
return
|
|
6824
|
+
return path4.join(homedir(), CACHE_REL_DIR);
|
|
6473
6825
|
}
|
|
6474
6826
|
function cachePath() {
|
|
6475
|
-
return
|
|
6827
|
+
return path4.join(cacheDir(), CACHE_FILE);
|
|
6476
6828
|
}
|
|
6477
6829
|
function readCache() {
|
|
6478
6830
|
try {
|
|
6479
|
-
const raw =
|
|
6831
|
+
const raw = fs5.readFileSync(cachePath(), "utf-8");
|
|
6480
6832
|
return JSON.parse(raw);
|
|
6481
6833
|
} catch {
|
|
6482
6834
|
return null;
|
|
@@ -6485,14 +6837,14 @@ function readCache() {
|
|
|
6485
6837
|
function writeCache(latest) {
|
|
6486
6838
|
try {
|
|
6487
6839
|
const dir2 = cacheDir();
|
|
6488
|
-
if (!
|
|
6489
|
-
|
|
6840
|
+
if (!fs5.existsSync(dir2)) fs5.mkdirSync(dir2, { recursive: true });
|
|
6841
|
+
fs5.writeFileSync(cachePath(), JSON.stringify({ latest, timestamp: Date.now() }));
|
|
6490
6842
|
} catch {
|
|
6491
6843
|
}
|
|
6492
6844
|
}
|
|
6493
6845
|
function readAutoUpdateState(dir2 = cacheDir()) {
|
|
6494
6846
|
try {
|
|
6495
|
-
const raw =
|
|
6847
|
+
const raw = fs5.readFileSync(path4.join(dir2, STATE_FILE), "utf-8");
|
|
6496
6848
|
const state = JSON.parse(raw);
|
|
6497
6849
|
return typeof state?.version === "string" ? state : null;
|
|
6498
6850
|
} catch {
|
|
@@ -6501,14 +6853,14 @@ function readAutoUpdateState(dir2 = cacheDir()) {
|
|
|
6501
6853
|
}
|
|
6502
6854
|
function writeAutoUpdateState(state, dir2 = cacheDir()) {
|
|
6503
6855
|
try {
|
|
6504
|
-
if (!
|
|
6505
|
-
|
|
6856
|
+
if (!fs5.existsSync(dir2)) fs5.mkdirSync(dir2, { recursive: true });
|
|
6857
|
+
fs5.writeFileSync(path4.join(dir2, STATE_FILE), JSON.stringify(state));
|
|
6506
6858
|
} catch {
|
|
6507
6859
|
}
|
|
6508
6860
|
}
|
|
6509
6861
|
function clearAutoUpdateState(dir2 = cacheDir()) {
|
|
6510
6862
|
try {
|
|
6511
|
-
|
|
6863
|
+
fs5.unlinkSync(path4.join(dir2, STATE_FILE));
|
|
6512
6864
|
} catch {
|
|
6513
6865
|
}
|
|
6514
6866
|
}
|
|
@@ -6551,7 +6903,7 @@ function decideAutoUpdate(info, opts) {
|
|
|
6551
6903
|
function startBackgroundUpdate(latest, pm, dir2 = cacheDir()) {
|
|
6552
6904
|
const startedAt = Date.now();
|
|
6553
6905
|
writeAutoUpdateState({ version: latest, startedAt, exitCode: null }, dir2);
|
|
6554
|
-
const stateFile =
|
|
6906
|
+
const stateFile = path4.join(dir2, STATE_FILE);
|
|
6555
6907
|
const { cmd, args } = updateCommand(pm);
|
|
6556
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}));`;
|
|
6557
6909
|
try {
|
|
@@ -6646,17 +6998,17 @@ var UPDATE_CHECK_MS = 6 * 60 * 6e4;
|
|
|
6646
6998
|
var SWEEP_MS = 10 * 6e4;
|
|
6647
6999
|
var MAX_WORKERS = 30;
|
|
6648
7000
|
var LOG_MAX_BYTES = 1e6;
|
|
6649
|
-
var LOG_FILE =
|
|
7001
|
+
var LOG_FILE = path4.join(os8.homedir(), ".standardagents", "daemon.log");
|
|
6650
7002
|
function daemonLog(line) {
|
|
6651
7003
|
try {
|
|
6652
|
-
|
|
7004
|
+
fs5.mkdirSync(path4.dirname(LOG_FILE), { recursive: true });
|
|
6653
7005
|
try {
|
|
6654
|
-
if (
|
|
6655
|
-
|
|
7006
|
+
if (fs5.statSync(LOG_FILE).size > LOG_MAX_BYTES) {
|
|
7007
|
+
fs5.renameSync(LOG_FILE, `${LOG_FILE}.old`);
|
|
6656
7008
|
}
|
|
6657
7009
|
} catch {
|
|
6658
7010
|
}
|
|
6659
|
-
|
|
7011
|
+
fs5.appendFileSync(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
6660
7012
|
`);
|
|
6661
7013
|
} catch {
|
|
6662
7014
|
}
|
|
@@ -6665,7 +7017,7 @@ function pathFromTags(tags) {
|
|
|
6665
7017
|
const tag = tags.find((t) => t.startsWith("path:"));
|
|
6666
7018
|
if (!tag) return null;
|
|
6667
7019
|
const raw = tag.slice("path:".length);
|
|
6668
|
-
return raw.replace(/^~(?=\/|$)/,
|
|
7020
|
+
return raw.replace(/^~(?=\/|$)/, os8.homedir());
|
|
6669
7021
|
}
|
|
6670
7022
|
var ThreadWorker = class {
|
|
6671
7023
|
constructor(api, identity, machineName, threadId, projectDir, createdAt2) {
|
|
@@ -6797,7 +7149,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
6797
7149
|
process.exit(1);
|
|
6798
7150
|
}
|
|
6799
7151
|
let displayName = machineDisplayName(await loadMachine(api, identity.machine_id).catch(() => null) ?? {
|
|
6800
|
-
hostname:
|
|
7152
|
+
hostname: os8.hostname(),
|
|
6801
7153
|
id: identity.machine_id
|
|
6802
7154
|
});
|
|
6803
7155
|
const applied = consumeAppliedUpdate(version);
|
|
@@ -6835,7 +7187,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
6835
7187
|
daemonLog(`detached ${oldest.threadId.slice(0, 8)} (worker cap)`);
|
|
6836
7188
|
}
|
|
6837
7189
|
try {
|
|
6838
|
-
|
|
7190
|
+
fs5.mkdirSync(projectDir, { recursive: true });
|
|
6839
7191
|
} catch (e) {
|
|
6840
7192
|
daemonLog(`cannot prepare project dir ${projectDir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
6841
7193
|
return;
|
|
@@ -6884,6 +7236,31 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
6884
7236
|
onThreadDeleted: (id) => detach(id)
|
|
6885
7237
|
});
|
|
6886
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();
|
|
6887
7264
|
await sweep();
|
|
6888
7265
|
const reclaim = setInterval(() => {
|
|
6889
7266
|
for (const worker of workers.values()) {
|
|
@@ -7007,6 +7384,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
7007
7384
|
clearInterval(sweeper);
|
|
7008
7385
|
for (const worker of workers.values()) worker.stop();
|
|
7009
7386
|
events.close();
|
|
7387
|
+
hub.close();
|
|
7010
7388
|
daemonLog(`daemon exiting (code ${code})`);
|
|
7011
7389
|
process.exit(code);
|
|
7012
7390
|
};
|
|
@@ -7019,19 +7397,19 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
7019
7397
|
var SERVICE_LABEL = "ai.standardcode.daemon";
|
|
7020
7398
|
var SYSTEMD_UNIT = "standardcode-daemon.service";
|
|
7021
7399
|
function resolveDaemonCommand(extraArgs = []) {
|
|
7022
|
-
const entry =
|
|
7400
|
+
const entry = path4.resolve(process.argv[1] ?? "");
|
|
7023
7401
|
if (!entry) throw new Error("Cannot determine how this CLI was launched.");
|
|
7024
7402
|
const argv = [process.execPath];
|
|
7025
7403
|
if (entry.endsWith(".ts")) {
|
|
7026
|
-
let dir2 =
|
|
7404
|
+
let dir2 = path4.dirname(entry);
|
|
7027
7405
|
let tsx = null;
|
|
7028
|
-
for (let i = 0; i < 6 && dir2 !==
|
|
7029
|
-
const candidate =
|
|
7030
|
-
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)) {
|
|
7031
7409
|
tsx = candidate;
|
|
7032
7410
|
break;
|
|
7033
7411
|
}
|
|
7034
|
-
dir2 =
|
|
7412
|
+
dir2 = path4.dirname(dir2);
|
|
7035
7413
|
}
|
|
7036
7414
|
if (!tsx) {
|
|
7037
7415
|
throw new Error(
|
|
@@ -7045,7 +7423,7 @@ function resolveDaemonCommand(extraArgs = []) {
|
|
|
7045
7423
|
}
|
|
7046
7424
|
function servicePath() {
|
|
7047
7425
|
const parts = [
|
|
7048
|
-
|
|
7426
|
+
path4.dirname(process.execPath),
|
|
7049
7427
|
"/opt/homebrew/bin",
|
|
7050
7428
|
"/usr/local/bin",
|
|
7051
7429
|
"/usr/bin",
|
|
@@ -7061,8 +7439,8 @@ function run2(cmd, args) {
|
|
|
7061
7439
|
const output4 = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
|
|
7062
7440
|
return { ok: res.status === 0, output: output4 };
|
|
7063
7441
|
}
|
|
7064
|
-
var plistPath = () =>
|
|
7065
|
-
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);
|
|
7066
7444
|
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
7067
7445
|
function installService(command, endpoint) {
|
|
7068
7446
|
if (process.platform === "darwin") return installLaunchd(command, endpoint);
|
|
@@ -7074,9 +7452,9 @@ function installService(command, endpoint) {
|
|
|
7074
7452
|
};
|
|
7075
7453
|
}
|
|
7076
7454
|
function installLaunchd(command, endpoint) {
|
|
7077
|
-
const logDir =
|
|
7078
|
-
|
|
7079
|
-
|
|
7455
|
+
const logDir = path4.join(os8.homedir(), ".standardagents");
|
|
7456
|
+
fs5.mkdirSync(logDir, { recursive: true });
|
|
7457
|
+
fs5.mkdirSync(path4.dirname(plistPath()), { recursive: true });
|
|
7080
7458
|
const envEntries = [
|
|
7081
7459
|
` <key>PATH</key><string>${xmlEscape(servicePath())}</string>`,
|
|
7082
7460
|
...endpoint ? [` <key>STANDARD_CODE_DAEMON_ENDPOINT</key><string>${xmlEscape(endpoint)}</string>`] : []
|
|
@@ -7093,8 +7471,8 @@ ${command.argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n")}
|
|
|
7093
7471
|
<key>RunAtLoad</key><true/>
|
|
7094
7472
|
<key>KeepAlive</key><true/>
|
|
7095
7473
|
<key>ThrottleInterval</key><integer>5</integer>
|
|
7096
|
-
<key>StandardOutPath</key><string>${xmlEscape(
|
|
7097
|
-
<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>
|
|
7098
7476
|
<key>EnvironmentVariables</key>
|
|
7099
7477
|
<dict>
|
|
7100
7478
|
${envEntries}
|
|
@@ -7102,7 +7480,7 @@ ${envEntries}
|
|
|
7102
7480
|
</dict>
|
|
7103
7481
|
</plist>
|
|
7104
7482
|
`;
|
|
7105
|
-
|
|
7483
|
+
fs5.writeFileSync(plistPath(), plist);
|
|
7106
7484
|
const uid = typeof process.getuid === "function" ? process.getuid() : 501;
|
|
7107
7485
|
run2("launchctl", ["bootout", `gui/${uid}`, plistPath()]);
|
|
7108
7486
|
const boot = run2("launchctl", ["bootstrap", `gui/${uid}`, plistPath()]);
|
|
@@ -7119,7 +7497,7 @@ ${envEntries}
|
|
|
7119
7497
|
return { ok: true, detail: `LaunchAgent installed (${plistPath()})` };
|
|
7120
7498
|
}
|
|
7121
7499
|
function installSystemd(command, endpoint) {
|
|
7122
|
-
|
|
7500
|
+
fs5.mkdirSync(path4.dirname(unitPath()), { recursive: true });
|
|
7123
7501
|
const unit = `[Unit]
|
|
7124
7502
|
Description=Standard Code daemon (headless coding-agent execution client)
|
|
7125
7503
|
After=network-online.target
|
|
@@ -7134,7 +7512,7 @@ ${endpoint ? `Environment=STANDARD_CODE_DAEMON_ENDPOINT=${endpoint}
|
|
|
7134
7512
|
[Install]
|
|
7135
7513
|
WantedBy=default.target
|
|
7136
7514
|
`;
|
|
7137
|
-
|
|
7515
|
+
fs5.writeFileSync(unitPath(), unit);
|
|
7138
7516
|
const reload = run2("systemctl", ["--user", "daemon-reload"]);
|
|
7139
7517
|
if (!reload.ok) {
|
|
7140
7518
|
return {
|
|
@@ -7147,10 +7525,10 @@ WantedBy=default.target
|
|
|
7147
7525
|
if (!enable.ok) {
|
|
7148
7526
|
return { ok: false, detail: `systemctl enable failed: ${enable.output}` };
|
|
7149
7527
|
}
|
|
7150
|
-
const linger = run2("loginctl", ["enable-linger",
|
|
7528
|
+
const linger = run2("loginctl", ["enable-linger", os8.userInfo().username]);
|
|
7151
7529
|
return {
|
|
7152
7530
|
ok: true,
|
|
7153
|
-
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}`)
|
|
7154
7532
|
};
|
|
7155
7533
|
}
|
|
7156
7534
|
function uninstallService() {
|
|
@@ -7158,7 +7536,7 @@ function uninstallService() {
|
|
|
7158
7536
|
const uid = typeof process.getuid === "function" ? process.getuid() : 501;
|
|
7159
7537
|
run2("launchctl", ["bootout", `gui/${uid}`, plistPath()]);
|
|
7160
7538
|
try {
|
|
7161
|
-
|
|
7539
|
+
fs5.unlinkSync(plistPath());
|
|
7162
7540
|
} catch {
|
|
7163
7541
|
}
|
|
7164
7542
|
return { ok: true, detail: "LaunchAgent removed" };
|
|
@@ -7166,7 +7544,7 @@ function uninstallService() {
|
|
|
7166
7544
|
if (process.platform === "linux") {
|
|
7167
7545
|
run2("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
|
|
7168
7546
|
try {
|
|
7169
|
-
|
|
7547
|
+
fs5.unlinkSync(unitPath());
|
|
7170
7548
|
} catch {
|
|
7171
7549
|
}
|
|
7172
7550
|
run2("systemctl", ["--user", "daemon-reload"]);
|
|
@@ -7176,7 +7554,7 @@ function uninstallService() {
|
|
|
7176
7554
|
}
|
|
7177
7555
|
function serviceStatus() {
|
|
7178
7556
|
if (process.platform === "darwin") {
|
|
7179
|
-
const installed =
|
|
7557
|
+
const installed = fs5.existsSync(plistPath());
|
|
7180
7558
|
const list = run2("launchctl", ["list", SERVICE_LABEL]);
|
|
7181
7559
|
const pidMatch = list.output.match(/"PID"\s*=\s*(\d+)/);
|
|
7182
7560
|
return {
|
|
@@ -7186,7 +7564,7 @@ function serviceStatus() {
|
|
|
7186
7564
|
};
|
|
7187
7565
|
}
|
|
7188
7566
|
if (process.platform === "linux") {
|
|
7189
|
-
const installed =
|
|
7567
|
+
const installed = fs5.existsSync(unitPath());
|
|
7190
7568
|
const active = run2("systemctl", ["--user", "is-active", SYSTEMD_UNIT]);
|
|
7191
7569
|
return {
|
|
7192
7570
|
installed,
|
|
@@ -7278,7 +7656,7 @@ async function installCommand(endpointFlag) {
|
|
|
7278
7656
|
const api = await ensureSignedIn(endpoint);
|
|
7279
7657
|
const identity = loadMachineIdentity();
|
|
7280
7658
|
const existing = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
7281
|
-
const suggested = machineDisplayName(existing ?? { hostname:
|
|
7659
|
+
const suggested = machineDisplayName(existing ?? { hostname: os8.hostname(), id: identity.machine_id });
|
|
7282
7660
|
const rl = readline2.createInterface({ input: stdin, output: stdout });
|
|
7283
7661
|
const answer = (await rl.question(
|
|
7284
7662
|
`${c2.bold}Machine name${c2.reset} ${c2.dim}(shown in the session picker)${c2.reset} [${suggested}]: `
|
|
@@ -7338,7 +7716,7 @@ async function statusCommand() {
|
|
|
7338
7716
|
const cred = getCredential(endpoint);
|
|
7339
7717
|
if (!cred) {
|
|
7340
7718
|
stdout.write(
|
|
7341
|
-
`${c2.bold}Machine:${c2.reset} ${
|
|
7719
|
+
`${c2.bold}Machine:${c2.reset} ${os8.hostname()} ${c2.dim}(${identity.machine_id})${c2.reset}
|
|
7342
7720
|
`
|
|
7343
7721
|
);
|
|
7344
7722
|
stdout.write(`${c2.bold}Account:${c2.reset} ${c2.yellow}not signed in to ${endpoint}${c2.reset}
|
|
@@ -7349,7 +7727,7 @@ async function statusCommand() {
|
|
|
7349
7727
|
const api = new ApiClient(endpoint, cred.access_token);
|
|
7350
7728
|
const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
7351
7729
|
stdout.write(
|
|
7352
|
-
`${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}
|
|
7353
7731
|
`
|
|
7354
7732
|
);
|
|
7355
7733
|
if (!record2) {
|
|
@@ -7373,8 +7751,8 @@ async function projectCommand(action, target) {
|
|
|
7373
7751
|
`);
|
|
7374
7752
|
process.exit(1);
|
|
7375
7753
|
}
|
|
7376
|
-
const dir2 =
|
|
7377
|
-
if (action === "add" && !
|
|
7754
|
+
const dir2 = path4.resolve(target);
|
|
7755
|
+
if (action === "add" && !fs5.existsSync(dir2)) {
|
|
7378
7756
|
stdout.write(`${c2.red}\u2717${c2.reset} ${dir2} does not exist on this machine.
|
|
7379
7757
|
`);
|
|
7380
7758
|
process.exit(1);
|
|
@@ -7383,7 +7761,7 @@ async function projectCommand(action, target) {
|
|
|
7383
7761
|
const api = await ensureSignedIn(endpoint);
|
|
7384
7762
|
const identity = loadMachineIdentity();
|
|
7385
7763
|
const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
7386
|
-
const displayName = machineDisplayName(record2 ?? { hostname:
|
|
7764
|
+
const displayName = machineDisplayName(record2 ?? { hostname: os8.hostname(), id: identity.machine_id });
|
|
7387
7765
|
if (action === "add") {
|
|
7388
7766
|
await registerProject(api, identity, dir2);
|
|
7389
7767
|
stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir2} for remote sessions on ${displayName}.
|
|
@@ -7598,7 +7976,7 @@ ${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.rese
|
|
|
7598
7976
|
`);
|
|
7599
7977
|
}
|
|
7600
7978
|
function printWelcome(endpoint, projectDir) {
|
|
7601
|
-
const home =
|
|
7979
|
+
const home = os8.homedir();
|
|
7602
7980
|
const dir2 = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
7603
7981
|
const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
7604
7982
|
const version = readVersion();
|
|
@@ -7668,8 +8046,8 @@ async function main() {
|
|
|
7668
8046
|
const endpointArg = cliArgs.endpoint;
|
|
7669
8047
|
const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
|
|
7670
8048
|
const dirArg = cliArgs.dir;
|
|
7671
|
-
const projectDir =
|
|
7672
|
-
const machine =
|
|
8049
|
+
const projectDir = path4.resolve(dirArg || process.cwd());
|
|
8050
|
+
const machine = os8.hostname();
|
|
7673
8051
|
const reader = { rl: null };
|
|
7674
8052
|
let handoffClosing = false;
|
|
7675
8053
|
let preflightArmed = false;
|
|
@@ -7883,7 +8261,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
7883
8261
|
const ok = await ensureSamaAuth(tui, api);
|
|
7884
8262
|
if (!ok) process.exit(0);
|
|
7885
8263
|
}
|
|
7886
|
-
const home =
|
|
8264
|
+
const home = os8.homedir();
|
|
7887
8265
|
const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
7888
8266
|
const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
|
|
7889
8267
|
const session = { mode: "local", identity };
|
|
@@ -9057,7 +9435,10 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
9057
9435
|
}
|
|
9058
9436
|
const polledBusy = (serverBusy ?? false) || threadBusy(msgs);
|
|
9059
9437
|
if (serverTool && !activeSteps.has(serverTool.id)) {
|
|
9060
|
-
activeSteps.set(
|
|
9438
|
+
activeSteps.set(
|
|
9439
|
+
serverTool.id,
|
|
9440
|
+
serverTool.waitingForHost ? "waiting for your machine to reconnect" : serverTool.name || "working"
|
|
9441
|
+
);
|
|
9061
9442
|
refreshStatus();
|
|
9062
9443
|
} else if (serverBusy !== null && !serverTool && activeSteps.size) {
|
|
9063
9444
|
activeSteps.clear();
|
|
@@ -9314,12 +9695,12 @@ async function manageMachineProjects(tui, api, self, machine, dispatch, applyNot
|
|
|
9314
9695
|
);
|
|
9315
9696
|
if (!picked) return;
|
|
9316
9697
|
if (picked === ADD) {
|
|
9317
|
-
const
|
|
9698
|
+
const path15 = await tui.prompt(
|
|
9318
9699
|
`Absolute project path on ${machine.name}`,
|
|
9319
9700
|
machine.id === self.machine_id ? process.cwd() : "/home/you/project"
|
|
9320
9701
|
);
|
|
9321
|
-
if (!
|
|
9322
|
-
const trimmed =
|
|
9702
|
+
if (!path15 || !path15.trim()) return;
|
|
9703
|
+
const trimmed = path15.trim();
|
|
9323
9704
|
if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
|
|
9324
9705
|
tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
|
|
9325
9706
|
return;
|