@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 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 file = configFile();
1212
- fs4.mkdirSync(path3.dirname(file), { recursive: true });
1213
- fs4.writeFileSync(file, JSON.stringify(cfg, null, 2), { mode: 384 });
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 file = this.resolve(String(args.path || ""));
1353
- const stat = await fsp.stat(file).catch(() => null);
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(file, "utf8");
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 file = this.resolve(String(args.path || ""));
1408
+ const file2 = this.resolve(String(args.path || ""));
1394
1409
  const content = String(args.content ?? "");
1395
- await fsp.mkdir(path3.dirname(file), { recursive: true });
1396
- const existed = fs4.existsSync(file);
1397
- await fsp.writeFile(file, content, "utf8");
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, file)} (${Buffer.byteLength(content)} bytes)`
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 file = this.resolve(String(args.path || ""));
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(file).catch(() => null);
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(file, "utf8");
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(file, updated, "utf8");
1419
- return { ok: true, result: `Edited ${path3.relative(this.projectDir, file)} (${count} replacement${count === 1 ? "" : "s"})` };
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 (dir) => {
1843
- const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => []);
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(dir, e.name);
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 DIR2 = path3.join(os9.homedir(), ".standardagents");
5043
- var FILE2 = path3.join(DIR2, "machine.json");
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(FILE2, "utf8"));
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(DIR2, { recursive: true });
5061
- fs4.writeFileSync(FILE2, JSON.stringify(identity, null, 2), { mode: 384 });
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
- return entries.map((e) => parseMachineRecord(e.value)).filter((m) => m !== null);
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
- return parseMachineRecord(await api.userKvGet(machineKey(machineId)));
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: machineDisplayName(identity),
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 loadMachine(api, identity.machine_id);
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 dir = cacheDir();
5261
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
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(dir = cacheDir()) {
5367
+ function readAutoUpdateState(dir2 = cacheDir()) {
5267
5368
  try {
5268
- const raw = fs4.readFileSync(path3.join(dir, STATE_FILE), "utf-8");
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, dir = cacheDir()) {
5376
+ function writeAutoUpdateState(state, dir2 = cacheDir()) {
5276
5377
  try {
5277
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
5278
- fs4.writeFileSync(path3.join(dir, STATE_FILE), JSON.stringify(state));
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(dir = cacheDir()) {
5383
+ function clearAutoUpdateState(dir2 = cacheDir()) {
5283
5384
  try {
5284
- fs4.unlinkSync(path3.join(dir, STATE_FILE));
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, dir = cacheDir()) {
5425
+ function startBackgroundUpdate(latest, pm, dir2 = cacheDir()) {
5325
5426
  const startedAt = Date.now();
5326
- writeAutoUpdateState({ version: latest, startedAt, exitCode: null }, dir);
5327
- const stateFile = path3.join(dir, STATE_FILE);
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() }, dir);
5436
+ writeAutoUpdateState({ version: latest, startedAt, exitCode: -1, finishedAt: Date.now() }, dir2);
5336
5437
  return false;
5337
5438
  }
5338
5439
  }
5339
- function consumeAppliedUpdate(currentVersion, dir = cacheDir()) {
5340
- const state = readAutoUpdateState(dir);
5440
+ function consumeAppliedUpdate(currentVersion, dir2 = cacheDir()) {
5441
+ const state = readAutoUpdateState(dir2);
5341
5442
  if (!state || state.version !== currentVersion) return null;
5342
- clearAutoUpdateState(dir);
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} (${machineDisplayName(identity)}), endpoint ${endpoint}`
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 dir = path3.dirname(entry);
5867
+ let dir2 = path3.dirname(entry);
5709
5868
  let tsx = null;
5710
- for (let i = 0; i < 6 && dir !== path3.dirname(dir); i++) {
5711
- const candidate = path3.join(dir, "node_modules", "tsx", "dist", "cli.mjs");
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
- dir = path3.dirname(dir);
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
- const named = loadMachineIdentity();
5970
- await updateOwnMachineRecord(api, named);
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, named.machine_id);
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}${machineDisplayName(named)}${c2.reset} is online.
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 dir = path3.resolve(target);
6052
- if (action === "add" && !fs4.existsSync(dir)) {
6053
- stdout.write(`${c2.red}\u2717${c2.reset} ${dir} does not exist on this machine.
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, dir);
6062
- stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir} for remote sessions on ${machineDisplayName(identity)}.
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, dir);
6066
- stdout.write(`${c2.green}\u2713${c2.reset} Removed ${dir} from this machine's projects.
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 dir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
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}${dir}${c3.reset}`
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(([dir, p]) => ({
6602
- label: shortenPath(dir, 48),
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: dir
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((m) => m.role === "user" || m.role === "assistant" && messageText(m.content).trim()).sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
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: `${machineDisplayName(session.identity)} (terminal)`,
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
- appendHistory(api, threadId, history, text);
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)`);