@standardagents/code 0.9.1 → 0.9.2

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,11 +5054,11 @@ 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
5063
  return parsed;
5049
5064
  }
@@ -5057,8 +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 });
5075
+ fs4.mkdirSync(dir(), { recursive: true });
5076
+ fs4.writeFileSync(file(), JSON.stringify(identity, null, 2), { mode: 384 });
5062
5077
  }
5063
5078
  function setMachineName(name) {
5064
5079
  const identity = loadMachineIdentity();
@@ -5080,10 +5095,14 @@ function machineIdFromDaemonClientId(clientId) {
5080
5095
  return clientId.startsWith("daemon:") ? clientId.slice("daemon:".length) : null;
5081
5096
  }
5082
5097
  var KEY_PREFIX = "standardcode.machine.";
5098
+ var CMD_SUFFIX = ".cmd";
5083
5099
  var DAEMON_ONLINE_WINDOW_MS = 90 * 1e3;
5084
5100
  function machineKey(machineId) {
5085
5101
  return `${KEY_PREFIX}${machineId}`;
5086
5102
  }
5103
+ function commandKey(machineId) {
5104
+ return `${KEY_PREFIX}${machineId}${CMD_SUFFIX}`;
5105
+ }
5087
5106
  function parseMachineRecord(value) {
5088
5107
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
5089
5108
  const r = value;
@@ -5094,6 +5113,7 @@ function parseMachineRecord(value) {
5094
5113
  hostname: typeof r.hostname === "string" ? r.hostname : "",
5095
5114
  platform: typeof r.platform === "string" ? r.platform : "",
5096
5115
  arch: typeof r.arch === "string" ? r.arch : "",
5116
+ version: typeof r.version === "string" ? r.version : void 0,
5097
5117
  daemon: r.daemon && typeof r.daemon === "object" && !Array.isArray(r.daemon) ? r.daemon : null,
5098
5118
  projects: r.projects && typeof r.projects === "object" && !Array.isArray(r.projects) ? r.projects : {},
5099
5119
  created_at: typeof r.created_at === "number" ? r.created_at : 0,
@@ -5118,6 +5138,7 @@ function newRecord(identity) {
5118
5138
  hostname: os9.hostname(),
5119
5139
  platform: process.platform,
5120
5140
  arch: process.arch,
5141
+ version: readVersion() || void 0,
5121
5142
  daemon: null,
5122
5143
  projects: {},
5123
5144
  created_at: now,
@@ -5127,10 +5148,11 @@ function newRecord(identity) {
5127
5148
  async function updateOwnMachineRecord(api, identity, mutate) {
5128
5149
  const existing = await loadMachine(api, identity.machine_id);
5129
5150
  const record = existing ?? newRecord(identity);
5130
- record.name = machineDisplayName(identity);
5151
+ record.name = machineDisplayName(loadMachineIdentity());
5131
5152
  record.hostname = os9.hostname();
5132
5153
  record.platform = process.platform;
5133
5154
  record.arch = process.arch;
5155
+ record.version = readVersion() || record.version;
5134
5156
  mutate?.(record);
5135
5157
  record.updated_at = Date.now();
5136
5158
  await api.userKvSet(machineKey(identity.machine_id), record);
@@ -5179,6 +5201,63 @@ async function clearDaemon(api, identity) {
5179
5201
  record.daemon = null;
5180
5202
  });
5181
5203
  }
5204
+ function parseCommands(value) {
5205
+ if (!Array.isArray(value)) return [];
5206
+ return value.filter(
5207
+ (c4) => !!c4 && typeof c4 === "object" && typeof c4.id === "string" && typeof c4.kind === "string"
5208
+ );
5209
+ }
5210
+ async function enqueueMachineCommand(api, machineId, kind, args) {
5211
+ const key = commandKey(machineId);
5212
+ const queue = parseCommands(await api.userKvGet(key));
5213
+ const cmd = {
5214
+ id: crypto.randomBytes(6).toString("hex"),
5215
+ kind,
5216
+ args,
5217
+ requested_at: Date.now()
5218
+ };
5219
+ queue.push(cmd);
5220
+ await api.userKvSet(key, queue);
5221
+ return cmd.id;
5222
+ }
5223
+ async function readMachineCommands(api, machineId) {
5224
+ return parseCommands(await api.userKvGet(commandKey(machineId)));
5225
+ }
5226
+ async function clearMachineCommands(api, machineId, appliedIds) {
5227
+ if (appliedIds.length === 0) return;
5228
+ const key = commandKey(machineId);
5229
+ const remaining = parseCommands(await api.userKvGet(key)).filter(
5230
+ (c4) => !appliedIds.includes(c4.id)
5231
+ );
5232
+ await api.userKvSet(key, remaining.length ? remaining : null);
5233
+ }
5234
+ async function applyMachineCommand(api, identity, cmd) {
5235
+ switch (cmd.kind) {
5236
+ case "rename": {
5237
+ const name = typeof cmd.args?.name === "string" ? cmd.args.name.trim() : "";
5238
+ if (!name) return "rename: ignored (empty name)";
5239
+ setMachineName(name);
5240
+ await updateOwnMachineRecord(api, identity);
5241
+ return `renamed to "${name}"`;
5242
+ }
5243
+ case "add_project": {
5244
+ const path12 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
5245
+ if (!path12) return "add_project: ignored (no path)";
5246
+ await registerProject(api, identity, path12);
5247
+ return `added project ${path12}`;
5248
+ }
5249
+ case "remove_project": {
5250
+ const path12 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
5251
+ if (!path12) return "remove_project: ignored (no path)";
5252
+ await unregisterProject(api, identity, path12);
5253
+ return `removed project ${path12}`;
5254
+ }
5255
+ case "update":
5256
+ return "update requested";
5257
+ default:
5258
+ return `unknown command ${cmd.kind}`;
5259
+ }
5260
+ }
5182
5261
 
5183
5262
  // src/relay.ts
5184
5263
  var APPROVAL_REQUEST_KEY = "approval_request";
@@ -5257,31 +5336,31 @@ function readCache() {
5257
5336
  }
5258
5337
  function writeCache(latest) {
5259
5338
  try {
5260
- const dir = cacheDir();
5261
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
5339
+ const dir2 = cacheDir();
5340
+ if (!fs4.existsSync(dir2)) fs4.mkdirSync(dir2, { recursive: true });
5262
5341
  fs4.writeFileSync(cachePath(), JSON.stringify({ latest, timestamp: Date.now() }));
5263
5342
  } catch {
5264
5343
  }
5265
5344
  }
5266
- function readAutoUpdateState(dir = cacheDir()) {
5345
+ function readAutoUpdateState(dir2 = cacheDir()) {
5267
5346
  try {
5268
- const raw = fs4.readFileSync(path3.join(dir, STATE_FILE), "utf-8");
5347
+ const raw = fs4.readFileSync(path3.join(dir2, STATE_FILE), "utf-8");
5269
5348
  const state = JSON.parse(raw);
5270
5349
  return typeof state?.version === "string" ? state : null;
5271
5350
  } catch {
5272
5351
  return null;
5273
5352
  }
5274
5353
  }
5275
- function writeAutoUpdateState(state, dir = cacheDir()) {
5354
+ function writeAutoUpdateState(state, dir2 = cacheDir()) {
5276
5355
  try {
5277
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
5278
- fs4.writeFileSync(path3.join(dir, STATE_FILE), JSON.stringify(state));
5356
+ if (!fs4.existsSync(dir2)) fs4.mkdirSync(dir2, { recursive: true });
5357
+ fs4.writeFileSync(path3.join(dir2, STATE_FILE), JSON.stringify(state));
5279
5358
  } catch {
5280
5359
  }
5281
5360
  }
5282
- function clearAutoUpdateState(dir = cacheDir()) {
5361
+ function clearAutoUpdateState(dir2 = cacheDir()) {
5283
5362
  try {
5284
- fs4.unlinkSync(path3.join(dir, STATE_FILE));
5363
+ fs4.unlinkSync(path3.join(dir2, STATE_FILE));
5285
5364
  } catch {
5286
5365
  }
5287
5366
  }
@@ -5321,10 +5400,10 @@ function decideAutoUpdate(info, opts) {
5321
5400
  }
5322
5401
  return "start";
5323
5402
  }
5324
- function startBackgroundUpdate(latest, pm, dir = cacheDir()) {
5403
+ function startBackgroundUpdate(latest, pm, dir2 = cacheDir()) {
5325
5404
  const startedAt = Date.now();
5326
- writeAutoUpdateState({ version: latest, startedAt, exitCode: null }, dir);
5327
- const stateFile = path3.join(dir, STATE_FILE);
5405
+ writeAutoUpdateState({ version: latest, startedAt, exitCode: null }, dir2);
5406
+ const stateFile = path3.join(dir2, STATE_FILE);
5328
5407
  const { cmd, args } = updateCommand(pm);
5329
5408
  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
5409
  try {
@@ -5332,14 +5411,14 @@ function startBackgroundUpdate(latest, pm, dir = cacheDir()) {
5332
5411
  child.unref();
5333
5412
  return true;
5334
5413
  } catch {
5335
- writeAutoUpdateState({ version: latest, startedAt, exitCode: -1, finishedAt: Date.now() }, dir);
5414
+ writeAutoUpdateState({ version: latest, startedAt, exitCode: -1, finishedAt: Date.now() }, dir2);
5336
5415
  return false;
5337
5416
  }
5338
5417
  }
5339
- function consumeAppliedUpdate(currentVersion, dir = cacheDir()) {
5340
- const state = readAutoUpdateState(dir);
5418
+ function consumeAppliedUpdate(currentVersion, dir2 = cacheDir()) {
5419
+ const state = readAutoUpdateState(dir2);
5341
5420
  if (!state || state.version !== currentVersion) return null;
5342
- clearAutoUpdateState(dir);
5421
+ clearAutoUpdateState(dir2);
5343
5422
  return state.exitCode === 0 ? state : null;
5344
5423
  }
5345
5424
  async function fetchLatest(currentVersion) {
@@ -5412,6 +5491,7 @@ function runUpdate(pm) {
5412
5491
 
5413
5492
  // src/daemon.ts
5414
5493
  var HEARTBEAT_MS = 3e4;
5494
+ var COMMAND_POLL_MS = 8e3;
5415
5495
  var RECLAIM_PROBE_MS = 2 * 6e4;
5416
5496
  var UPDATE_CHECK_MS = 6 * 60 * 6e4;
5417
5497
  var SWEEP_MS = 10 * 6e4;
@@ -5674,6 +5754,55 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5674
5754
  };
5675
5755
  void checkUpdates();
5676
5756
  const updateTimer = setInterval(() => void checkUpdates(), UPDATE_CHECK_MS);
5757
+ let draining = false;
5758
+ const drainCommands = async () => {
5759
+ if (draining) return;
5760
+ draining = true;
5761
+ try {
5762
+ const cmds = await readMachineCommands(api, identity.machine_id);
5763
+ if (cmds.length === 0) return;
5764
+ const applied2 = [];
5765
+ let forcedUpdate = false;
5766
+ for (const cmd of cmds) {
5767
+ try {
5768
+ if (cmd.kind === "update") {
5769
+ forcedUpdate = true;
5770
+ applied2.push(cmd.id);
5771
+ continue;
5772
+ }
5773
+ const result = await applyMachineCommand(api, identity, cmd);
5774
+ daemonLog(`command ${cmd.kind}: ${result}`);
5775
+ applied2.push(cmd.id);
5776
+ } catch (e) {
5777
+ daemonLog(`command ${cmd.kind} failed: ${e instanceof Error ? e.message : String(e)}`);
5778
+ applied2.push(cmd.id);
5779
+ }
5780
+ }
5781
+ await clearMachineCommands(api, identity.machine_id, applied2).catch(() => {
5782
+ });
5783
+ if (forcedUpdate) {
5784
+ const pm = detectPackageManager();
5785
+ if (!pm) {
5786
+ daemonLog("forced update requested but this is not an installed build \u2014 ignoring");
5787
+ } else if (![...workers.values()].some((w) => w.busy)) {
5788
+ daemonLog(`forced update: running ${pm} install now`);
5789
+ const { ok, output: output4 } = await runUpdate(pm);
5790
+ daemonLog(`forced update ${ok ? "succeeded" : "failed"}: ${output4.trim().split("\n").slice(-2).join(" | ")}`);
5791
+ if (ok) {
5792
+ daemonLog("restarting to apply the forced update");
5793
+ shutdown(0);
5794
+ }
5795
+ } else {
5796
+ daemonLog("forced update deferred \u2014 a session is busy; will retry");
5797
+ void checkUpdates();
5798
+ }
5799
+ }
5800
+ } finally {
5801
+ draining = false;
5802
+ }
5803
+ };
5804
+ void drainCommands();
5805
+ const commandTimer = setInterval(() => void drainCommands(), COMMAND_POLL_MS);
5677
5806
  const sweeper = setInterval(() => {
5678
5807
  if (updateReady && ![...workers.values()].some((w) => w.busy)) {
5679
5808
  daemonLog("restarting to apply the installed update");
@@ -5686,6 +5815,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5686
5815
  clearInterval(heartbeat);
5687
5816
  clearInterval(reclaim);
5688
5817
  clearInterval(updateTimer);
5818
+ clearInterval(commandTimer);
5689
5819
  clearInterval(sweeper);
5690
5820
  for (const worker of workers.values()) worker.stop();
5691
5821
  events.close();
@@ -5705,15 +5835,15 @@ function resolveDaemonCommand(extraArgs = []) {
5705
5835
  if (!entry) throw new Error("Cannot determine how this CLI was launched.");
5706
5836
  const argv = [process.execPath];
5707
5837
  if (entry.endsWith(".ts")) {
5708
- let dir = path3.dirname(entry);
5838
+ let dir2 = path3.dirname(entry);
5709
5839
  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");
5840
+ for (let i = 0; i < 6 && dir2 !== path3.dirname(dir2); i++) {
5841
+ const candidate = path3.join(dir2, "node_modules", "tsx", "dist", "cli.mjs");
5712
5842
  if (fs4.existsSync(candidate)) {
5713
5843
  tsx = candidate;
5714
5844
  break;
5715
5845
  }
5716
- dir = path3.dirname(dir);
5846
+ dir2 = path3.dirname(dir2);
5717
5847
  }
5718
5848
  if (!tsx) {
5719
5849
  throw new Error(
@@ -6048,9 +6178,9 @@ async function projectCommand(action, target) {
6048
6178
  `);
6049
6179
  process.exit(1);
6050
6180
  }
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.
6181
+ const dir2 = path3.resolve(target);
6182
+ if (action === "add" && !fs4.existsSync(dir2)) {
6183
+ stdout.write(`${c2.red}\u2717${c2.reset} ${dir2} does not exist on this machine.
6054
6184
  `);
6055
6185
  process.exit(1);
6056
6186
  }
@@ -6058,12 +6188,12 @@ async function projectCommand(action, target) {
6058
6188
  const api = await ensureSignedIn(endpoint);
6059
6189
  const identity = loadMachineIdentity();
6060
6190
  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)}.
6191
+ await registerProject(api, identity, dir2);
6192
+ stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir2} for remote sessions on ${machineDisplayName(identity)}.
6063
6193
  `);
6064
6194
  } else {
6065
- await unregisterProject(api, identity, dir);
6066
- stdout.write(`${c2.green}\u2713${c2.reset} Removed ${dir} from this machine's projects.
6195
+ await unregisterProject(api, identity, dir2);
6196
+ stdout.write(`${c2.green}\u2713${c2.reset} Removed ${dir2} from this machine's projects.
6067
6197
  `);
6068
6198
  }
6069
6199
  }
@@ -6195,6 +6325,20 @@ function parseArgs2(args) {
6195
6325
  }
6196
6326
  return parsed;
6197
6327
  }
6328
+ function printCommandBlock(tui, command, output4, ok, where) {
6329
+ tui.print("");
6330
+ const note = where ? ` ${c3.dim}(ran on ${where})${c3.reset}` : "";
6331
+ tui.print(`${c3.magenta}!${c3.reset} ${c3.bold}${command}${c3.reset}${note}`);
6332
+ const body = (output4 ?? "").replace(/\s+$/, "");
6333
+ if (body) {
6334
+ for (const line of body.split("\n")) {
6335
+ tui.print(` ${ok ? c3.dim : c3.red}${line}${c3.reset}`);
6336
+ }
6337
+ } else {
6338
+ tui.print(` ${c3.dim}(no output)${c3.reset}`);
6339
+ }
6340
+ tui.print("");
6341
+ }
6198
6342
  function printAssistant(tui, text) {
6199
6343
  const cols2 = Math.max(20, (process.stdout.columns || 80) - 3);
6200
6344
  tui.clearStream();
@@ -6243,7 +6387,7 @@ ${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.rese
6243
6387
  }
6244
6388
  function printWelcome(endpoint, projectDir) {
6245
6389
  const home = os9.homedir();
6246
- const dir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
6390
+ const dir2 = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
6247
6391
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
6248
6392
  const version = readVersion();
6249
6393
  const pad = " ";
@@ -6251,7 +6395,7 @@ function printWelcome(endpoint, projectDir) {
6251
6395
  `${c3.bold}${gradientText("Standard Code")}${c3.reset}${version ? ` ${c3.dim}v${version}${c3.reset}` : ""}`,
6252
6396
  `${c3.dim}terminal coding agent${c3.reset}`,
6253
6397
  ...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c3.teal}${host}${c3.reset}`],
6254
- `${c3.dim}${dir}${c3.reset}`
6398
+ `${c3.dim}${dir2}${c3.reset}`
6255
6399
  ];
6256
6400
  const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
6257
6401
  const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
@@ -6598,10 +6742,10 @@ async function pickRemoteProject(tui, runner) {
6598
6742
  const projects = Object.entries(runner.projects).sort(
6599
6743
  (a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
6600
6744
  );
6601
- const items = projects.map(([dir, p]) => ({
6602
- label: shortenPath(dir, 48),
6745
+ const items = projects.map(([dir2, p]) => ({
6746
+ label: shortenPath(dir2, 48),
6603
6747
  hint: p?.last_used_at ? relativeTime(p.last_used_at / 1e3) : "",
6604
- value: dir
6748
+ value: dir2
6605
6749
  }));
6606
6750
  items.push({ label: `\uFF0B Another path on ${runner.name}\u2026`, hint: "type a directory", value: ENTER_PATH });
6607
6751
  const picked = await tui.select(
@@ -6687,12 +6831,23 @@ async function printHistory(api, threadId, tui) {
6687
6831
  } catch {
6688
6832
  return;
6689
6833
  }
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));
6834
+ const convo = msgs.filter(
6835
+ (m) => m.metadata?.user_command || m.role === "user" || m.role === "assistant" && messageText(m.content).trim()
6836
+ ).sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
6691
6837
  if (!convo.length) return;
6692
6838
  const shown = convo.slice(-24);
6693
6839
  tui.print(`${c3.dim}\u2500\u2500 resuming session \xB7 ${convo.length} message${convo.length === 1 ? "" : "s"} \u2500\u2500${c3.reset}`);
6694
6840
  if (shown.length < convo.length) tui.print(`${c3.dim} \u2026 earlier messages omitted${c3.reset}`);
6695
6841
  for (const m of shown) {
6842
+ if (m.metadata?.user_command) {
6843
+ printCommandBlock(
6844
+ tui,
6845
+ String(m.metadata.command ?? ""),
6846
+ String(m.metadata.output ?? messageText(m.content)),
6847
+ m.metadata.ok !== false
6848
+ );
6849
+ continue;
6850
+ }
6696
6851
  const text = messageText(m.content).trim();
6697
6852
  if (!text) continue;
6698
6853
  if (m.role === "user") tui.printUserMessage(text);
@@ -7009,6 +7164,25 @@ why: ${req.requestPermission}` : ""}`,
7009
7164
  busy = true;
7010
7165
  tui.setWorking(true);
7011
7166
  };
7167
+ const whereLabel = remote ? runnerName : "this machine";
7168
+ let bangRunning = false;
7169
+ const runBangCommand = async (command) => {
7170
+ if (bangRunning) {
7171
+ tui.print(`${c3.dim}a command is already running \u2014 one at a time.${c3.reset}`);
7172
+ return;
7173
+ }
7174
+ bangRunning = true;
7175
+ tui.print(`${c3.magenta}!${c3.reset} ${c3.dim}running on ${whereLabel}\u2026${c3.reset}`);
7176
+ try {
7177
+ const res = await api.runCommand(threadId, command);
7178
+ if (res.messageId) shownIds.add(res.messageId);
7179
+ printCommandBlock(tui, command, res.ok ? res.output ?? "" : res.error ?? "command failed", res.ok, whereLabel);
7180
+ } catch (e) {
7181
+ printCommandBlock(tui, command, e instanceof Error ? e.message : String(e), false, whereLabel);
7182
+ } finally {
7183
+ bangRunning = false;
7184
+ }
7185
+ };
7012
7186
  const flushQueued = async () => {
7013
7187
  if (!queued.length) return;
7014
7188
  const toSend = queued.splice(0);
@@ -7173,6 +7347,12 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7173
7347
  run: () => runMcpMenu(tui, mcpCtl)
7174
7348
  }
7175
7349
  ],
7350
+ {
7351
+ name: "machines",
7352
+ label: "Your machines",
7353
+ hint: "list, rename, update, manage projects",
7354
+ run: () => runMachinesMenu(tui, api, session.identity)
7355
+ },
7176
7356
  {
7177
7357
  name: "daemon",
7178
7358
  label: "Machine daemon",
@@ -7206,7 +7386,18 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7206
7386
  const history = await loadHistory(api, threadId, historySeedThreadId);
7207
7387
  tui.setHistory(history);
7208
7388
  tui.onSubmit = (text, images) => {
7209
- appendHistory(api, threadId, history, text);
7389
+ const trimmed = text.trimStart();
7390
+ if (trimmed.startsWith("!") && !trimmed.startsWith("!!")) {
7391
+ const command = trimmed.slice(1).trim();
7392
+ if (command) {
7393
+ appendHistory(api, threadId, history, text);
7394
+ void runBangCommand(command);
7395
+ }
7396
+ return;
7397
+ }
7398
+ const outgoing = trimmed.startsWith("!!") ? text.replace("!!", "!") : text;
7399
+ appendHistory(api, threadId, history, outgoing);
7400
+ text = outgoing;
7210
7401
  if (editingQueued) {
7211
7402
  editingQueued = false;
7212
7403
  queued.push({ text, images });
@@ -7339,6 +7530,15 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
7339
7530
  void offerUpgrade({ auto: true });
7340
7531
  continue;
7341
7532
  }
7533
+ if (m.metadata?.user_command) {
7534
+ printCommandBlock(
7535
+ tui,
7536
+ String(m.metadata.command ?? ""),
7537
+ String(m.metadata.output ?? text),
7538
+ m.metadata.ok !== false
7539
+ );
7540
+ continue;
7541
+ }
7342
7542
  if (m.role === "assistant" && text) printAssistant(tui, text);
7343
7543
  else if (m.role === "system" && text) tui.print(`${c3.dim}${text}${c3.reset}`);
7344
7544
  else if (m.role === "user" && text) {
@@ -7492,6 +7692,117 @@ async function runLevelMenu(tui, perm) {
7492
7692
  perm.level = picked;
7493
7693
  }
7494
7694
  }
7695
+ async function runMachinesMenu(tui, api, self) {
7696
+ let machines;
7697
+ try {
7698
+ machines = await loadMachines(api);
7699
+ } catch (e) {
7700
+ tui.print(`${c3.red}\u2717 couldn't load machines:${c3.reset} ${c3.gray}${e instanceof Error ? e.message : String(e)}${c3.reset}`);
7701
+ return;
7702
+ }
7703
+ if (!machines.length) {
7704
+ tui.print(`${c3.gray}No machines registered yet. Run standardcode on a machine (or install its daemon) to register it.${c3.reset}`);
7705
+ return;
7706
+ }
7707
+ machines.sort((a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0));
7708
+ const picked = await tui.select(
7709
+ `${c3.bold}Your machines${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 esc)${c3.reset}`,
7710
+ machines.map((m) => {
7711
+ const isSelf = m.id === self.machine_id;
7712
+ const online = daemonOnline(m);
7713
+ const daemonBit = m.daemon ? online ? "daemon online" : "daemon offline" : "no daemon";
7714
+ const nproj = Object.keys(m.projects).length;
7715
+ return {
7716
+ label: `${m.name}${isSelf ? " (this machine)" : ""}`,
7717
+ hint: `${m.hostname} \xB7 ${m.platform}/${m.arch} \xB7 v${m.version ?? "?"} \xB7 ${daemonBit} \xB7 ${nproj} project${nproj === 1 ? "" : "s"}`,
7718
+ value: m.id
7719
+ };
7720
+ })
7721
+ );
7722
+ if (!picked) return;
7723
+ const machine = machines.find((m) => m.id === picked);
7724
+ await manageMachine(tui, api, self, machine);
7725
+ }
7726
+ async function manageMachine(tui, api, self, machine) {
7727
+ const isSelf = machine.id === self.machine_id;
7728
+ const online = daemonOnline(machine);
7729
+ const remoteReachable = !isSelf && !!machine.daemon;
7730
+ if (!isSelf && !machine.daemon) {
7731
+ tui.print(
7732
+ `${c3.yellow}${machine.name} has no daemon${c3.reset} ${c3.gray}\u2014 it only runs the interactive CLI, so it can't be managed remotely. Manage it from that machine, or install its daemon.${c3.reset}`
7733
+ );
7734
+ return;
7735
+ }
7736
+ if (remoteReachable && !online) {
7737
+ tui.print(
7738
+ `${c3.dim}${machine.name}'s daemon is offline \u2014 queued changes apply when it next comes online.${c3.reset}`
7739
+ );
7740
+ }
7741
+ const action = await tui.select(`${c3.bold}${machine.name}${c3.reset}`, [
7742
+ { label: "Rename", value: "rename" },
7743
+ { label: "Update standardcode", hint: `v${machine.version ?? "?"} \u2192 latest`, value: "update" },
7744
+ { label: "Manage projects", hint: `${Object.keys(machine.projects).length}`, value: "projects" },
7745
+ { label: "Back", value: "back" }
7746
+ ]);
7747
+ if (!action || action === "back") return;
7748
+ const dispatch = async (kind, args) => {
7749
+ if (isSelf) {
7750
+ await applyMachineCommand(api, self, { kind, args});
7751
+ } else {
7752
+ await enqueueMachineCommand(api, machine.id, kind, args);
7753
+ }
7754
+ };
7755
+ const applyNote = isSelf ? "applied" : `queued \u2014 ${machine.name}'s daemon will apply it within ~10s`;
7756
+ if (action === "rename") {
7757
+ const name = await tui.prompt(`New name for ${machine.name}`, machine.name);
7758
+ if (!name || !name.trim()) return;
7759
+ await dispatch("rename", { name: name.trim() });
7760
+ tui.print(`${c3.green}\u2713${c3.reset} Rename to "${name.trim()}" ${c3.gray}${applyNote}.${c3.reset}`);
7761
+ } else if (action === "update") {
7762
+ if (isSelf) {
7763
+ await runUpdateCommand(tui);
7764
+ } else {
7765
+ const go = await tui.select(`Force ${machine.name} to update standardcode now?`, [
7766
+ { label: "Yes, update & restart its daemon", value: "yes" },
7767
+ { label: "Cancel", value: "no" }
7768
+ ]);
7769
+ if (go !== "yes") return;
7770
+ await dispatch("update");
7771
+ tui.print(`${c3.green}\u2713${c3.reset} Update ${c3.gray}${applyNote} (its daemon updates and restarts on the new version).${c3.reset}`);
7772
+ }
7773
+ } else if (action === "projects") {
7774
+ await manageMachineProjects(tui, api, self, machine, dispatch, applyNote);
7775
+ }
7776
+ }
7777
+ async function manageMachineProjects(tui, api, self, machine, dispatch, applyNote) {
7778
+ const ADD = "__add__";
7779
+ const paths = Object.keys(machine.projects).sort();
7780
+ const picked = await tui.select(
7781
+ `${c3.bold}Projects on ${machine.name}${c3.reset} ${c3.dim}(enter to remove \xB7 esc)${c3.reset}`,
7782
+ [
7783
+ ...paths.map((p) => ({ label: p, hint: "enter to remove", value: p })),
7784
+ { label: "\uFF0B Add a project directory\u2026", hint: "absolute path", value: ADD }
7785
+ ]
7786
+ );
7787
+ if (!picked) return;
7788
+ if (picked === ADD) {
7789
+ const path12 = await tui.prompt(
7790
+ `Absolute project path on ${machine.name}`,
7791
+ machine.id === self.machine_id ? process.cwd() : "/home/you/project"
7792
+ );
7793
+ if (!path12 || !path12.trim()) return;
7794
+ const trimmed = path12.trim();
7795
+ if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
7796
+ tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
7797
+ return;
7798
+ }
7799
+ await dispatch("add_project", { path: trimmed });
7800
+ tui.print(`${c3.green}\u2713${c3.reset} Add ${trimmed} ${c3.gray}${applyNote}.${c3.reset}`);
7801
+ } else {
7802
+ await dispatch("remove_project", { path: picked });
7803
+ tui.print(`${c3.green}\u2713${c3.reset} Remove ${picked} ${c3.gray}${applyNote}.${c3.reset}`);
7804
+ }
7805
+ }
7495
7806
  function showDaemonInfo(tui, session) {
7496
7807
  if (session.mode === "remote" && session.runner) {
7497
7808
  tui.print(
@@ -7519,6 +7830,7 @@ function showKeybindings(tui) {
7519
7830
  tui.print(`${c3.gray}shortcuts:${c3.reset}`);
7520
7831
  tui.print(`${c3.gray} shift-tab${c3.reset} cycle auto-accept level (1\u20135)`);
7521
7832
  tui.print(`${c3.gray} /${c3.reset} open the command palette (type to filter)`);
7833
+ tui.print(`${c3.gray} !cmd${c3.reset} run a shell command on the session's machine (e.g. !ls); !! to send a literal !`);
7522
7834
  tui.print(`${c3.gray} ctrl-v${c3.reset} paste an image from the clipboard ([#Image 1])`);
7523
7835
  tui.print(`${c3.gray} \u2191 / \u2193${c3.reset} cycle past messages (on the input's top line)`);
7524
7836
  tui.print(`${c3.gray} \u2190${c3.reset} from the start of the input: select the [\u2699 n bg] badge (enter opens it)`);