@standardagents/code 0.9.1 → 0.9.3
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 +445 -86
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -409,6 +409,21 @@ var ApiClient = class {
|
|
|
409
409
|
} catch {
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Run a user-typed `!command` on the thread's execution owner (wherever the
|
|
414
|
+
* session runs — e.g. a remote VPS daemon). The instance forwards it over
|
|
415
|
+
* the bridge, records it in history, and returns the combined output.
|
|
416
|
+
*/
|
|
417
|
+
async runCommand(threadId, command) {
|
|
418
|
+
try {
|
|
419
|
+
return await this.json(
|
|
420
|
+
`/api/threads/${threadId}/run_command`,
|
|
421
|
+
{ method: "POST", body: JSON.stringify({ command }) }
|
|
422
|
+
);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
412
427
|
/** The thread's current goal (set via set_goal / update_goal_step). */
|
|
413
428
|
async getGoal(threadId) {
|
|
414
429
|
try {
|
|
@@ -1208,9 +1223,9 @@ function setMcpServerEnabled(name, enabled) {
|
|
|
1208
1223
|
write(cfg);
|
|
1209
1224
|
}
|
|
1210
1225
|
function write(cfg) {
|
|
1211
|
-
const
|
|
1212
|
-
fs4.mkdirSync(path3.dirname(
|
|
1213
|
-
fs4.writeFileSync(
|
|
1226
|
+
const file2 = configFile();
|
|
1227
|
+
fs4.mkdirSync(path3.dirname(file2), { recursive: true });
|
|
1228
|
+
fs4.writeFileSync(file2, JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
1214
1229
|
}
|
|
1215
1230
|
function parseServerSpec(spec) {
|
|
1216
1231
|
const trimmed = spec.trim();
|
|
@@ -1349,12 +1364,12 @@ var HostTools = class {
|
|
|
1349
1364
|
}
|
|
1350
1365
|
}
|
|
1351
1366
|
async readFile(args) {
|
|
1352
|
-
const
|
|
1353
|
-
const stat = await fsp.stat(
|
|
1367
|
+
const file2 = this.resolve(String(args.path || ""));
|
|
1368
|
+
const stat = await fsp.stat(file2).catch(() => null);
|
|
1354
1369
|
if (!stat) return { ok: false, error: `File not found: ${args.path}` };
|
|
1355
1370
|
if (stat.isDirectory()) return { ok: false, error: `${args.path} is a directory` };
|
|
1356
1371
|
if (stat.size > 2e6) return { ok: false, error: `File too large (${stat.size} bytes)` };
|
|
1357
|
-
const content = await fsp.readFile(
|
|
1372
|
+
const content = await fsp.readFile(file2, "utf8");
|
|
1358
1373
|
const lines = content.split("\n");
|
|
1359
1374
|
const offset = typeof args.offset === "number" ? Math.max(1, args.offset) : 1;
|
|
1360
1375
|
const limit = typeof args.limit === "number" ? args.limit : lines.length;
|
|
@@ -1390,24 +1405,24 @@ var HostTools = class {
|
|
|
1390
1405
|
return { ok: true, result: rel.join("\n") || "(no files)" };
|
|
1391
1406
|
}
|
|
1392
1407
|
async writeFile(args) {
|
|
1393
|
-
const
|
|
1408
|
+
const file2 = this.resolve(String(args.path || ""));
|
|
1394
1409
|
const content = String(args.content ?? "");
|
|
1395
|
-
await fsp.mkdir(path3.dirname(
|
|
1396
|
-
const existed = fs4.existsSync(
|
|
1397
|
-
await fsp.writeFile(
|
|
1410
|
+
await fsp.mkdir(path3.dirname(file2), { recursive: true });
|
|
1411
|
+
const existed = fs4.existsSync(file2);
|
|
1412
|
+
await fsp.writeFile(file2, content, "utf8");
|
|
1398
1413
|
return {
|
|
1399
1414
|
ok: true,
|
|
1400
|
-
result: `${existed ? "Overwrote" : "Created"} ${path3.relative(this.projectDir,
|
|
1415
|
+
result: `${existed ? "Overwrote" : "Created"} ${path3.relative(this.projectDir, file2)} (${Buffer.byteLength(content)} bytes)`
|
|
1401
1416
|
};
|
|
1402
1417
|
}
|
|
1403
1418
|
async editFile(args) {
|
|
1404
|
-
const
|
|
1419
|
+
const file2 = this.resolve(String(args.path || ""));
|
|
1405
1420
|
const oldStr = String(args.old_string ?? "");
|
|
1406
1421
|
const newStr = String(args.new_string ?? "");
|
|
1407
1422
|
const replaceAll = args.replace_all === true;
|
|
1408
|
-
const stat = await fsp.stat(
|
|
1423
|
+
const stat = await fsp.stat(file2).catch(() => null);
|
|
1409
1424
|
if (!stat) return { ok: false, error: `File not found: ${args.path}` };
|
|
1410
|
-
const content = await fsp.readFile(
|
|
1425
|
+
const content = await fsp.readFile(file2, "utf8");
|
|
1411
1426
|
if (oldStr === "") return { ok: false, error: "old_string cannot be empty" };
|
|
1412
1427
|
const count = content.split(oldStr).length - 1;
|
|
1413
1428
|
if (count === 0) return { ok: false, error: "old_string not found in file (it must match exactly)." };
|
|
@@ -1415,8 +1430,8 @@ var HostTools = class {
|
|
|
1415
1430
|
return { ok: false, error: `old_string is not unique (${count} matches). Add more context or set replace_all.` };
|
|
1416
1431
|
}
|
|
1417
1432
|
const updated = replaceAll ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
|
|
1418
|
-
await fsp.writeFile(
|
|
1419
|
-
return { ok: true, result: `Edited ${path3.relative(this.projectDir,
|
|
1433
|
+
await fsp.writeFile(file2, updated, "utf8");
|
|
1434
|
+
return { ok: true, result: `Edited ${path3.relative(this.projectDir, file2)} (${count} replacement${count === 1 ? "" : "s"})` };
|
|
1420
1435
|
}
|
|
1421
1436
|
/**
|
|
1422
1437
|
* Copy a file from the THREAD filesystem (e.g. a generated /attachments/*
|
|
@@ -1839,11 +1854,11 @@ ${tail}` : " No output was captured.")
|
|
|
1839
1854
|
async walkGlob(base, pattern) {
|
|
1840
1855
|
const re = globToRegExp(pattern);
|
|
1841
1856
|
const out = [];
|
|
1842
|
-
const walk = async (
|
|
1843
|
-
const entries = await fsp.readdir(
|
|
1857
|
+
const walk = async (dir2) => {
|
|
1858
|
+
const entries = await fsp.readdir(dir2, { withFileTypes: true }).catch(() => []);
|
|
1844
1859
|
for (const e of entries) {
|
|
1845
1860
|
if (e.name === ".git" || e.name === "node_modules") continue;
|
|
1846
|
-
const full = path3.join(
|
|
1861
|
+
const full = path3.join(dir2, e.name);
|
|
1847
1862
|
if (e.isDirectory()) await walk(full);
|
|
1848
1863
|
else {
|
|
1849
1864
|
const rel = path3.relative(this.projectDir, full);
|
|
@@ -5039,13 +5054,13 @@ function relaxTlsForLocalEndpoint(endpoint) {
|
|
|
5039
5054
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
5040
5055
|
return true;
|
|
5041
5056
|
}
|
|
5042
|
-
var
|
|
5043
|
-
var
|
|
5057
|
+
var dir = () => path3.join(os9.homedir(), ".standardagents");
|
|
5058
|
+
var file = () => path3.join(dir(), "machine.json");
|
|
5044
5059
|
function loadMachineIdentity() {
|
|
5045
5060
|
try {
|
|
5046
|
-
const parsed = JSON.parse(fs4.readFileSync(
|
|
5061
|
+
const parsed = JSON.parse(fs4.readFileSync(file(), "utf8"));
|
|
5047
5062
|
if (typeof parsed.machine_id === "string" && parsed.machine_id.length > 0) {
|
|
5048
|
-
return parsed;
|
|
5063
|
+
return { machine_id: parsed.machine_id, created_at: parsed.created_at ?? Date.now() };
|
|
5049
5064
|
}
|
|
5050
5065
|
} catch {
|
|
5051
5066
|
}
|
|
@@ -5057,17 +5072,8 @@ function loadMachineIdentity() {
|
|
|
5057
5072
|
return identity;
|
|
5058
5073
|
}
|
|
5059
5074
|
function saveMachineIdentity(identity) {
|
|
5060
|
-
fs4.mkdirSync(
|
|
5061
|
-
fs4.writeFileSync(
|
|
5062
|
-
}
|
|
5063
|
-
function setMachineName(name) {
|
|
5064
|
-
const identity = loadMachineIdentity();
|
|
5065
|
-
identity.name = name.trim() || void 0;
|
|
5066
|
-
saveMachineIdentity(identity);
|
|
5067
|
-
return identity;
|
|
5068
|
-
}
|
|
5069
|
-
function machineDisplayName(identity) {
|
|
5070
|
-
return identity.name?.trim() || os9.hostname();
|
|
5075
|
+
fs4.mkdirSync(dir(), { recursive: true });
|
|
5076
|
+
fs4.writeFileSync(file(), JSON.stringify(identity, null, 2), { mode: 384 });
|
|
5071
5077
|
}
|
|
5072
5078
|
function daemonClientId(identity) {
|
|
5073
5079
|
return `daemon:${identity.machine_id}`;
|
|
@@ -5080,10 +5086,29 @@ function machineIdFromDaemonClientId(clientId) {
|
|
|
5080
5086
|
return clientId.startsWith("daemon:") ? clientId.slice("daemon:".length) : null;
|
|
5081
5087
|
}
|
|
5082
5088
|
var KEY_PREFIX = "standardcode.machine.";
|
|
5089
|
+
var CMD_SUFFIX = ".cmd";
|
|
5090
|
+
var NAME_SUFFIX = ".name";
|
|
5083
5091
|
var DAEMON_ONLINE_WINDOW_MS = 90 * 1e3;
|
|
5084
5092
|
function machineKey(machineId) {
|
|
5085
5093
|
return `${KEY_PREFIX}${machineId}`;
|
|
5086
5094
|
}
|
|
5095
|
+
function commandKey(machineId) {
|
|
5096
|
+
return `${KEY_PREFIX}${machineId}${CMD_SUFFIX}`;
|
|
5097
|
+
}
|
|
5098
|
+
function nameKey(machineId) {
|
|
5099
|
+
return `${KEY_PREFIX}${machineId}${NAME_SUFFIX}`;
|
|
5100
|
+
}
|
|
5101
|
+
function machineDisplayName(record) {
|
|
5102
|
+
return record.name?.trim() || "" || (record.hostname || "") || (record.id || "") || "machine";
|
|
5103
|
+
}
|
|
5104
|
+
async function getMachineName(api, machineId) {
|
|
5105
|
+
const v = await api.userKvGet(nameKey(machineId));
|
|
5106
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
5107
|
+
}
|
|
5108
|
+
async function setMachineName(api, machineId, name) {
|
|
5109
|
+
const trimmed = name.trim();
|
|
5110
|
+
await api.userKvSet(nameKey(machineId), trimmed || null);
|
|
5111
|
+
}
|
|
5087
5112
|
function parseMachineRecord(value) {
|
|
5088
5113
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
5089
5114
|
const r = value;
|
|
@@ -5094,18 +5119,43 @@ function parseMachineRecord(value) {
|
|
|
5094
5119
|
hostname: typeof r.hostname === "string" ? r.hostname : "",
|
|
5095
5120
|
platform: typeof r.platform === "string" ? r.platform : "",
|
|
5096
5121
|
arch: typeof r.arch === "string" ? r.arch : "",
|
|
5122
|
+
version: typeof r.version === "string" ? r.version : void 0,
|
|
5097
5123
|
daemon: r.daemon && typeof r.daemon === "object" && !Array.isArray(r.daemon) ? r.daemon : null,
|
|
5098
5124
|
projects: r.projects && typeof r.projects === "object" && !Array.isArray(r.projects) ? r.projects : {},
|
|
5099
5125
|
created_at: typeof r.created_at === "number" ? r.created_at : 0,
|
|
5100
5126
|
updated_at: typeof r.updated_at === "number" ? r.updated_at : 0
|
|
5101
5127
|
};
|
|
5102
5128
|
}
|
|
5129
|
+
async function loadRawMachine(api, machineId) {
|
|
5130
|
+
return parseMachineRecord(await api.userKvGet(machineKey(machineId)));
|
|
5131
|
+
}
|
|
5103
5132
|
async function loadMachines(api) {
|
|
5104
5133
|
const entries = await api.userKvList(KEY_PREFIX);
|
|
5105
|
-
|
|
5134
|
+
const records = [];
|
|
5135
|
+
const names = /* @__PURE__ */ new Map();
|
|
5136
|
+
for (const e of entries) {
|
|
5137
|
+
const rest = e.key.slice(KEY_PREFIX.length);
|
|
5138
|
+
if (rest.endsWith(CMD_SUFFIX)) continue;
|
|
5139
|
+
if (rest.endsWith(NAME_SUFFIX)) {
|
|
5140
|
+
const id = rest.slice(0, -NAME_SUFFIX.length);
|
|
5141
|
+
if (typeof e.value === "string" && e.value.trim()) names.set(id, e.value.trim());
|
|
5142
|
+
continue;
|
|
5143
|
+
}
|
|
5144
|
+
const rec = parseMachineRecord(e.value);
|
|
5145
|
+
if (rec) records.push(rec);
|
|
5146
|
+
}
|
|
5147
|
+
for (const rec of records) {
|
|
5148
|
+
const override = names.get(rec.id);
|
|
5149
|
+
if (override) rec.name = override;
|
|
5150
|
+
}
|
|
5151
|
+
return records;
|
|
5106
5152
|
}
|
|
5107
5153
|
async function loadMachine(api, machineId) {
|
|
5108
|
-
|
|
5154
|
+
const rec = await loadRawMachine(api, machineId);
|
|
5155
|
+
if (!rec) return null;
|
|
5156
|
+
const override = await getMachineName(api, machineId);
|
|
5157
|
+
if (override) rec.name = override;
|
|
5158
|
+
return rec;
|
|
5109
5159
|
}
|
|
5110
5160
|
function daemonOnline(record, now = Date.now()) {
|
|
5111
5161
|
return !!record.daemon && now - record.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
|
|
@@ -5114,10 +5164,11 @@ function newRecord(identity) {
|
|
|
5114
5164
|
const now = Date.now();
|
|
5115
5165
|
return {
|
|
5116
5166
|
id: identity.machine_id,
|
|
5117
|
-
name:
|
|
5167
|
+
name: os9.hostname(),
|
|
5118
5168
|
hostname: os9.hostname(),
|
|
5119
5169
|
platform: process.platform,
|
|
5120
5170
|
arch: process.arch,
|
|
5171
|
+
version: readVersion() || void 0,
|
|
5121
5172
|
daemon: null,
|
|
5122
5173
|
projects: {},
|
|
5123
5174
|
created_at: now,
|
|
@@ -5125,12 +5176,12 @@ function newRecord(identity) {
|
|
|
5125
5176
|
};
|
|
5126
5177
|
}
|
|
5127
5178
|
async function updateOwnMachineRecord(api, identity, mutate) {
|
|
5128
|
-
const existing = await
|
|
5179
|
+
const existing = await loadRawMachine(api, identity.machine_id);
|
|
5129
5180
|
const record = existing ?? newRecord(identity);
|
|
5130
|
-
record.name = machineDisplayName(identity);
|
|
5131
5181
|
record.hostname = os9.hostname();
|
|
5132
5182
|
record.platform = process.platform;
|
|
5133
5183
|
record.arch = process.arch;
|
|
5184
|
+
record.version = readVersion() || record.version;
|
|
5134
5185
|
mutate?.(record);
|
|
5135
5186
|
record.updated_at = Date.now();
|
|
5136
5187
|
await api.userKvSet(machineKey(identity.machine_id), record);
|
|
@@ -5179,6 +5230,56 @@ async function clearDaemon(api, identity) {
|
|
|
5179
5230
|
record.daemon = null;
|
|
5180
5231
|
});
|
|
5181
5232
|
}
|
|
5233
|
+
function parseCommands(value) {
|
|
5234
|
+
if (!Array.isArray(value)) return [];
|
|
5235
|
+
return value.filter(
|
|
5236
|
+
(c4) => !!c4 && typeof c4 === "object" && typeof c4.id === "string" && typeof c4.kind === "string"
|
|
5237
|
+
);
|
|
5238
|
+
}
|
|
5239
|
+
async function enqueueMachineCommand(api, machineId, kind, args) {
|
|
5240
|
+
const key = commandKey(machineId);
|
|
5241
|
+
const queue = parseCommands(await api.userKvGet(key));
|
|
5242
|
+
const cmd = {
|
|
5243
|
+
id: crypto.randomBytes(6).toString("hex"),
|
|
5244
|
+
kind,
|
|
5245
|
+
args,
|
|
5246
|
+
requested_at: Date.now()
|
|
5247
|
+
};
|
|
5248
|
+
queue.push(cmd);
|
|
5249
|
+
await api.userKvSet(key, queue);
|
|
5250
|
+
return cmd.id;
|
|
5251
|
+
}
|
|
5252
|
+
async function readMachineCommands(api, machineId) {
|
|
5253
|
+
return parseCommands(await api.userKvGet(commandKey(machineId)));
|
|
5254
|
+
}
|
|
5255
|
+
async function clearMachineCommands(api, machineId, appliedIds) {
|
|
5256
|
+
if (appliedIds.length === 0) return;
|
|
5257
|
+
const key = commandKey(machineId);
|
|
5258
|
+
const remaining = parseCommands(await api.userKvGet(key)).filter(
|
|
5259
|
+
(c4) => !appliedIds.includes(c4.id)
|
|
5260
|
+
);
|
|
5261
|
+
await api.userKvSet(key, remaining.length ? remaining : null);
|
|
5262
|
+
}
|
|
5263
|
+
async function applyMachineCommand(api, identity, cmd) {
|
|
5264
|
+
switch (cmd.kind) {
|
|
5265
|
+
case "add_project": {
|
|
5266
|
+
const path12 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
|
|
5267
|
+
if (!path12) return "add_project: ignored (no path)";
|
|
5268
|
+
await registerProject(api, identity, path12);
|
|
5269
|
+
return `added project ${path12}`;
|
|
5270
|
+
}
|
|
5271
|
+
case "remove_project": {
|
|
5272
|
+
const path12 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
|
|
5273
|
+
if (!path12) return "remove_project: ignored (no path)";
|
|
5274
|
+
await unregisterProject(api, identity, path12);
|
|
5275
|
+
return `removed project ${path12}`;
|
|
5276
|
+
}
|
|
5277
|
+
case "update":
|
|
5278
|
+
return "update requested";
|
|
5279
|
+
default:
|
|
5280
|
+
return `unknown command ${cmd.kind}`;
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5182
5283
|
|
|
5183
5284
|
// src/relay.ts
|
|
5184
5285
|
var APPROVAL_REQUEST_KEY = "approval_request";
|
|
@@ -5257,31 +5358,31 @@ function readCache() {
|
|
|
5257
5358
|
}
|
|
5258
5359
|
function writeCache(latest) {
|
|
5259
5360
|
try {
|
|
5260
|
-
const
|
|
5261
|
-
if (!fs4.existsSync(
|
|
5361
|
+
const dir2 = cacheDir();
|
|
5362
|
+
if (!fs4.existsSync(dir2)) fs4.mkdirSync(dir2, { recursive: true });
|
|
5262
5363
|
fs4.writeFileSync(cachePath(), JSON.stringify({ latest, timestamp: Date.now() }));
|
|
5263
5364
|
} catch {
|
|
5264
5365
|
}
|
|
5265
5366
|
}
|
|
5266
|
-
function readAutoUpdateState(
|
|
5367
|
+
function readAutoUpdateState(dir2 = cacheDir()) {
|
|
5267
5368
|
try {
|
|
5268
|
-
const raw = fs4.readFileSync(path3.join(
|
|
5369
|
+
const raw = fs4.readFileSync(path3.join(dir2, STATE_FILE), "utf-8");
|
|
5269
5370
|
const state = JSON.parse(raw);
|
|
5270
5371
|
return typeof state?.version === "string" ? state : null;
|
|
5271
5372
|
} catch {
|
|
5272
5373
|
return null;
|
|
5273
5374
|
}
|
|
5274
5375
|
}
|
|
5275
|
-
function writeAutoUpdateState(state,
|
|
5376
|
+
function writeAutoUpdateState(state, dir2 = cacheDir()) {
|
|
5276
5377
|
try {
|
|
5277
|
-
if (!fs4.existsSync(
|
|
5278
|
-
fs4.writeFileSync(path3.join(
|
|
5378
|
+
if (!fs4.existsSync(dir2)) fs4.mkdirSync(dir2, { recursive: true });
|
|
5379
|
+
fs4.writeFileSync(path3.join(dir2, STATE_FILE), JSON.stringify(state));
|
|
5279
5380
|
} catch {
|
|
5280
5381
|
}
|
|
5281
5382
|
}
|
|
5282
|
-
function clearAutoUpdateState(
|
|
5383
|
+
function clearAutoUpdateState(dir2 = cacheDir()) {
|
|
5283
5384
|
try {
|
|
5284
|
-
fs4.unlinkSync(path3.join(
|
|
5385
|
+
fs4.unlinkSync(path3.join(dir2, STATE_FILE));
|
|
5285
5386
|
} catch {
|
|
5286
5387
|
}
|
|
5287
5388
|
}
|
|
@@ -5321,10 +5422,10 @@ function decideAutoUpdate(info, opts) {
|
|
|
5321
5422
|
}
|
|
5322
5423
|
return "start";
|
|
5323
5424
|
}
|
|
5324
|
-
function startBackgroundUpdate(latest, pm,
|
|
5425
|
+
function startBackgroundUpdate(latest, pm, dir2 = cacheDir()) {
|
|
5325
5426
|
const startedAt = Date.now();
|
|
5326
|
-
writeAutoUpdateState({ version: latest, startedAt, exitCode: null },
|
|
5327
|
-
const stateFile = path3.join(
|
|
5427
|
+
writeAutoUpdateState({ version: latest, startedAt, exitCode: null }, dir2);
|
|
5428
|
+
const stateFile = path3.join(dir2, STATE_FILE);
|
|
5328
5429
|
const { cmd, args } = updateCommand(pm);
|
|
5329
5430
|
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}));`;
|
|
5330
5431
|
try {
|
|
@@ -5332,14 +5433,14 @@ function startBackgroundUpdate(latest, pm, dir = cacheDir()) {
|
|
|
5332
5433
|
child.unref();
|
|
5333
5434
|
return true;
|
|
5334
5435
|
} catch {
|
|
5335
|
-
writeAutoUpdateState({ version: latest, startedAt, exitCode: -1, finishedAt: Date.now() },
|
|
5436
|
+
writeAutoUpdateState({ version: latest, startedAt, exitCode: -1, finishedAt: Date.now() }, dir2);
|
|
5336
5437
|
return false;
|
|
5337
5438
|
}
|
|
5338
5439
|
}
|
|
5339
|
-
function consumeAppliedUpdate(currentVersion,
|
|
5340
|
-
const state = readAutoUpdateState(
|
|
5440
|
+
function consumeAppliedUpdate(currentVersion, dir2 = cacheDir()) {
|
|
5441
|
+
const state = readAutoUpdateState(dir2);
|
|
5341
5442
|
if (!state || state.version !== currentVersion) return null;
|
|
5342
|
-
clearAutoUpdateState(
|
|
5443
|
+
clearAutoUpdateState(dir2);
|
|
5343
5444
|
return state.exitCode === 0 ? state : null;
|
|
5344
5445
|
}
|
|
5345
5446
|
async function fetchLatest(currentVersion) {
|
|
@@ -5412,6 +5513,7 @@ function runUpdate(pm) {
|
|
|
5412
5513
|
|
|
5413
5514
|
// src/daemon.ts
|
|
5414
5515
|
var HEARTBEAT_MS = 3e4;
|
|
5516
|
+
var COMMAND_POLL_MS = 8e3;
|
|
5415
5517
|
var RECLAIM_PROBE_MS = 2 * 6e4;
|
|
5416
5518
|
var UPDATE_CHECK_MS = 6 * 60 * 6e4;
|
|
5417
5519
|
var SWEEP_MS = 10 * 6e4;
|
|
@@ -5439,11 +5541,10 @@ function pathFromTags(tags) {
|
|
|
5439
5541
|
return raw.replace(/^~(?=\/|$)/, os9.homedir());
|
|
5440
5542
|
}
|
|
5441
5543
|
var ThreadWorker = class {
|
|
5442
|
-
constructor(api, identity, threadId, projectDir, createdAt) {
|
|
5544
|
+
constructor(api, identity, machineName, threadId, projectDir, createdAt) {
|
|
5443
5545
|
this.threadId = threadId;
|
|
5444
5546
|
this.projectDir = projectDir;
|
|
5445
5547
|
this.createdAt = createdAt;
|
|
5446
|
-
const machineName = machineDisplayName(identity);
|
|
5447
5548
|
this.perm = { level: 1, alwaysAllow: /* @__PURE__ */ new Set(), allowRisk: /* @__PURE__ */ new Set() };
|
|
5448
5549
|
this.session = new ExecutionSession({
|
|
5449
5550
|
api,
|
|
@@ -5568,9 +5669,13 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
5568
5669
|
`);
|
|
5569
5670
|
process.exit(1);
|
|
5570
5671
|
}
|
|
5672
|
+
let displayName = machineDisplayName(await loadMachine(api, identity.machine_id).catch(() => null) ?? {
|
|
5673
|
+
hostname: os9.hostname(),
|
|
5674
|
+
id: identity.machine_id
|
|
5675
|
+
});
|
|
5571
5676
|
const applied = consumeAppliedUpdate(version);
|
|
5572
5677
|
daemonLog(
|
|
5573
|
-
`daemon starting \u2014 v${version}${applied ? " (freshly auto-updated)" : ""}, machine ${identity.machine_id} (${
|
|
5678
|
+
`daemon starting \u2014 v${version}${applied ? " (freshly auto-updated)" : ""}, machine ${identity.machine_id} (${displayName}), endpoint ${endpoint}`
|
|
5574
5679
|
);
|
|
5575
5680
|
process.on("uncaughtException", (err) => {
|
|
5576
5681
|
daemonLog(`FATAL uncaughtException: ${err?.stack || err}`);
|
|
@@ -5597,7 +5702,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
5597
5702
|
daemonLog(`cannot prepare project dir ${projectDir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
5598
5703
|
return;
|
|
5599
5704
|
}
|
|
5600
|
-
const worker = new ThreadWorker(api, identity, threadId, projectDir, createdAt || Date.now());
|
|
5705
|
+
const worker = new ThreadWorker(api, identity, displayName, threadId, projectDir, createdAt || Date.now());
|
|
5601
5706
|
worker.onEvicted = (id) => detach(id);
|
|
5602
5707
|
workers.set(threadId, worker);
|
|
5603
5708
|
daemonLog(`attached ${threadId.slice(0, 8)} \u2192 ${projectDir}`);
|
|
@@ -5642,6 +5747,10 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
5642
5747
|
const heartbeat = setInterval(() => {
|
|
5643
5748
|
void touchDaemon(api, identity, version).catch(() => {
|
|
5644
5749
|
});
|
|
5750
|
+
void getMachineName(api, identity.machine_id).then((n) => {
|
|
5751
|
+
if (n) displayName = n;
|
|
5752
|
+
}).catch(() => {
|
|
5753
|
+
});
|
|
5645
5754
|
}, HEARTBEAT_MS);
|
|
5646
5755
|
const reclaim = setInterval(() => {
|
|
5647
5756
|
for (const worker of workers.values()) {
|
|
@@ -5674,6 +5783,55 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
5674
5783
|
};
|
|
5675
5784
|
void checkUpdates();
|
|
5676
5785
|
const updateTimer = setInterval(() => void checkUpdates(), UPDATE_CHECK_MS);
|
|
5786
|
+
let draining = false;
|
|
5787
|
+
const drainCommands = async () => {
|
|
5788
|
+
if (draining) return;
|
|
5789
|
+
draining = true;
|
|
5790
|
+
try {
|
|
5791
|
+
const cmds = await readMachineCommands(api, identity.machine_id);
|
|
5792
|
+
if (cmds.length === 0) return;
|
|
5793
|
+
const applied2 = [];
|
|
5794
|
+
let forcedUpdate = false;
|
|
5795
|
+
for (const cmd of cmds) {
|
|
5796
|
+
try {
|
|
5797
|
+
if (cmd.kind === "update") {
|
|
5798
|
+
forcedUpdate = true;
|
|
5799
|
+
applied2.push(cmd.id);
|
|
5800
|
+
continue;
|
|
5801
|
+
}
|
|
5802
|
+
const result = await applyMachineCommand(api, identity, cmd);
|
|
5803
|
+
daemonLog(`command ${cmd.kind}: ${result}`);
|
|
5804
|
+
applied2.push(cmd.id);
|
|
5805
|
+
} catch (e) {
|
|
5806
|
+
daemonLog(`command ${cmd.kind} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
5807
|
+
applied2.push(cmd.id);
|
|
5808
|
+
}
|
|
5809
|
+
}
|
|
5810
|
+
await clearMachineCommands(api, identity.machine_id, applied2).catch(() => {
|
|
5811
|
+
});
|
|
5812
|
+
if (forcedUpdate) {
|
|
5813
|
+
const pm = detectPackageManager();
|
|
5814
|
+
if (!pm) {
|
|
5815
|
+
daemonLog("forced update requested but this is not an installed build \u2014 ignoring");
|
|
5816
|
+
} else if (![...workers.values()].some((w) => w.busy)) {
|
|
5817
|
+
daemonLog(`forced update: running ${pm} install now`);
|
|
5818
|
+
const { ok, output: output4 } = await runUpdate(pm);
|
|
5819
|
+
daemonLog(`forced update ${ok ? "succeeded" : "failed"}: ${output4.trim().split("\n").slice(-2).join(" | ")}`);
|
|
5820
|
+
if (ok) {
|
|
5821
|
+
daemonLog("restarting to apply the forced update");
|
|
5822
|
+
shutdown(0);
|
|
5823
|
+
}
|
|
5824
|
+
} else {
|
|
5825
|
+
daemonLog("forced update deferred \u2014 a session is busy; will retry");
|
|
5826
|
+
void checkUpdates();
|
|
5827
|
+
}
|
|
5828
|
+
}
|
|
5829
|
+
} finally {
|
|
5830
|
+
draining = false;
|
|
5831
|
+
}
|
|
5832
|
+
};
|
|
5833
|
+
void drainCommands();
|
|
5834
|
+
const commandTimer = setInterval(() => void drainCommands(), COMMAND_POLL_MS);
|
|
5677
5835
|
const sweeper = setInterval(() => {
|
|
5678
5836
|
if (updateReady && ![...workers.values()].some((w) => w.busy)) {
|
|
5679
5837
|
daemonLog("restarting to apply the installed update");
|
|
@@ -5686,6 +5844,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
5686
5844
|
clearInterval(heartbeat);
|
|
5687
5845
|
clearInterval(reclaim);
|
|
5688
5846
|
clearInterval(updateTimer);
|
|
5847
|
+
clearInterval(commandTimer);
|
|
5689
5848
|
clearInterval(sweeper);
|
|
5690
5849
|
for (const worker of workers.values()) worker.stop();
|
|
5691
5850
|
events.close();
|
|
@@ -5705,15 +5864,15 @@ function resolveDaemonCommand(extraArgs = []) {
|
|
|
5705
5864
|
if (!entry) throw new Error("Cannot determine how this CLI was launched.");
|
|
5706
5865
|
const argv = [process.execPath];
|
|
5707
5866
|
if (entry.endsWith(".ts")) {
|
|
5708
|
-
let
|
|
5867
|
+
let dir2 = path3.dirname(entry);
|
|
5709
5868
|
let tsx = null;
|
|
5710
|
-
for (let i = 0; i < 6 &&
|
|
5711
|
-
const candidate = path3.join(
|
|
5869
|
+
for (let i = 0; i < 6 && dir2 !== path3.dirname(dir2); i++) {
|
|
5870
|
+
const candidate = path3.join(dir2, "node_modules", "tsx", "dist", "cli.mjs");
|
|
5712
5871
|
if (fs4.existsSync(candidate)) {
|
|
5713
5872
|
tsx = candidate;
|
|
5714
5873
|
break;
|
|
5715
5874
|
}
|
|
5716
|
-
|
|
5875
|
+
dir2 = path3.dirname(dir2);
|
|
5717
5876
|
}
|
|
5718
5877
|
if (!tsx) {
|
|
5719
5878
|
throw new Error(
|
|
@@ -5959,15 +6118,16 @@ async function installCommand(endpointFlag) {
|
|
|
5959
6118
|
const endpoint = resolveEndpoint(endpointFlag);
|
|
5960
6119
|
const api = await ensureSignedIn(endpoint);
|
|
5961
6120
|
const identity = loadMachineIdentity();
|
|
6121
|
+
const existing = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
6122
|
+
const suggested = machineDisplayName(existing ?? { hostname: os9.hostname(), id: identity.machine_id });
|
|
5962
6123
|
const rl = readline2.createInterface({ input: stdin, output: stdout });
|
|
5963
|
-
const suggested = machineDisplayName(identity);
|
|
5964
6124
|
const answer = (await rl.question(
|
|
5965
6125
|
`${c2.bold}Machine name${c2.reset} ${c2.dim}(shown in the session picker)${c2.reset} [${suggested}]: `
|
|
5966
6126
|
)).trim();
|
|
5967
6127
|
rl.close();
|
|
5968
|
-
if (answer) setMachineName(answer);
|
|
5969
|
-
|
|
5970
|
-
|
|
6128
|
+
if (answer) await setMachineName(api, identity.machine_id, answer);
|
|
6129
|
+
await updateOwnMachineRecord(api, identity);
|
|
6130
|
+
const displayName = answer || suggested;
|
|
5971
6131
|
stdout.write(`${c2.dim}Installing the always-on service\u2026${c2.reset}
|
|
5972
6132
|
`);
|
|
5973
6133
|
const extra = endpointFlag ? ["--endpoint", endpoint] : [];
|
|
@@ -5987,7 +6147,7 @@ async function installCommand(endpointFlag) {
|
|
|
5987
6147
|
let alive = false;
|
|
5988
6148
|
while (Date.now() < deadline) {
|
|
5989
6149
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
5990
|
-
const record = await loadMachine(api,
|
|
6150
|
+
const record = await loadMachine(api, identity.machine_id);
|
|
5991
6151
|
if (record && daemonOnline(record)) {
|
|
5992
6152
|
alive = true;
|
|
5993
6153
|
break;
|
|
@@ -5995,7 +6155,7 @@ async function installCommand(endpointFlag) {
|
|
|
5995
6155
|
}
|
|
5996
6156
|
if (alive) {
|
|
5997
6157
|
stdout.write(
|
|
5998
|
-
`${c2.green}\u2713${c2.reset} ${c2.bold}${
|
|
6158
|
+
`${c2.green}\u2713${c2.reset} ${c2.bold}${displayName}${c2.reset} is online.
|
|
5999
6159
|
|
|
6000
6160
|
Sessions started elsewhere can now run on this machine.
|
|
6001
6161
|
${c2.dim}Projects register automatically when you run standardcode in a directory here,
|
|
@@ -6014,12 +6174,14 @@ async function statusCommand() {
|
|
|
6014
6174
|
const status = serviceStatus();
|
|
6015
6175
|
const identity = loadMachineIdentity();
|
|
6016
6176
|
stdout.write(`${c2.bold}Service:${c2.reset} ${status.detail}
|
|
6017
|
-
`);
|
|
6018
|
-
stdout.write(`${c2.bold}Machine:${c2.reset} ${machineDisplayName(identity)} ${c2.dim}(${identity.machine_id})${c2.reset}
|
|
6019
6177
|
`);
|
|
6020
6178
|
const endpoint = resolveEndpoint();
|
|
6021
6179
|
const cred = getCredential(endpoint);
|
|
6022
6180
|
if (!cred) {
|
|
6181
|
+
stdout.write(
|
|
6182
|
+
`${c2.bold}Machine:${c2.reset} ${os9.hostname()} ${c2.dim}(${identity.machine_id})${c2.reset}
|
|
6183
|
+
`
|
|
6184
|
+
);
|
|
6023
6185
|
stdout.write(`${c2.bold}Account:${c2.reset} ${c2.yellow}not signed in to ${endpoint}${c2.reset}
|
|
6024
6186
|
`);
|
|
6025
6187
|
return;
|
|
@@ -6027,6 +6189,10 @@ async function statusCommand() {
|
|
|
6027
6189
|
relaxTlsForLocalEndpoint(endpoint);
|
|
6028
6190
|
const api = new ApiClient(endpoint, cred.access_token);
|
|
6029
6191
|
const record = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
6192
|
+
stdout.write(
|
|
6193
|
+
`${c2.bold}Machine:${c2.reset} ${machineDisplayName(record ?? { hostname: os9.hostname(), id: identity.machine_id })} ${c2.dim}(${identity.machine_id})${c2.reset}
|
|
6194
|
+
`
|
|
6195
|
+
);
|
|
6030
6196
|
if (!record) {
|
|
6031
6197
|
stdout.write(`${c2.bold}Registry:${c2.reset} not registered yet
|
|
6032
6198
|
`);
|
|
@@ -6048,22 +6214,24 @@ async function projectCommand(action, target) {
|
|
|
6048
6214
|
`);
|
|
6049
6215
|
process.exit(1);
|
|
6050
6216
|
}
|
|
6051
|
-
const
|
|
6052
|
-
if (action === "add" && !fs4.existsSync(
|
|
6053
|
-
stdout.write(`${c2.red}\u2717${c2.reset} ${
|
|
6217
|
+
const dir2 = path3.resolve(target);
|
|
6218
|
+
if (action === "add" && !fs4.existsSync(dir2)) {
|
|
6219
|
+
stdout.write(`${c2.red}\u2717${c2.reset} ${dir2} does not exist on this machine.
|
|
6054
6220
|
`);
|
|
6055
6221
|
process.exit(1);
|
|
6056
6222
|
}
|
|
6057
6223
|
const endpoint = resolveEndpoint();
|
|
6058
6224
|
const api = await ensureSignedIn(endpoint);
|
|
6059
6225
|
const identity = loadMachineIdentity();
|
|
6226
|
+
const record = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
6227
|
+
const displayName = machineDisplayName(record ?? { hostname: os9.hostname(), id: identity.machine_id });
|
|
6060
6228
|
if (action === "add") {
|
|
6061
|
-
await registerProject(api, identity,
|
|
6062
|
-
stdout.write(`${c2.green}\u2713${c2.reset} Registered ${
|
|
6229
|
+
await registerProject(api, identity, dir2);
|
|
6230
|
+
stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir2} for remote sessions on ${displayName}.
|
|
6063
6231
|
`);
|
|
6064
6232
|
} else {
|
|
6065
|
-
await unregisterProject(api, identity,
|
|
6066
|
-
stdout.write(`${c2.green}\u2713${c2.reset} Removed ${
|
|
6233
|
+
await unregisterProject(api, identity, dir2);
|
|
6234
|
+
stdout.write(`${c2.green}\u2713${c2.reset} Removed ${dir2} from this machine's projects.
|
|
6067
6235
|
`);
|
|
6068
6236
|
}
|
|
6069
6237
|
}
|
|
@@ -6195,6 +6363,20 @@ function parseArgs2(args) {
|
|
|
6195
6363
|
}
|
|
6196
6364
|
return parsed;
|
|
6197
6365
|
}
|
|
6366
|
+
function printCommandBlock(tui, command, output4, ok, where) {
|
|
6367
|
+
tui.print("");
|
|
6368
|
+
const note = where ? ` ${c3.dim}(ran on ${where})${c3.reset}` : "";
|
|
6369
|
+
tui.print(`${c3.magenta}!${c3.reset} ${c3.bold}${command}${c3.reset}${note}`);
|
|
6370
|
+
const body = (output4 ?? "").replace(/\s+$/, "");
|
|
6371
|
+
if (body) {
|
|
6372
|
+
for (const line of body.split("\n")) {
|
|
6373
|
+
tui.print(` ${ok ? c3.dim : c3.red}${line}${c3.reset}`);
|
|
6374
|
+
}
|
|
6375
|
+
} else {
|
|
6376
|
+
tui.print(` ${c3.dim}(no output)${c3.reset}`);
|
|
6377
|
+
}
|
|
6378
|
+
tui.print("");
|
|
6379
|
+
}
|
|
6198
6380
|
function printAssistant(tui, text) {
|
|
6199
6381
|
const cols2 = Math.max(20, (process.stdout.columns || 80) - 3);
|
|
6200
6382
|
tui.clearStream();
|
|
@@ -6243,7 +6425,7 @@ ${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.rese
|
|
|
6243
6425
|
}
|
|
6244
6426
|
function printWelcome(endpoint, projectDir) {
|
|
6245
6427
|
const home = os9.homedir();
|
|
6246
|
-
const
|
|
6428
|
+
const dir2 = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
6247
6429
|
const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
6248
6430
|
const version = readVersion();
|
|
6249
6431
|
const pad = " ";
|
|
@@ -6251,7 +6433,7 @@ function printWelcome(endpoint, projectDir) {
|
|
|
6251
6433
|
`${c3.bold}${gradientText("Standard Code")}${c3.reset}${version ? ` ${c3.dim}v${version}${c3.reset}` : ""}`,
|
|
6252
6434
|
`${c3.dim}terminal coding agent${c3.reset}`,
|
|
6253
6435
|
...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c3.teal}${host}${c3.reset}`],
|
|
6254
|
-
`${c3.dim}${
|
|
6436
|
+
`${c3.dim}${dir2}${c3.reset}`
|
|
6255
6437
|
];
|
|
6256
6438
|
const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
|
|
6257
6439
|
const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
|
|
@@ -6598,10 +6780,10 @@ async function pickRemoteProject(tui, runner) {
|
|
|
6598
6780
|
const projects = Object.entries(runner.projects).sort(
|
|
6599
6781
|
(a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
|
|
6600
6782
|
);
|
|
6601
|
-
const items = projects.map(([
|
|
6602
|
-
label: shortenPath(
|
|
6783
|
+
const items = projects.map(([dir2, p]) => ({
|
|
6784
|
+
label: shortenPath(dir2, 48),
|
|
6603
6785
|
hint: p?.last_used_at ? relativeTime(p.last_used_at / 1e3) : "",
|
|
6604
|
-
value:
|
|
6786
|
+
value: dir2
|
|
6605
6787
|
}));
|
|
6606
6788
|
items.push({ label: `\uFF0B Another path on ${runner.name}\u2026`, hint: "type a directory", value: ENTER_PATH });
|
|
6607
6789
|
const picked = await tui.select(
|
|
@@ -6687,12 +6869,23 @@ async function printHistory(api, threadId, tui) {
|
|
|
6687
6869
|
} catch {
|
|
6688
6870
|
return;
|
|
6689
6871
|
}
|
|
6690
|
-
const convo = msgs.filter(
|
|
6872
|
+
const convo = msgs.filter(
|
|
6873
|
+
(m) => m.metadata?.user_command || m.role === "user" || m.role === "assistant" && messageText(m.content).trim()
|
|
6874
|
+
).sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
|
|
6691
6875
|
if (!convo.length) return;
|
|
6692
6876
|
const shown = convo.slice(-24);
|
|
6693
6877
|
tui.print(`${c3.dim}\u2500\u2500 resuming session \xB7 ${convo.length} message${convo.length === 1 ? "" : "s"} \u2500\u2500${c3.reset}`);
|
|
6694
6878
|
if (shown.length < convo.length) tui.print(`${c3.dim} \u2026 earlier messages omitted${c3.reset}`);
|
|
6695
6879
|
for (const m of shown) {
|
|
6880
|
+
if (m.metadata?.user_command) {
|
|
6881
|
+
printCommandBlock(
|
|
6882
|
+
tui,
|
|
6883
|
+
String(m.metadata.command ?? ""),
|
|
6884
|
+
String(m.metadata.output ?? messageText(m.content)),
|
|
6885
|
+
m.metadata.ok !== false
|
|
6886
|
+
);
|
|
6887
|
+
continue;
|
|
6888
|
+
}
|
|
6696
6889
|
const text = messageText(m.content).trim();
|
|
6697
6890
|
if (!text) continue;
|
|
6698
6891
|
if (m.role === "user") tui.printUserMessage(text);
|
|
@@ -6752,6 +6945,12 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
|
|
|
6752
6945
|
if (ownerDaemonMachine === session.identity.machine_id) claim = "takeover";
|
|
6753
6946
|
}
|
|
6754
6947
|
if (!remote) {
|
|
6948
|
+
const localDisplayName = machineDisplayName(
|
|
6949
|
+
await loadMachine(api, session.identity.machine_id).catch(() => null) ?? {
|
|
6950
|
+
hostname: machine,
|
|
6951
|
+
id: session.identity.machine_id
|
|
6952
|
+
}
|
|
6953
|
+
);
|
|
6755
6954
|
exec = new ExecutionSession({
|
|
6756
6955
|
api,
|
|
6757
6956
|
threadId,
|
|
@@ -6760,7 +6959,7 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
|
|
|
6760
6959
|
perm,
|
|
6761
6960
|
identity: {
|
|
6762
6961
|
clientId: interactiveClientId(session.identity),
|
|
6763
|
-
clientName: `${
|
|
6962
|
+
clientName: `${localDisplayName} (terminal)`,
|
|
6764
6963
|
clientKind: "interactive",
|
|
6765
6964
|
claim
|
|
6766
6965
|
},
|
|
@@ -7009,6 +7208,25 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
7009
7208
|
busy = true;
|
|
7010
7209
|
tui.setWorking(true);
|
|
7011
7210
|
};
|
|
7211
|
+
const whereLabel = remote ? runnerName : "this machine";
|
|
7212
|
+
let bangRunning = false;
|
|
7213
|
+
const runBangCommand = async (command) => {
|
|
7214
|
+
if (bangRunning) {
|
|
7215
|
+
tui.print(`${c3.dim}a command is already running \u2014 one at a time.${c3.reset}`);
|
|
7216
|
+
return;
|
|
7217
|
+
}
|
|
7218
|
+
bangRunning = true;
|
|
7219
|
+
tui.print(`${c3.magenta}!${c3.reset} ${c3.dim}running on ${whereLabel}\u2026${c3.reset}`);
|
|
7220
|
+
try {
|
|
7221
|
+
const res = await api.runCommand(threadId, command);
|
|
7222
|
+
if (res.messageId) shownIds.add(res.messageId);
|
|
7223
|
+
printCommandBlock(tui, command, res.ok ? res.output ?? "" : res.error ?? "command failed", res.ok, whereLabel);
|
|
7224
|
+
} catch (e) {
|
|
7225
|
+
printCommandBlock(tui, command, e instanceof Error ? e.message : String(e), false, whereLabel);
|
|
7226
|
+
} finally {
|
|
7227
|
+
bangRunning = false;
|
|
7228
|
+
}
|
|
7229
|
+
};
|
|
7012
7230
|
const flushQueued = async () => {
|
|
7013
7231
|
if (!queued.length) return;
|
|
7014
7232
|
const toSend = queued.splice(0);
|
|
@@ -7173,6 +7391,12 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
7173
7391
|
run: () => runMcpMenu(tui, mcpCtl)
|
|
7174
7392
|
}
|
|
7175
7393
|
],
|
|
7394
|
+
{
|
|
7395
|
+
name: "machines",
|
|
7396
|
+
label: "Your machines",
|
|
7397
|
+
hint: "list, rename, update, manage projects",
|
|
7398
|
+
run: () => runMachinesMenu(tui, api, session.identity)
|
|
7399
|
+
},
|
|
7176
7400
|
{
|
|
7177
7401
|
name: "daemon",
|
|
7178
7402
|
label: "Machine daemon",
|
|
@@ -7206,7 +7430,18 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
7206
7430
|
const history = await loadHistory(api, threadId, historySeedThreadId);
|
|
7207
7431
|
tui.setHistory(history);
|
|
7208
7432
|
tui.onSubmit = (text, images) => {
|
|
7209
|
-
|
|
7433
|
+
const trimmed = text.trimStart();
|
|
7434
|
+
if (trimmed.startsWith("!") && !trimmed.startsWith("!!")) {
|
|
7435
|
+
const command = trimmed.slice(1).trim();
|
|
7436
|
+
if (command) {
|
|
7437
|
+
appendHistory(api, threadId, history, text);
|
|
7438
|
+
void runBangCommand(command);
|
|
7439
|
+
}
|
|
7440
|
+
return;
|
|
7441
|
+
}
|
|
7442
|
+
const outgoing = trimmed.startsWith("!!") ? text.replace("!!", "!") : text;
|
|
7443
|
+
appendHistory(api, threadId, history, outgoing);
|
|
7444
|
+
text = outgoing;
|
|
7210
7445
|
if (editingQueued) {
|
|
7211
7446
|
editingQueued = false;
|
|
7212
7447
|
queued.push({ text, images });
|
|
@@ -7339,6 +7574,15 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
7339
7574
|
void offerUpgrade({ auto: true });
|
|
7340
7575
|
continue;
|
|
7341
7576
|
}
|
|
7577
|
+
if (m.metadata?.user_command) {
|
|
7578
|
+
printCommandBlock(
|
|
7579
|
+
tui,
|
|
7580
|
+
String(m.metadata.command ?? ""),
|
|
7581
|
+
String(m.metadata.output ?? text),
|
|
7582
|
+
m.metadata.ok !== false
|
|
7583
|
+
);
|
|
7584
|
+
continue;
|
|
7585
|
+
}
|
|
7342
7586
|
if (m.role === "assistant" && text) printAssistant(tui, text);
|
|
7343
7587
|
else if (m.role === "system" && text) tui.print(`${c3.dim}${text}${c3.reset}`);
|
|
7344
7588
|
else if (m.role === "user" && text) {
|
|
@@ -7492,6 +7736,120 @@ async function runLevelMenu(tui, perm) {
|
|
|
7492
7736
|
perm.level = picked;
|
|
7493
7737
|
}
|
|
7494
7738
|
}
|
|
7739
|
+
async function runMachinesMenu(tui, api, self) {
|
|
7740
|
+
let machines;
|
|
7741
|
+
try {
|
|
7742
|
+
machines = await loadMachines(api);
|
|
7743
|
+
} catch (e) {
|
|
7744
|
+
tui.print(`${c3.red}\u2717 couldn't load machines:${c3.reset} ${c3.gray}${e instanceof Error ? e.message : String(e)}${c3.reset}`);
|
|
7745
|
+
return;
|
|
7746
|
+
}
|
|
7747
|
+
if (!machines.length) {
|
|
7748
|
+
tui.print(`${c3.gray}No machines registered yet. Run standardcode on a machine (or install its daemon) to register it.${c3.reset}`);
|
|
7749
|
+
return;
|
|
7750
|
+
}
|
|
7751
|
+
machines.sort((a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0));
|
|
7752
|
+
const picked = await tui.select(
|
|
7753
|
+
`${c3.bold}Your machines${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 esc)${c3.reset}`,
|
|
7754
|
+
machines.map((m) => {
|
|
7755
|
+
const isSelf = m.id === self.machine_id;
|
|
7756
|
+
const online = daemonOnline(m);
|
|
7757
|
+
const daemonBit = m.daemon ? online ? "daemon online" : "daemon offline" : "no daemon";
|
|
7758
|
+
const nproj = Object.keys(m.projects).length;
|
|
7759
|
+
return {
|
|
7760
|
+
label: `${m.name}${isSelf ? " (this machine)" : ""}`,
|
|
7761
|
+
hint: `${m.hostname} \xB7 ${m.platform}/${m.arch} \xB7 v${m.version ?? "?"} \xB7 ${daemonBit} \xB7 ${nproj} project${nproj === 1 ? "" : "s"}`,
|
|
7762
|
+
value: m.id
|
|
7763
|
+
};
|
|
7764
|
+
})
|
|
7765
|
+
);
|
|
7766
|
+
if (!picked) return;
|
|
7767
|
+
const machine = machines.find((m) => m.id === picked);
|
|
7768
|
+
await manageMachine(tui, api, self, machine);
|
|
7769
|
+
}
|
|
7770
|
+
async function manageMachine(tui, api, self, machine) {
|
|
7771
|
+
const isSelf = machine.id === self.machine_id;
|
|
7772
|
+
const online = daemonOnline(machine);
|
|
7773
|
+
const canRunCommands = isSelf || !!machine.daemon;
|
|
7774
|
+
const options = [
|
|
7775
|
+
{ label: "Rename", value: "rename" }
|
|
7776
|
+
];
|
|
7777
|
+
if (canRunCommands) {
|
|
7778
|
+
options.push(
|
|
7779
|
+
{ label: "Update standardcode", hint: `v${machine.version ?? "?"} \u2192 latest`, value: "update" },
|
|
7780
|
+
{ label: "Manage projects", hint: `${Object.keys(machine.projects).length}`, value: "projects" }
|
|
7781
|
+
);
|
|
7782
|
+
}
|
|
7783
|
+
options.push({ label: "Back", value: "back" });
|
|
7784
|
+
if (!isSelf && !machine.daemon) {
|
|
7785
|
+
tui.print(
|
|
7786
|
+
`${c3.dim}${machine.name} has no daemon \u2014 you can rename it here; update and project changes need its daemon installed.${c3.reset}`
|
|
7787
|
+
);
|
|
7788
|
+
} else if (!isSelf && machine.daemon && !online) {
|
|
7789
|
+
tui.print(
|
|
7790
|
+
`${c3.dim}${machine.name}'s daemon is offline \u2014 queued changes apply when it next comes online.${c3.reset}`
|
|
7791
|
+
);
|
|
7792
|
+
}
|
|
7793
|
+
const action = await tui.select(`${c3.bold}${machine.name}${c3.reset}`, options);
|
|
7794
|
+
if (!action || action === "back") return;
|
|
7795
|
+
const dispatch = async (kind, args) => {
|
|
7796
|
+
if (isSelf) {
|
|
7797
|
+
await applyMachineCommand(api, self, { kind, args});
|
|
7798
|
+
} else {
|
|
7799
|
+
await enqueueMachineCommand(api, machine.id, kind, args);
|
|
7800
|
+
}
|
|
7801
|
+
};
|
|
7802
|
+
const applyNote = isSelf ? "applied" : `queued \u2014 ${machine.name}'s daemon will apply it within ~10s`;
|
|
7803
|
+
if (action === "rename") {
|
|
7804
|
+
const name = await tui.prompt(`New name for ${machine.name}`, machine.name);
|
|
7805
|
+
if (name === null || !name.trim()) return;
|
|
7806
|
+
await setMachineName(api, machine.id, name.trim());
|
|
7807
|
+
tui.print(`${c3.green}\u2713${c3.reset} Renamed ${c3.bold}${machine.name}${c3.reset} \u2192 ${c3.bold}${name.trim()}${c3.reset}.`);
|
|
7808
|
+
} else if (action === "update") {
|
|
7809
|
+
if (isSelf) {
|
|
7810
|
+
await runUpdateCommand(tui);
|
|
7811
|
+
} else {
|
|
7812
|
+
const go = await tui.select(`Force ${machine.name} to update standardcode now?`, [
|
|
7813
|
+
{ label: "Yes, update & restart its daemon", value: "yes" },
|
|
7814
|
+
{ label: "Cancel", value: "no" }
|
|
7815
|
+
]);
|
|
7816
|
+
if (go !== "yes") return;
|
|
7817
|
+
await dispatch("update");
|
|
7818
|
+
tui.print(`${c3.green}\u2713${c3.reset} Update ${c3.gray}${applyNote} (its daemon updates and restarts on the new version).${c3.reset}`);
|
|
7819
|
+
}
|
|
7820
|
+
} else if (action === "projects") {
|
|
7821
|
+
await manageMachineProjects(tui, api, self, machine, dispatch, applyNote);
|
|
7822
|
+
}
|
|
7823
|
+
}
|
|
7824
|
+
async function manageMachineProjects(tui, api, self, machine, dispatch, applyNote) {
|
|
7825
|
+
const ADD = "__add__";
|
|
7826
|
+
const paths = Object.keys(machine.projects).sort();
|
|
7827
|
+
const picked = await tui.select(
|
|
7828
|
+
`${c3.bold}Projects on ${machine.name}${c3.reset} ${c3.dim}(enter to remove \xB7 esc)${c3.reset}`,
|
|
7829
|
+
[
|
|
7830
|
+
...paths.map((p) => ({ label: p, hint: "enter to remove", value: p })),
|
|
7831
|
+
{ label: "\uFF0B Add a project directory\u2026", hint: "absolute path", value: ADD }
|
|
7832
|
+
]
|
|
7833
|
+
);
|
|
7834
|
+
if (!picked) return;
|
|
7835
|
+
if (picked === ADD) {
|
|
7836
|
+
const path12 = await tui.prompt(
|
|
7837
|
+
`Absolute project path on ${machine.name}`,
|
|
7838
|
+
machine.id === self.machine_id ? process.cwd() : "/home/you/project"
|
|
7839
|
+
);
|
|
7840
|
+
if (!path12 || !path12.trim()) return;
|
|
7841
|
+
const trimmed = path12.trim();
|
|
7842
|
+
if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
|
|
7843
|
+
tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
|
|
7844
|
+
return;
|
|
7845
|
+
}
|
|
7846
|
+
await dispatch("add_project", { path: trimmed });
|
|
7847
|
+
tui.print(`${c3.green}\u2713${c3.reset} Add ${trimmed} ${c3.gray}${applyNote}.${c3.reset}`);
|
|
7848
|
+
} else {
|
|
7849
|
+
await dispatch("remove_project", { path: picked });
|
|
7850
|
+
tui.print(`${c3.green}\u2713${c3.reset} Remove ${picked} ${c3.gray}${applyNote}.${c3.reset}`);
|
|
7851
|
+
}
|
|
7852
|
+
}
|
|
7495
7853
|
function showDaemonInfo(tui, session) {
|
|
7496
7854
|
if (session.mode === "remote" && session.runner) {
|
|
7497
7855
|
tui.print(
|
|
@@ -7519,6 +7877,7 @@ function showKeybindings(tui) {
|
|
|
7519
7877
|
tui.print(`${c3.gray}shortcuts:${c3.reset}`);
|
|
7520
7878
|
tui.print(`${c3.gray} shift-tab${c3.reset} cycle auto-accept level (1\u20135)`);
|
|
7521
7879
|
tui.print(`${c3.gray} /${c3.reset} open the command palette (type to filter)`);
|
|
7880
|
+
tui.print(`${c3.gray} !cmd${c3.reset} run a shell command on the session's machine (e.g. !ls); !! to send a literal !`);
|
|
7522
7881
|
tui.print(`${c3.gray} ctrl-v${c3.reset} paste an image from the clipboard ([#Image 1])`);
|
|
7523
7882
|
tui.print(`${c3.gray} \u2191 / \u2193${c3.reset} cycle past messages (on the input's top line)`);
|
|
7524
7883
|
tui.print(`${c3.gray} \u2190${c3.reset} from the start of the input: select the [\u2699 n bg] badge (enter opens it)`);
|