@standardagents/code 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import os6, { homedir } from 'os';
3
- import fs4 from 'fs';
2
+ import os9, { homedir } from 'os';
4
3
  import path3 from 'path';
5
4
  import readline2 from 'readline/promises';
6
- import { spawn, execFile } from 'child_process';
7
5
  import { stdout, stdin } from 'process';
6
+ import fs4 from 'fs';
8
7
  import fsp from 'fs/promises';
9
8
  import crypto from 'crypto';
9
+ import { spawn, execFileSync, spawnSync, execFile } from 'child_process';
10
10
  import readline from 'readline';
11
11
  import { fileURLToPath } from 'url';
12
12
 
@@ -251,8 +251,16 @@ var ApiClient = class {
251
251
  * Retries with backoff — this is the durable delivery path, so it must land
252
252
  * even if the connection is briefly flaky after a permission wait.
253
253
  */
254
- async postToolResult(threadId, toolCallId, ok, result, error) {
255
- const body = JSON.stringify({ tool_call_id: toolCallId, ok, result, error });
254
+ async postToolResult(threadId, toolCallId, ok, result, error, executor) {
255
+ const body = JSON.stringify({
256
+ tool_call_id: toolCallId,
257
+ ok,
258
+ result,
259
+ error,
260
+ // Ownership fence: calls dispatched to a specific execution owner are
261
+ // only accepted back from that owner at that generation.
262
+ ...executor ? { client_id: executor.clientId, generation: executor.generation ?? 0 } : {}
263
+ });
256
264
  for (let attempt = 0; attempt < 6; attempt++) {
257
265
  try {
258
266
  await this.json(`/api/threads/${threadId}/tool-result`, {
@@ -283,6 +291,39 @@ var ApiClient = class {
283
291
  return null;
284
292
  }
285
293
  }
294
+ // ── account-wide user KV (per-user, spans every thread) ───────────────────
295
+ // Backed by the instance's /api/users/me/kv store. This is where the machine
296
+ // registry lives: which machines (laptops / VPS daemons) belong to this
297
+ // account, and which projects each machine has.
298
+ /** Read one value from the signed-in user's account-wide KV (null if absent). */
299
+ async userKvGet(key) {
300
+ try {
301
+ const res = await this.json(
302
+ `/api/users/me/kv?key=${encodeURIComponent(key)}`
303
+ );
304
+ return res?.value ?? null;
305
+ } catch {
306
+ return null;
307
+ }
308
+ }
309
+ /** Write one value to the signed-in user's account-wide KV (null deletes). */
310
+ async userKvSet(key, value) {
311
+ await this.json(`/api/users/me/kv`, {
312
+ method: "POST",
313
+ body: JSON.stringify({ key, value })
314
+ });
315
+ }
316
+ /** List the signed-in user's account-wide KV entries under a key prefix. */
317
+ async userKvList(prefix) {
318
+ try {
319
+ const res = await this.json(
320
+ `/api/users/me/kv?prefix=${encodeURIComponent(prefix)}&limit=200`
321
+ );
322
+ return Array.isArray(res?.entries) ? res.entries : [];
323
+ } catch {
324
+ return [];
325
+ }
326
+ }
286
327
  /** Read a value from the thread's durable KV store (null if absent). */
287
328
  async kvGet(threadId, key) {
288
329
  try {
@@ -654,18 +695,20 @@ function gradientText(text, phase = 0) {
654
695
  // src/bridge.ts
655
696
  var PATH_ARG_TOOLS = /* @__PURE__ */ new Set(["read_file", "grep", "glob", "write_file", "edit_file"]);
656
697
  var Bridge = class {
657
- constructor(api, threadId, host, perm, hooks) {
698
+ constructor(api, threadId, host, perm, hooks, identity) {
658
699
  this.api = api;
659
700
  this.threadId = threadId;
660
701
  this.host = host;
661
702
  this.perm = perm;
662
703
  this.hooks = hooks;
704
+ this.identity = identity;
663
705
  }
664
706
  api;
665
707
  threadId;
666
708
  host;
667
709
  perm;
668
710
  hooks;
711
+ identity;
669
712
  ws = null;
670
713
  closed = false;
671
714
  heartbeat = null;
@@ -675,6 +718,43 @@ var Bridge = class {
675
718
  // Durable forwarded calls we've started handling, so a server re-send (after a
676
719
  // reconnect) doesn't prompt or run them twice.
677
720
  handledDurable = /* @__PURE__ */ new Set();
721
+ // Whether this client currently holds execution ownership. Starts true so an
722
+ // instance that predates the owner concept (no owner field in bridge_ready)
723
+ // behaves exactly as before; an owner-aware instance sets it on every connect.
724
+ owner = true;
725
+ // Ownership generation from the last bridge_ready/owner_changed (echoed in
726
+ // durable deliveries so the server's fence can match).
727
+ ownerGeneration = 0;
728
+ // Resolved once the first owner-aware bridge_ready arrives (or never, on a
729
+ // legacy instance — callers pair this with a timeout).
730
+ ownershipKnownResolve = null;
731
+ ownershipKnownPromise = new Promise((resolve) => {
732
+ this.ownershipKnownResolve = resolve;
733
+ });
734
+ /** Whether this client currently executes forwarded tool calls. */
735
+ get isOwner() {
736
+ return this.owner;
737
+ }
738
+ /** Adjust the ownership claim used on the NEXT (re)connect — e.g. an
739
+ * interactive session that became owner reasserts with `takeover` after a
740
+ * brief drop, instead of silently losing execution to a daemon's
741
+ * stale-claim probe. */
742
+ setClaim(claim) {
743
+ if (this.identity) this.identity.claim = claim;
744
+ }
745
+ /**
746
+ * Resolve once the instance has said whether this client owns execution
747
+ * (the first owner-aware bridge_ready). Legacy instances never say — the
748
+ * timeout resolves to the current (assumed-owner) state so callers can gate
749
+ * owner-only side effects like session_info/mcp_catalog writes.
750
+ */
751
+ async whenOwnershipKnown(timeoutMs = 3e3) {
752
+ await Promise.race([
753
+ this.ownershipKnownPromise,
754
+ new Promise((resolve) => setTimeout(resolve, timeoutMs))
755
+ ]);
756
+ return this.owner;
757
+ }
678
758
  /**
679
759
  * Connect and keep the bridge connected. Resolves on the first successful
680
760
  * open; thereafter any drop is reconnected automatically with exponential
@@ -697,7 +777,13 @@ var Bridge = class {
697
777
  }
698
778
  openSocket() {
699
779
  if (this.closed) return;
700
- const url = `${this.api.wsEndpoint}/api/threads/${this.threadId}/bridge?token=${encodeURIComponent(this.api.bearer)}`;
780
+ let url = `${this.api.wsEndpoint}/api/threads/${this.threadId}/bridge?token=${encodeURIComponent(this.api.bearer)}`;
781
+ if (this.identity) {
782
+ url += `&client_id=${encodeURIComponent(this.identity.clientId)}&client_kind=${encodeURIComponent(this.identity.clientKind)}&claim=${encodeURIComponent(this.identity.claim)}`;
783
+ if (this.identity.clientName) {
784
+ url += `&client_name=${encodeURIComponent(this.identity.clientName)}`;
785
+ }
786
+ }
701
787
  let ws;
702
788
  try {
703
789
  ws = new WebSocket(url);
@@ -718,7 +804,17 @@ var Bridge = class {
718
804
  this.onMessage(String(ev.data));
719
805
  });
720
806
  ws.addEventListener("error", () => this.handleDrop(ws));
721
- ws.addEventListener("close", () => this.handleDrop(ws));
807
+ ws.addEventListener("close", (ev) => {
808
+ if (ev.code === 4001 && this.ws === ws) {
809
+ this.closed = true;
810
+ this.owner = false;
811
+ this.ws = null;
812
+ this.stopHeartbeat();
813
+ this.hooks.onSuperseded?.();
814
+ return;
815
+ }
816
+ this.handleDrop(ws);
817
+ });
722
818
  }
723
819
  handleDrop(ws) {
724
820
  if (this.ws !== ws) return;
@@ -757,6 +853,24 @@ var Bridge = class {
757
853
  }
758
854
  this.ws?.close();
759
855
  }
856
+ /**
857
+ * Force a reconnect now. Ownership claims are evaluated at bridge connect,
858
+ * so a non-owner that wants to re-check (e.g. the daemon probing whether a
859
+ * disconnected interactive owner has gone stale) cycles its socket.
860
+ */
861
+ refresh() {
862
+ if (this.closed) return;
863
+ const ws = this.ws;
864
+ if (ws) {
865
+ try {
866
+ ws.close();
867
+ } catch {
868
+ }
869
+ this.handleDrop(ws);
870
+ } else {
871
+ this.scheduleReconnect();
872
+ }
873
+ }
760
874
  send(payload) {
761
875
  try {
762
876
  this.ws?.send(JSON.stringify(payload));
@@ -770,7 +884,44 @@ var Bridge = class {
770
884
  } catch {
771
885
  return;
772
886
  }
887
+ if (msg.type === "bridge_ready") {
888
+ if (typeof msg.owner === "boolean") {
889
+ this.owner = msg.owner;
890
+ const owner = parseOwnerInfo(msg.current_owner);
891
+ this.ownerGeneration = owner?.generation ?? 0;
892
+ if (typeof msg.claim_refused === "string") {
893
+ this.hooks.onClaimRefused?.(msg.claim_refused);
894
+ }
895
+ this.hooks.onOwnership?.(msg.owner, owner);
896
+ this.ownershipKnownResolve?.();
897
+ }
898
+ return;
899
+ }
900
+ if (msg.type === "owner_changed") {
901
+ const owner = parseOwnerInfo(msg.owner);
902
+ this.owner = !!owner && !!this.identity && owner.client_id === this.identity.clientId;
903
+ this.ownerGeneration = owner?.generation ?? 0;
904
+ this.hooks.onOwnership?.(this.owner, owner);
905
+ return;
906
+ }
907
+ if (msg.type === "superseded") {
908
+ this.closed = true;
909
+ this.owner = false;
910
+ this.stopHeartbeat();
911
+ if (this.reconnectTimer) {
912
+ clearTimeout(this.reconnectTimer);
913
+ this.reconnectTimer = null;
914
+ }
915
+ try {
916
+ this.ws?.close();
917
+ } catch {
918
+ }
919
+ this.ws = null;
920
+ this.hooks.onSuperseded?.();
921
+ return;
922
+ }
773
923
  if (msg.type !== "tool_request") return;
924
+ if (!this.owner) return;
774
925
  const req = msg;
775
926
  await this.handleToolRequest(req);
776
927
  }
@@ -781,7 +932,11 @@ var Bridge = class {
781
932
  */
782
933
  respond(req, ok, result, error) {
783
934
  if (req.durable && req.toolCallId) {
784
- void this.api.postToolResult(this.threadId, req.toolCallId, ok, result, error);
935
+ const executor = this.identity ? {
936
+ clientId: this.identity.clientId,
937
+ generation: typeof req.generation === "number" ? req.generation : this.ownerGeneration
938
+ } : void 0;
939
+ void this.api.postToolResult(this.threadId, req.toolCallId, ok, result, error, executor);
785
940
  } else {
786
941
  this.send({ type: "tool_response", id: req.id, ok, result, error });
787
942
  }
@@ -801,6 +956,8 @@ var Bridge = class {
801
956
  this.respond(req, false, void 0, "Blocked: this command is considered catastrophic and was refused by the client safety guard.");
802
957
  return;
803
958
  }
959
+ await this.hooks.refreshPermissions?.().catch(() => {
960
+ });
804
961
  const permKey = permissionKey(req);
805
962
  const decision = decide(this.perm, permKey, effectiveRisk, !!req.requestPermission);
806
963
  if (decision === "deny") {
@@ -845,6 +1002,17 @@ var Bridge = class {
845
1002
  }
846
1003
  }
847
1004
  };
1005
+ function parseOwnerInfo(value) {
1006
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1007
+ const r = value;
1008
+ if (typeof r.client_id !== "string" || r.client_id.length === 0) return null;
1009
+ return {
1010
+ client_id: r.client_id,
1011
+ client_name: typeof r.client_name === "string" ? r.client_name : null,
1012
+ client_kind: typeof r.client_kind === "string" ? r.client_kind : null,
1013
+ generation: typeof r.generation === "number" ? r.generation : void 0
1014
+ };
1015
+ }
848
1016
  function permissionKey(req) {
849
1017
  if (req.tool !== "mcp") return req.tool;
850
1018
  const a = req.args;
@@ -929,7 +1097,7 @@ function detailSuffix(tool, result) {
929
1097
  const lines = result.split("\n").length;
930
1098
  return ` (${lines} line${lines === 1 ? "" : "s"})`;
931
1099
  }
932
- var LOG_DIR = path3.join(os6.homedir(), ".standardagents", "process-logs");
1100
+ var LOG_DIR = path3.join(os9.homedir(), ".standardagents", "process-logs");
933
1101
  var KEY2 = "bg_processes";
934
1102
  function isAlive(pid) {
935
1103
  try {
@@ -1006,7 +1174,7 @@ var ProcessRegistry = class {
1006
1174
  }
1007
1175
  };
1008
1176
  function configFile() {
1009
- return process.env.STANDARDAGENTS_MCP_CONFIG || path3.join(os6.homedir(), ".standardagents", "mcp.json");
1177
+ return process.env.STANDARDAGENTS_MCP_CONFIG || path3.join(os9.homedir(), ".standardagents", "mcp.json");
1010
1178
  }
1011
1179
  function loadMcpConfig() {
1012
1180
  try {
@@ -1063,12 +1231,12 @@ function serverFromCommand(name, commandLine, env) {
1063
1231
  const [command, ...args] = parts;
1064
1232
  return { name: cleanName, command, args, env, enabled: true };
1065
1233
  }
1066
- function tokenize(input2) {
1234
+ function tokenize(input3) {
1067
1235
  const out = [];
1068
1236
  let cur = "";
1069
1237
  let quote = null;
1070
- for (let i = 0; i < input2.length; i++) {
1071
- const ch = input2[i];
1238
+ for (let i = 0; i < input3.length; i++) {
1239
+ const ch = input3[i];
1072
1240
  if (quote) {
1073
1241
  if (ch === quote) quote = null;
1074
1242
  else cur += ch;
@@ -1308,7 +1476,7 @@ var HostTools = class {
1308
1476
  }
1309
1477
  }
1310
1478
  const hash = crypto.createHash("sha256").update(JSON.stringify(files)).digest("hex").slice(0, 12);
1311
- const skillDir = path3.join(os6.tmpdir(), "standardcode-skills", `${skill}-${hash}`);
1479
+ const skillDir = path3.join(os9.tmpdir(), "standardcode-skills", `${skill}-${hash}`);
1312
1480
  for (const f of files) {
1313
1481
  const dest = path3.resolve(skillDir, f.path);
1314
1482
  if (path3.relative(skillDir, dest).startsWith("..")) {
@@ -2163,7 +2331,7 @@ function fromFile(filePath) {
2163
2331
  }
2164
2332
  }
2165
2333
  async function readDarwin() {
2166
- const tmp = path3.join(os6.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
2334
+ const tmp = path3.join(os9.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
2167
2335
  const script = [
2168
2336
  `set d to the clipboard as \xABclass PNGf\xBB`,
2169
2337
  `set f to open for access POSIX file "${tmp}" with write permission`,
@@ -2200,7 +2368,7 @@ async function readLinux() {
2200
2368
  return null;
2201
2369
  }
2202
2370
  async function readWindows() {
2203
- const tmp = path3.join(os6.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
2371
+ const tmp = path3.join(os9.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
2204
2372
  const ps = [
2205
2373
  "Add-Type -AssemblyName System.Windows.Forms;",
2206
2374
  "$img = [System.Windows.Forms.Clipboard]::GetImage();",
@@ -2274,6 +2442,13 @@ function buildBoxBottom(boxW, borderColor, reset = "\x1B[0m") {
2274
2442
  function buildBoxTopPlain(boxW, borderColor, reset = "\x1B[0m") {
2275
2443
  return `${borderColor}\u256D${"\u2500".repeat(Math.max(1, boxW - 2))}\u256E${reset}`;
2276
2444
  }
2445
+ function buildBoxTopLeftLabel(boxW, label, borderColor, labelColor = "\x1B[90m", reset = "\x1B[0m") {
2446
+ const plain = ` ${label} `;
2447
+ const budget = Math.max(1, boxW - 3);
2448
+ const lab = plain.length > budget ? plain.slice(0, Math.max(1, budget)) : plain;
2449
+ const fill = Math.max(1, boxW - 2 - lab.length);
2450
+ return `${borderColor}\u256D${reset}${labelColor}${lab}${reset}${borderColor}${"\u2500".repeat(fill)}\u256E${reset}`;
2451
+ }
2277
2452
  function buildBoxBody(content, contentW, borderColor, reset = "\x1B[0m") {
2278
2453
  const vw = boxVisibleWidth(content);
2279
2454
  const pad = Math.max(0, contentW - vw);
@@ -2302,17 +2477,104 @@ function wrapInputBodyLines(inputBuffer, prefix, prefixWidth, contentW) {
2302
2477
  if (out.length === 0) out.push(prefix);
2303
2478
  return out;
2304
2479
  }
2480
+ function borderHighlightGrey(intensity) {
2481
+ const t = Math.max(0, Math.min(1, intensity));
2482
+ const e = t * t;
2483
+ const lo = 88;
2484
+ const hi = 236;
2485
+ const v = Math.round(lo + (hi - lo) * e);
2486
+ const ct = process.env.COLORTERM ?? "";
2487
+ const tc = /truecolor|24bit/i.test(ct) || /iterm|kitty|wezterm|ghostty|alacritty/i.test(process.env.TERM_PROGRAM ?? "");
2488
+ if (tc) return `\x1B[38;2;${v};${v};${v}m`;
2489
+ return `\x1B[38;5;${232 + Math.round(23 * e)}m`;
2490
+ }
2491
+ function rotatingBorderCellColor(index, perimeter, phase, trailFrac = 0.2) {
2492
+ const P = Math.max(1, perimeter);
2493
+ const pos = index / P;
2494
+ const dBehind = ((phase - pos) % 1 + 1) % 1;
2495
+ if (dBehind <= trailFrac) {
2496
+ return borderHighlightGrey(1 - dBehind / trailFrac);
2497
+ }
2498
+ return inputBoxBorderColor();
2499
+ }
2500
+ function perimeterIndex(edge, offset, boxW, bodyH) {
2501
+ const W = boxW;
2502
+ const H = Math.max(1, bodyH);
2503
+ if (edge === "top") return Math.max(0, Math.min(W - 1, offset));
2504
+ if (edge === "right") return W + Math.max(0, Math.min(H - 1, offset));
2505
+ if (edge === "bottom") return W + H + Math.max(0, Math.min(W - 1, offset));
2506
+ return 2 * W + H + Math.max(0, Math.min(H - 1, offset));
2507
+ }
2305
2508
  function buildInputBoxRows(opts) {
2306
2509
  const { cols: cols2, inputBuffer, prefix, prefixWidth, level, levelColor } = opts;
2307
2510
  const borderColor = opts.borderColor ?? inputBoxBorderColor();
2308
2511
  const geo = inputBoxGeometry(cols2);
2309
2512
  const body = wrapInputBodyLines(inputBuffer, prefix, prefixWidth, geo.contentW);
2310
- const dots = borderLevelDots(level);
2311
- const dotsStyled = borderLevelDotsStyled(level, levelColor);
2312
- const top = buildBoxTop(geo.boxW, dots, borderColor, levelColor, "\x1B[0m", dotsStyled);
2313
- const bottom = buildBoxBottom(geo.boxW, borderColor);
2314
2513
  const pad = " ".repeat(geo.margin);
2315
- return [pad + top, ...body.map((b) => pad + buildBoxBody(b, geo.contentW, borderColor)), pad + bottom];
2514
+ const phase = opts.borderPhase;
2515
+ if (phase == null || !Number.isFinite(phase)) {
2516
+ const dots2 = borderLevelDots(level);
2517
+ const dotsStyled2 = borderLevelDotsStyled(level, levelColor);
2518
+ const top2 = buildBoxTop(geo.boxW, dots2, borderColor, levelColor, "\x1B[0m", dotsStyled2);
2519
+ const bottom2 = buildBoxBottom(geo.boxW, borderColor);
2520
+ return [pad + top2, ...body.map((b) => pad + buildBoxBody(b, geo.contentW, borderColor)), pad + bottom2];
2521
+ }
2522
+ const W = geo.boxW;
2523
+ const bodyH = Math.max(1, body.length);
2524
+ const P = 2 * W + 2 * bodyH;
2525
+ const reset = "\x1B[0m";
2526
+ const colorAt = (i) => rotatingBorderCellColor(i, P, phase);
2527
+ const dots = borderLevelDots(level);
2528
+ borderLevelDotsStyled(level, levelColor);
2529
+ const labelPlain = ` ${dots} `;
2530
+ const rightFillN = 1;
2531
+ const budget = Math.max(1, W - 2 - rightFillN);
2532
+ const plain = labelPlain.length > budget ? labelPlain.slice(0, Math.max(1, budget)) : labelPlain;
2533
+ const leftFill = Math.max(1, W - 2 - plain.length - rightFillN);
2534
+ let top = "";
2535
+ let xi = 0;
2536
+ top += colorAt(perimeterIndex("top", xi++, W, bodyH)) + "\u256D" + reset;
2537
+ for (let k = 0; k < leftFill; k++) {
2538
+ top += colorAt(perimeterIndex("top", xi++, W, bodyH)) + "\u2500" + reset;
2539
+ }
2540
+ {
2541
+ const plainChars = [...plain];
2542
+ let di = 0;
2543
+ for (const ch of plainChars) {
2544
+ const c4 = colorAt(perimeterIndex("top", xi++, W, bodyH));
2545
+ if (ch === "\u25CF" || ch === "\u25CB") {
2546
+ const levelN = Math.max(1, Math.min(5, level));
2547
+ const filled = di < levelN;
2548
+ di++;
2549
+ top += (filled ? levelColor || c4 : "\x1B[38;5;240m") + (filled ? "\u25CF" : "\u25CB") + reset;
2550
+ } else {
2551
+ top += c4 + ch + reset;
2552
+ }
2553
+ }
2554
+ }
2555
+ for (let k = 0; k < rightFillN; k++) {
2556
+ top += colorAt(perimeterIndex("top", xi++, W, bodyH)) + "\u2500" + reset;
2557
+ }
2558
+ top += colorAt(perimeterIndex("top", Math.min(xi, W - 1), W, bodyH)) + "\u256E" + reset;
2559
+ const bodyRows = [];
2560
+ for (let y = 0; y < bodyH; y++) {
2561
+ const leftC = colorAt(perimeterIndex("left", bodyH - 1 - y, W, bodyH));
2562
+ const rightC = colorAt(perimeterIndex("right", y, W, bodyH));
2563
+ const vw = boxVisibleWidth(body[y]);
2564
+ const sp = Math.max(0, geo.contentW - vw);
2565
+ bodyRows.push(
2566
+ `${pad}${leftC}\u2502${reset} ${body[y]}${" ".repeat(sp)} ${rightC}\u2502${reset}`
2567
+ );
2568
+ }
2569
+ let bottom = "";
2570
+ for (let x = 0; x < W; x++) {
2571
+ const idx = perimeterIndex("bottom", W - 1 - x, W, bodyH);
2572
+ const c4 = colorAt(idx);
2573
+ if (x === 0) bottom += c4 + "\u2570" + reset;
2574
+ else if (x === W - 1) bottom += c4 + "\u256F" + reset;
2575
+ else bottom += c4 + "\u2500" + reset;
2576
+ }
2577
+ return [pad + top, ...bodyRows, pad + bottom];
2316
2578
  }
2317
2579
  var C = {
2318
2580
  reset: "\x1B[0m",
@@ -2367,7 +2629,7 @@ var Tui = class _Tui {
2367
2629
  process.stdin.resume();
2368
2630
  process.stdout.write("\x1B[?2004h");
2369
2631
  process.on("exit", () => process.stdout.write("\x1B[?2004l\x1B[?25h"));
2370
- process.stdout.on("resize", () => this.renderBottom());
2632
+ process.stdout.on("resize", () => this.scheduleResizeRedraw());
2371
2633
  }
2372
2634
  level;
2373
2635
  // input + indicators
@@ -2426,13 +2688,22 @@ var Tui = class _Tui {
2426
2688
  connected = true;
2427
2689
  bottomDrawn = false;
2428
2690
  started = false;
2429
- // Resize bookkeeping: the width the region was last drawn at, and the visible
2430
- // width of every HUD row written above the input. When the terminal is
2431
- // resized, previously drawn rows re-wrap (a full-width ruler becomes 2+ rows
2432
- // when narrowed), so the move-up count recorded at draw time is wrong these
2433
- // let moveToRegionTop recompute the region height under the NEW wrap instead
2434
- // of leaving stale rulers behind.
2691
+ // Resize bookkeeping. When the terminal width changes, previously drawn HUD
2692
+ // rows re-wrap (a full-width ruler becomes 2+ physical rows when narrowed),
2693
+ // so the caret-relative move-up from the last paint is stale. We store the
2694
+ // visible width of EVERY region row (not just above the body) plus the caret
2695
+ // row index so moveToRegionTop can recompute physical height under the new
2696
+ // wrap. Resize events are debounced — drag-resizing fires dozens of events
2697
+ // and redrawing each one desyncs and leaves ghost chrome.
2435
2698
  lastDrawnCols = 0;
2699
+ /** Visible width of every HUD row from the last paint, top → bottom. */
2700
+ drawnRegionWidths = [];
2701
+ /** Index into drawnRegionWidths of the row the caret sat on last paint. */
2702
+ lastCaretRegionIndex = 0;
2703
+ resizeTimer = null;
2704
+ /** True between a resize event and the debounced repaint — spinner ticks no-op. */
2705
+ resizePending = false;
2706
+ /** @deprecated kept as alias during paint — prefer drawnRegionWidths */
2436
2707
  drawnHudWidths = [];
2437
2708
  // takeover (approval / menu) state
2438
2709
  takeoverHandler = null;
@@ -2471,6 +2742,23 @@ var Tui = class _Tui {
2471
2742
  };
2472
2743
  onQuit = () => process.exit(0);
2473
2744
  levelListeners = [];
2745
+ /**
2746
+ * Terminal resize fires continuously while the user drags. Redrawing on every
2747
+ * event desyncs the region-height math (each paint uses a half-rewrapped
2748
+ * intermediate width) and stamps ghost boxes into the scrollback. Wait for
2749
+ * the size to settle (~1–2 frames), then do one clean clear+repaint. While
2750
+ * pending, spinner ticks skip paint so they don't thrash mid-drag.
2751
+ */
2752
+ scheduleResizeRedraw() {
2753
+ this.resizePending = true;
2754
+ if (this.resizeTimer) clearTimeout(this.resizeTimer);
2755
+ this.resizeTimer = setTimeout(() => {
2756
+ this.resizeTimer = null;
2757
+ this.resizePending = false;
2758
+ if (!this.started || this.takeoverHandler) return;
2759
+ this.renderBottom();
2760
+ }, 40);
2761
+ }
2474
2762
  get colors() {
2475
2763
  return C;
2476
2764
  }
@@ -2512,6 +2800,10 @@ var Tui = class _Tui {
2512
2800
  clearTimeout(this.streamRedrawTimer);
2513
2801
  this.streamRedrawTimer = null;
2514
2802
  }
2803
+ if (this.resizeTimer) {
2804
+ clearTimeout(this.resizeTimer);
2805
+ this.resizeTimer = null;
2806
+ }
2515
2807
  if (this.spinnerTimer) {
2516
2808
  clearInterval(this.spinnerTimer);
2517
2809
  this.spinnerTimer = null;
@@ -2962,7 +3254,7 @@ var Tui = class _Tui {
2962
3254
  const q = this.inputBuffer.slice(1).trim().toLowerCase();
2963
3255
  if (q === "") return this.commands;
2964
3256
  return this.commands.filter(
2965
- (c2) => c2.name.startsWith(q) || c2.name.includes(q) || c2.label.toLowerCase().includes(q)
3257
+ (c4) => c4.name.startsWith(q) || c4.name.includes(q) || c4.label.toLowerCase().includes(q)
2966
3258
  );
2967
3259
  }
2968
3260
  runCommand(cmd) {
@@ -3109,25 +3401,38 @@ var Tui = class _Tui {
3109
3401
  return line;
3110
3402
  });
3111
3403
  }
3404
+ /**
3405
+ * Physical rows a previously painted line of visible width `w` occupies after
3406
+ * the terminal rewraps it to `cols`. Hard-written HUD lines reflow this way.
3407
+ */
3408
+ rewrapRows(w, cols2) {
3409
+ return Math.max(1, Math.ceil(Math.max(w, 1) / Math.max(1, cols2)));
3410
+ }
3112
3411
  /**
3113
3412
  * Move the cursor to the top-left of the current bottom region.
3114
3413
  *
3115
3414
  * Same width as the last draw → the caret's recorded row offset is exact.
3116
3415
  * Width CHANGED (terminal resized) → previously drawn rows re-wrapped, so
3117
- * that offset is stale; recompute it under the new wrap instead: each drawn
3118
- * HUD row of visible width w now occupies ceil(w / cols) physical rows
3119
- * (reflowing terminals re-wrap hard lines; the cursor follows its logical
3120
- * position in the input text, which inputLayout locates at the new width).
3416
+ * that offset is stale. Recompute using every stored region row width under
3417
+ * the NEW wrap: rows above the caret row + (caret row's rewrap − 1) so we
3418
+ * prefer a slight over-move (clean wipe) over under-move (ghost chrome).
3121
3419
  */
3122
3420
  moveToRegionTop() {
3123
3421
  process.stdout.write("\r");
3124
3422
  if (!this.bottomDrawn) return;
3125
3423
  const cols2 = process.stdout.columns || 80;
3126
3424
  let up;
3127
- if (cols2 !== this.lastDrawnCols && this.lastDrawnCols > 0) {
3425
+ if (cols2 !== this.lastDrawnCols && this.lastDrawnCols > 0 && this.drawnRegionWidths.length) {
3426
+ const widths = this.drawnRegionWidths.length > 0 ? this.drawnRegionWidths : this.drawnHudWidths;
3427
+ const caretIdx = Math.max(
3428
+ 0,
3429
+ Math.min(this.lastCaretRegionIndex, Math.max(0, widths.length - 1))
3430
+ );
3128
3431
  let above = 0;
3129
- for (const w of this.drawnHudWidths) above += Math.max(1, Math.ceil(Math.max(w, 1) / cols2));
3130
- up = above + this.inputLayout().caretRow;
3432
+ for (let i = 0; i < caretIdx; i++) above += this.rewrapRows(widths[i], cols2);
3433
+ const caretPhysical = this.rewrapRows(widths[caretIdx] ?? 1, cols2);
3434
+ up = above + Math.max(0, caretPhysical - 1);
3435
+ up += 1;
3131
3436
  } else {
3132
3437
  up = this.lastCursorRow;
3133
3438
  }
@@ -3145,6 +3450,7 @@ var Tui = class _Tui {
3145
3450
  */
3146
3451
  renderBottom() {
3147
3452
  if (!this.started || this.takeoverHandler) return;
3453
+ if (this.resizePending) return;
3148
3454
  const cols2 = process.stdout.columns || 80;
3149
3455
  this.moveToRegionTop();
3150
3456
  const hudWidths = [];
@@ -3181,6 +3487,8 @@ var Tui = class _Tui {
3181
3487
  for (const line of goalLines) writeHudRow(workPad + line);
3182
3488
  const prefix = this.promptPrefix();
3183
3489
  const pw = this.visibleWidth(prefix);
3490
+ const borderAnimating = this.working;
3491
+ const borderPhase = borderAnimating ? Date.now() % 1400 / 1400 : null;
3184
3492
  const boxRows = buildInputBoxRows({
3185
3493
  cols: cols2,
3186
3494
  inputBuffer: this.inputBuffer,
@@ -3188,14 +3496,14 @@ var Tui = class _Tui {
3188
3496
  prefixWidth: pw,
3189
3497
  level: this.level,
3190
3498
  levelColor: this.levelColor(),
3191
- borderColor: inputBoxBorderColor()
3499
+ borderColor: inputBoxBorderColor(),
3500
+ borderPhase
3192
3501
  });
3193
3502
  const boxTop = boxRows[0];
3194
3503
  const boxBottom = boxRows[boxRows.length - 1];
3195
3504
  const boxBody = boxRows.slice(1, -1);
3196
3505
  writeHudRow(boxTop);
3197
- const aboveBodyWidths = hudWidths.slice();
3198
- const aboveBodyRows = aboveBodyWidths.length;
3506
+ const bodyStartIdx = hudWidths.length;
3199
3507
  for (const row of boxBody) writeHudRow(row);
3200
3508
  writeHudRow(boxBottom);
3201
3509
  const paletteBlock = this.paletteBlockLines(cols2);
@@ -3203,10 +3511,12 @@ var Tui = class _Tui {
3203
3511
  const layout = this.inputLayout();
3204
3512
  const bodyRowCount = Math.max(1, boxBody.length);
3205
3513
  const caretBodyRow = Math.max(0, Math.min(layout.caretRow, bodyRowCount - 1));
3206
- const caretRegionRow = aboveBodyRows + caretBodyRow;
3514
+ const caretRegionRow = bodyStartIdx + caretBodyRow;
3207
3515
  const caretScreenCol = Math.min(rowCap - 1, geo.leftPad + layout.caretCol);
3208
3516
  this.lastCursorRow = caretRegionRow;
3209
- this.drawnHudWidths = aboveBodyWidths;
3517
+ this.lastCaretRegionIndex = caretRegionRow;
3518
+ this.drawnRegionWidths = hudWidths.slice();
3519
+ this.drawnHudWidths = hudWidths.slice(0, bodyStartIdx);
3210
3520
  this.lastDrawnCols = cols2;
3211
3521
  this.bottomDrawn = true;
3212
3522
  const totalRows = hudRows.length;
@@ -3563,70 +3873,249 @@ var Tui = class _Tui {
3563
3873
  }
3564
3874
  this.renderBottom();
3565
3875
  }
3566
- /** Approval prompt: arrow-navigable with y/a/l/n shortcuts. Pauses input.
3567
- * Tab resolves "deny_with_reason" so the caller can collect a free-text reason. */
3876
+ /**
3877
+ * ## Boxed prompt reusable HUD free-text component
3878
+ *
3879
+ * Single-line free-text capture using the **same chrome as the main input
3880
+ * box** (side margin, grey rounded rim, level-tinted `❯`, real caret). A
3881
+ * left-aligned label sits in the top border rim instead of the permission
3882
+ * level dots.
3883
+ *
3884
+ * Prefer this over {@link Tui.prompt} whenever the capture should feel like
3885
+ * part of the HUD rather than a legacy bar — denial feedback, handoff notes,
3886
+ * reject-with-reason, rename, or any other short free-text moment.
3887
+ *
3888
+ * ```
3889
+ * ╭ Denial feedback ────────────────────────╮
3890
+ * │ ❯ the user-typed note │
3891
+ * ╰──────────────────────────────────────────╯
3892
+ * ```
3893
+ *
3894
+ * **Keys**
3895
+ * - Enter — submit (resolves the trimmed string; may be `""`)
3896
+ * - Esc — clear a non-empty draft; Esc again (empty) cancels → `null`
3897
+ * (override with `escClearsFirst: false` to always cancel)
3898
+ *
3899
+ * **Nested mode** (`nested: true`) — call from inside another takeover
3900
+ * (e.g. approval → feedback) so this component only steals the key handler
3901
+ * and paints/erases its own three rows; the parent still owns begin/end.
3902
+ *
3903
+ * Do **not** use brand-gradient text for the label — gradient is reserved
3904
+ * for success moments (`✓ Goal complete.`, farewell, etc.).
3905
+ *
3906
+ * @returns trimmed text on Enter, or `null` if the user cancelled with Esc
3907
+ */
3908
+ boxedPrompt(opts) {
3909
+ return new Promise((resolve) => {
3910
+ const nested = !!opts.nested;
3911
+ const escClearsFirst = opts.escClearsFirst !== false;
3912
+ const borderLabel = opts.borderLabel || "Feedback";
3913
+ const labelColor = opts.labelColor ?? "\x1B[90m";
3914
+ let buf = opts.initial ?? "";
3915
+ if (!nested) this.beginTakeover();
3916
+ process.stdout.write("\x1B[?25h");
3917
+ const cols2 = process.stdout.columns || 80;
3918
+ const geo = inputBoxGeometry(cols2);
3919
+ const border = inputBoxBorderColor();
3920
+ const pad = " ".repeat(geo.margin);
3921
+ const rowCap = Math.max(1, cols2 - 1);
3922
+ const reset = C.reset;
3923
+ const prompt = `${this.levelColor()}\u276F${reset} `;
3924
+ const promptW = 2;
3925
+ const writeRow = (line) => {
3926
+ const row = this.clampVisible(sanitizeHudRow(line), rowCap);
3927
+ process.stdout.write(`\r\x1B[K${row}
3928
+ `);
3929
+ };
3930
+ const draw = () => {
3931
+ writeRow(pad + buildBoxTopLeftLabel(geo.boxW, borderLabel, border, labelColor));
3932
+ writeRow(pad + buildBoxBody(prompt + buf, geo.contentW, border));
3933
+ writeRow(pad + buildBoxBottom(geo.boxW, border));
3934
+ process.stdout.write("\r\x1B[2A");
3935
+ const col = geo.leftPad + promptW + buf.length;
3936
+ if (col > 0) process.stdout.write(`\x1B[${Math.min(rowCap - 1, col)}C`);
3937
+ };
3938
+ const clearFrame = () => {
3939
+ process.stdout.write("\r\x1B[1A\x1B[J");
3940
+ };
3941
+ const redraw = () => {
3942
+ process.stdout.write("\r\x1B[1A");
3943
+ draw();
3944
+ };
3945
+ const finish = (value) => {
3946
+ clearFrame();
3947
+ if (!nested) this.endTakeover();
3948
+ else this.takeoverHandler = null;
3949
+ resolve(value);
3950
+ };
3951
+ draw();
3952
+ this.takeoverHandler = (str, key) => {
3953
+ if (key?.name === "escape") {
3954
+ if (escClearsFirst && buf.length > 0) {
3955
+ buf = "";
3956
+ redraw();
3957
+ return;
3958
+ }
3959
+ finish(null);
3960
+ return;
3961
+ }
3962
+ if (key?.name === "return" || key?.name === "enter") {
3963
+ finish(buf.trim());
3964
+ return;
3965
+ }
3966
+ if (key?.name === "backspace") {
3967
+ if (!buf.length) return;
3968
+ buf = buf.slice(0, -1);
3969
+ redraw();
3970
+ return;
3971
+ }
3972
+ if (str && !key?.ctrl && !key?.meta && str >= " ") {
3973
+ if (buf.length < Math.max(8, geo.contentW - promptW - 1)) {
3974
+ buf += str;
3975
+ redraw();
3976
+ }
3977
+ }
3978
+ };
3979
+ });
3980
+ }
3981
+ /**
3982
+ * Permission prompt: boxed chrome matching the HUD, arrow-navigable options
3983
+ * with shortcuts. Includes "Deny with feedback" — choosing it (or Tab)
3984
+ * opens {@link Tui.boxedPrompt} so the reason can ride back to the agent.
3985
+ */
3568
3986
  approval(question, risk) {
3569
3987
  return new Promise((resolve) => {
3570
3988
  const options = [
3571
3989
  { value: "allow", label: "Allow once", shortcut: "y", color: C.green },
3572
3990
  { value: "always", label: "Always allow this tool", shortcut: "a", color: C.cyan },
3573
- { value: "always_risk", label: `Allow level ${risk} and below this session`, shortcut: "l", color: C.cyan },
3574
- { value: "deny", label: "Deny", shortcut: "n", color: C.red }
3991
+ {
3992
+ value: "always_risk",
3993
+ label: `Allow level ${risk} and below this session`,
3994
+ shortcut: "l",
3995
+ color: C.cyan
3996
+ },
3997
+ { value: "deny", label: "Deny", shortcut: "n", color: C.red },
3998
+ {
3999
+ value: "deny_feedback",
4000
+ label: "Deny with feedback",
4001
+ shortcut: "d",
4002
+ color: C.magenta,
4003
+ hint: "tell the agent why"
4004
+ }
3575
4005
  ];
3576
4006
  let idx = 0;
3577
- const riskBar = `${C.red}${"\u25CF".repeat(risk)}${C.gray}${"\u25CB".repeat(5 - risk)}${C.reset}`;
4007
+ const riskLevel = Math.max(1, Math.min(5, Math.round(risk)));
4008
+ const riskBar = borderLevelDotsStyled(riskLevel, C.yellow, "\x1B[38;5;240m");
3578
4009
  this.beginTakeover();
3579
- const cols2 = Math.max(1, process.stdout.columns || 80);
3580
- const physRows = (line) => Math.max(1, Math.ceil(this.visibleWidth(line) / cols2));
3581
- const headerText = `${C.yellow}\u2503${C.reset} ${C.bold}Permission needed${C.reset} risk ${riskBar}`;
3582
- const questionLines = question.split("\n").map((line) => `${C.yellow}\u2503${C.reset} ${line}`);
3583
- const hintLine = `${C.yellow}\u2503${C.reset} ${C.gray}\u21E5 tab \u2014 deny with a reason${C.reset}`;
3584
- const blockLines = [headerText, ...questionLines, hintLine];
3585
- const headerRows = 1 + blockLines.reduce((n, line) => n + physRows(line), 0);
3586
- process.stdout.write(`
3587
- ` + blockLines.map((line) => `${line}
3588
- `).join(""));
3589
- const renderLine = (i) => {
4010
+ const cols2 = process.stdout.columns || 80;
4011
+ const geo = inputBoxGeometry(cols2);
4012
+ const border = inputBoxBorderColor();
4013
+ const pad = " ".repeat(geo.margin);
4014
+ const rowCap = Math.max(1, cols2 - 1);
4015
+ const reset = C.reset;
4016
+ const wrapContent = (text) => {
4017
+ const flat2 = text.replace(/\s+/g, " ").trim();
4018
+ if (!flat2) return [""];
4019
+ const out = [];
4020
+ let rest = flat2;
4021
+ while (rest.length > geo.contentW) {
4022
+ out.push(rest.slice(0, geo.contentW - 1) + "\u2026");
4023
+ rest = rest.slice(geo.contentW - 1);
4024
+ if (out.length >= 4) {
4025
+ break;
4026
+ }
4027
+ }
4028
+ if (out.length < 4) out.push(rest);
4029
+ return out.length ? out : [""];
4030
+ };
4031
+ const summaryLines = question.split("\n").flatMap((line) => {
4032
+ const plain = line.replace(/\x1b\[[0-9;]*m/g, "").trim();
4033
+ if (!plain) return [];
4034
+ const isWhy = /^why:/i.test(plain);
4035
+ return wrapContent(plain).map((l) => isWhy ? `${C.dim}${l}${reset}` : l);
4036
+ });
4037
+ if (!summaryLines.length) summaryLines.push(`${C.dim}(no details)${reset}`);
4038
+ const writeRow = (line) => {
4039
+ const row = this.clampVisible(sanitizeHudRow(line), rowCap);
4040
+ process.stdout.write(`\r\x1B[K${row}
4041
+ `);
4042
+ };
4043
+ const optionCount = options.length;
4044
+ const summaryCount = summaryLines.length;
4045
+ const boxRows = 1 + summaryCount + 1 + optionCount + 1;
4046
+ const titleRows = 2;
4047
+ const totalRows = titleRows + boxRows;
4048
+ const renderOptionContent = (i) => {
3590
4049
  const o = options[i];
3591
4050
  const sel = i === idx;
3592
- const pointer = sel ? `${o.color}\u276F${C.reset}` : " ";
3593
- const label = sel ? `${C.bold}${o.label}${C.reset}` : o.label;
3594
- return `${C.yellow}\u2503${C.reset} ${pointer} ${label} ${C.gray}(${o.shortcut})${C.reset}`;
4051
+ const pointer = sel ? `${o.color}\u276F${reset} ` : " ";
4052
+ const label = sel ? `${C.bold}${o.color}${o.label}${reset}` : `${C.dim}${o.label}${reset}`;
4053
+ const shortcut = `${C.gray}(${o.shortcut})${reset}`;
4054
+ const hint = o.hint && sel ? `${C.dim} ${o.hint}${reset}` : "";
4055
+ const plainLabel = o.label;
4056
+ const used = 2 + plainLabel.length + 1 + 3 + (o.hint && sel ? 2 + o.hint.length : 0);
4057
+ const gap = Math.max(1, geo.contentW - used);
4058
+ return `${pointer}${label}${" ".repeat(gap)}${shortcut}${hint}`;
3595
4059
  };
3596
- const draw = (moveUp) => {
3597
- if (moveUp) process.stdout.write(`\x1B[${options.length}A`);
3598
- for (let i = 0; i < options.length; i++) process.stdout.write(`\r\x1B[K${renderLine(i)}
3599
- `);
4060
+ const drawFrame = (moveUp) => {
4061
+ if (moveUp) process.stdout.write(`\x1B[${totalRows}A`);
4062
+ writeRow("");
4063
+ writeRow(
4064
+ `${pad}${C.bold}Permission needed${reset} ${C.dim}risk${reset} ${riskBar}`
4065
+ );
4066
+ writeRow(pad + buildBoxTopPlain(geo.boxW, border));
4067
+ for (const line of summaryLines) {
4068
+ writeRow(pad + buildBoxBody(line, geo.contentW, border));
4069
+ }
4070
+ writeRow(pad + buildBoxBody("", geo.contentW, border));
4071
+ for (let i = 0; i < optionCount; i++) {
4072
+ writeRow(pad + buildBoxBody(renderOptionContent(i), geo.contentW, border));
4073
+ }
4074
+ writeRow(pad + buildBoxBottom(geo.boxW, border));
3600
4075
  };
3601
- draw(false);
4076
+ drawFrame(false);
3602
4077
  const erase = () => {
3603
4078
  process.stdout.write("\r");
3604
- const up = headerRows + options.length;
3605
- if (up > 0) process.stdout.write(`\x1B[${up}A`);
4079
+ if (totalRows > 0) process.stdout.write(`\x1B[${totalRows}A`);
3606
4080
  process.stdout.write("\x1B[J");
3607
4081
  };
3608
- const finish = (choice) => {
4082
+ const done = (choice, reason) => {
3609
4083
  erase();
3610
4084
  this.endTakeover();
3611
- resolve(choice);
4085
+ resolve({ choice });
4086
+ };
4087
+ const collectFeedback = () => {
4088
+ erase();
4089
+ void this.boxedPrompt({ borderLabel: "Denial feedback", nested: true }).then((why) => {
4090
+ this.endTakeover();
4091
+ if (why) resolve({ choice: "deny", reason: why });
4092
+ else resolve({ choice: "deny" });
4093
+ });
4094
+ };
4095
+ const pick = (value) => {
4096
+ if (value === "deny_feedback") collectFeedback();
4097
+ else done(value);
3612
4098
  };
3613
4099
  this.takeoverHandler = (str, key) => {
3614
4100
  if (key?.name === "up" || str === "k") {
3615
- idx = (idx - 1 + options.length) % options.length;
3616
- draw(true);
4101
+ idx = (idx - 1 + optionCount) % optionCount;
4102
+ drawFrame(true);
3617
4103
  } else if (key?.name === "down" || str === "j") {
3618
- idx = (idx + 1) % options.length;
3619
- draw(true);
4104
+ idx = (idx + 1) % optionCount;
4105
+ drawFrame(true);
3620
4106
  } else if (key?.name === "tab") {
3621
- finish("deny_with_reason");
4107
+ pick("deny_feedback");
3622
4108
  } else if (key?.name === "return" || key?.name === "enter") {
3623
- finish(options[idx].value);
4109
+ pick(options[idx].value);
4110
+ } else if (key?.name === "escape") {
4111
+ done("deny");
3624
4112
  } else {
3625
4113
  const k = (str || "").toLowerCase();
3626
- if (k === "y") finish("allow");
3627
- else if (k === "a") finish("always");
3628
- else if (k === "l") finish("always_risk");
3629
- else if (k === "n" || key?.name === "escape") finish("deny");
4114
+ if (k === "y") pick("allow");
4115
+ else if (k === "a") pick("always");
4116
+ else if (k === "l") pick("always_risk");
4117
+ else if (k === "n") pick("deny");
4118
+ else if (k === "d" || k === "f") pick("deny_feedback");
3630
4119
  }
3631
4120
  };
3632
4121
  });
@@ -3850,7 +4339,7 @@ function tableCells(row) {
3850
4339
  let r = row.trim();
3851
4340
  if (r.startsWith("|")) r = r.slice(1);
3852
4341
  if (r.endsWith("|")) r = r.slice(0, -1);
3853
- return r.split("|").map((c2) => c2.trim());
4342
+ return r.split("|").map((c4) => c4.trim());
3854
4343
  }
3855
4344
  var SEPARATOR = /^[\s|:-]+$/;
3856
4345
  function isTableSeparator(line) {
@@ -3859,17 +4348,17 @@ function isTableSeparator(line) {
3859
4348
  function renderTable(rows) {
3860
4349
  const cols2 = Math.max(...rows.map((r) => r.length));
3861
4350
  const widths = [];
3862
- for (let c2 = 0; c2 < cols2; c2++) {
3863
- widths[c2] = Math.max(...rows.map((r) => visibleWidth(inline(r[c2] ?? ""))));
4351
+ for (let c4 = 0; c4 < cols2; c4++) {
4352
+ widths[c4] = Math.max(...rows.map((r) => visibleWidth(inline(r[c4] ?? ""))));
3864
4353
  }
3865
4354
  const sep = `${GRAY} \u2502 ${R}`;
3866
4355
  const out = [];
3867
4356
  rows.forEach((r, ri) => {
3868
4357
  const cells = [];
3869
- for (let c2 = 0; c2 < cols2; c2++) {
3870
- const raw = r[c2] ?? "";
4358
+ for (let c4 = 0; c4 < cols2; c4++) {
4359
+ const raw = r[c4] ?? "";
3871
4360
  const styled = ri === 0 ? `${BOLD2}${inline(raw)}${R}` : inline(raw);
3872
- cells.push(padEndVisible(styled, widths[c2]));
4361
+ cells.push(padEndVisible(styled, widths[c4]));
3873
4362
  }
3874
4363
  out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
3875
4364
  if (ri === 0) {
@@ -4189,7 +4678,7 @@ var McpManager = class {
4189
4678
  }
4190
4679
  }
4191
4680
  closeAll() {
4192
- for (const [, c2] of this.clients) c2.close();
4681
+ for (const [, c4] of this.clients) c4.close();
4193
4682
  this.clients.clear();
4194
4683
  }
4195
4684
  get(name) {
@@ -4200,13 +4689,13 @@ var McpManager = class {
4200
4689
  }
4201
4690
  toolCount() {
4202
4691
  let n = 0;
4203
- for (const [, c2] of this.clients) n += c2.tools.length;
4692
+ for (const [, c4] of this.clients) n += c4.tools.length;
4204
4693
  return n;
4205
4694
  }
4206
4695
  /** A JSON-serializable catalog of every connected server for the KV/context. */
4207
4696
  catalog() {
4208
4697
  return {
4209
- servers: Array.from(this.clients.values()).map((c2) => c2.catalogEntry()),
4698
+ servers: Array.from(this.clients.values()).map((c4) => c4.catalogEntry()),
4210
4699
  generatedAt: Date.now()
4211
4700
  };
4212
4701
  }
@@ -4296,9 +4785,9 @@ function flattenContent(content, structured) {
4296
4785
  }
4297
4786
  function flattenResourceContents(contents) {
4298
4787
  const parts = [];
4299
- for (const c2 of contents || []) {
4300
- if (typeof c2.text === "string") parts.push(c2.text);
4301
- else if (typeof c2.blob === "string") parts.push(`[binary resource ${String(c2.uri ?? "")} (${c2.blob.length} b64 chars)]`);
4788
+ for (const c4 of contents || []) {
4789
+ if (typeof c4.text === "string") parts.push(c4.text);
4790
+ else if (typeof c4.blob === "string") parts.push(`[binary resource ${String(c4.uri ?? "")} (${c4.blob.length} b64 chars)]`);
4302
4791
  }
4303
4792
  return parts.join("\n").trim();
4304
4793
  }
@@ -4320,10 +4809,10 @@ function sortKeys(value) {
4320
4809
  }
4321
4810
  return value;
4322
4811
  }
4323
- function sha256(input2) {
4324
- return crypto.createHash("sha256").update(input2).digest("hex");
4812
+ function sha256(input3) {
4813
+ return crypto.createHash("sha256").update(input3).digest("hex");
4325
4814
  }
4326
- var DIR = path3.join(os6.homedir(), ".standardagents");
4815
+ var DIR = path3.join(os9.homedir(), ".standardagents");
4327
4816
  var FILE = path3.join(DIR, "credentials");
4328
4817
  function normalizeEndpoint(endpoint) {
4329
4818
  let e = endpoint.trim();
@@ -4381,49 +4870,319 @@ function saveDefaultEndpoint(endpoint) {
4381
4870
  } catch {
4382
4871
  }
4383
4872
  }
4384
- var PKG_NAME = "@standardagents/code";
4385
- var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}`;
4386
- var CACHE_REL_DIR = ".config/standardagents-cli";
4387
- var CACHE_FILE = "update-check.json";
4388
- var STATE_FILE = "auto-update.json";
4389
- var CACHE_TTL_MS = 1e3 * 60 * 60 * 6;
4390
- var CHECK_TIMEOUT_MS = 4e3;
4391
- var IN_FLIGHT_TTL_MS = 15 * 60 * 1e3;
4392
- function cacheDir() {
4393
- return path3.join(homedir(), CACHE_REL_DIR);
4394
- }
4395
- function cachePath() {
4396
- return path3.join(cacheDir(), CACHE_FILE);
4873
+ var c = {
4874
+ reset: "\x1B[0m",
4875
+ dim: "\x1B[2m",
4876
+ teal: "\x1B[38;5;37m"
4877
+ };
4878
+ async function deviceLogin(endpoint) {
4879
+ const start = await fetch(`${endpoint}/api/auth/device/start`, { method: "POST" });
4880
+ if (!start.ok) throw new Error(`This instance does not support browser sign-in (HTTP ${start.status}). Paste an API token instead.`);
4881
+ const info = await start.json();
4882
+ stdout.write(`${c.dim}Opening your browser to sign in. If it doesn't open, visit:${c.reset}
4883
+ `);
4884
+ stdout.write(`
4885
+ ${c.teal}${info.verify_url}${c.reset}
4886
+
4887
+ `);
4888
+ stdout.write(`${c.dim}Waiting for sign-in to complete\u2026 (Ctrl-C to cancel)${c.reset}
4889
+ `);
4890
+ openUrl(info.verify_url);
4891
+ const deadline = Date.now() + (info.expires_in ?? 600) * 1e3;
4892
+ const interval = Math.max(2, info.interval ?? 2) * 1e3;
4893
+ while (Date.now() < deadline) {
4894
+ await new Promise((r) => setTimeout(r, interval));
4895
+ const res = await fetch(info.poll_url).catch(() => null);
4896
+ if (!res) continue;
4897
+ if (res.status === 404) throw new Error("The sign-in link expired. Try again.");
4898
+ const body = await res.json().catch(() => ({}));
4899
+ if (body.status === "approved" && body.token) return body.token;
4900
+ if (body.status === "denied") throw new Error("Sign-in was denied in the browser.");
4901
+ }
4902
+ throw new Error("Timed out waiting for browser approval. Try again.");
4397
4903
  }
4398
- function readCache() {
4904
+ function openUrl(url) {
4905
+ const platform = process.platform;
4906
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
4907
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
4399
4908
  try {
4400
- const raw = fs4.readFileSync(cachePath(), "utf-8");
4401
- return JSON.parse(raw);
4909
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
4910
+ child.unref();
4402
4911
  } catch {
4403
- return null;
4404
4912
  }
4405
4913
  }
4406
- function writeCache(latest) {
4914
+ var AGENT_ID = "standard_code_agent";
4915
+ var AGENT_ID_VARIANTS = [
4916
+ AGENT_ID,
4917
+ "standard_code_low_agent",
4918
+ "standard_code_high_agent"
4919
+ ];
4920
+ var PRODUCTION_ENDPOINT = "https://api.standardcode.ai";
4921
+ function readVersion() {
4407
4922
  try {
4408
- const dir = cacheDir();
4409
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
4410
- fs4.writeFileSync(cachePath(), JSON.stringify({ latest, timestamp: Date.now() }));
4923
+ const pkg = JSON.parse(fs4.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
4924
+ return typeof pkg.version === "string" ? pkg.version : "";
4411
4925
  } catch {
4926
+ return "";
4412
4927
  }
4413
4928
  }
4414
- function readAutoUpdateState(dir = cacheDir()) {
4929
+ function isLocalHost(host) {
4930
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.endsWith(".localhost") || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
4931
+ }
4932
+ function relaxTlsForLocalEndpoint(endpoint) {
4933
+ let host = "";
4415
4934
  try {
4416
- const raw = fs4.readFileSync(path3.join(dir, STATE_FILE), "utf-8");
4417
- const state = JSON.parse(raw);
4418
- return typeof state?.version === "string" ? state : null;
4935
+ host = new URL(endpoint).hostname;
4419
4936
  } catch {
4420
- return null;
4937
+ return false;
4421
4938
  }
4939
+ if (!endpoint.startsWith("https:") || !isLocalHost(host)) return false;
4940
+ const origEmit = process.emitWarning.bind(process);
4941
+ process.emitWarning = ((warning, ...args) => {
4942
+ const msg = typeof warning === "string" ? warning : warning?.message ?? "";
4943
+ if (msg.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
4944
+ return origEmit(warning, ...args);
4945
+ });
4946
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
4947
+ return true;
4422
4948
  }
4423
- function writeAutoUpdateState(state, dir = cacheDir()) {
4949
+ var DIR2 = path3.join(os9.homedir(), ".standardagents");
4950
+ var FILE2 = path3.join(DIR2, "machine.json");
4951
+ function loadMachineIdentity() {
4424
4952
  try {
4425
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
4426
- fs4.writeFileSync(path3.join(dir, STATE_FILE), JSON.stringify(state));
4953
+ const parsed = JSON.parse(fs4.readFileSync(FILE2, "utf8"));
4954
+ if (typeof parsed.machine_id === "string" && parsed.machine_id.length > 0) {
4955
+ return parsed;
4956
+ }
4957
+ } catch {
4958
+ }
4959
+ const identity = {
4960
+ machine_id: crypto.randomUUID(),
4961
+ created_at: Date.now()
4962
+ };
4963
+ saveMachineIdentity(identity);
4964
+ return identity;
4965
+ }
4966
+ function saveMachineIdentity(identity) {
4967
+ fs4.mkdirSync(DIR2, { recursive: true });
4968
+ fs4.writeFileSync(FILE2, JSON.stringify(identity, null, 2), { mode: 384 });
4969
+ }
4970
+ function setMachineName(name) {
4971
+ const identity = loadMachineIdentity();
4972
+ identity.name = name.trim() || void 0;
4973
+ saveMachineIdentity(identity);
4974
+ return identity;
4975
+ }
4976
+ function machineDisplayName(identity) {
4977
+ return identity.name?.trim() || os9.hostname();
4978
+ }
4979
+ function daemonClientId(identity) {
4980
+ return `daemon:${identity.machine_id}`;
4981
+ }
4982
+ function interactiveClientId(identity) {
4983
+ return `cli:${identity.machine_id}:${crypto.randomBytes(4).toString("hex")}`;
4984
+ }
4985
+ function machineIdFromDaemonClientId(clientId) {
4986
+ if (typeof clientId !== "string") return null;
4987
+ return clientId.startsWith("daemon:") ? clientId.slice("daemon:".length) : null;
4988
+ }
4989
+ var KEY_PREFIX = "standardcode.machine.";
4990
+ var DAEMON_ONLINE_WINDOW_MS = 90 * 1e3;
4991
+ function machineKey(machineId) {
4992
+ return `${KEY_PREFIX}${machineId}`;
4993
+ }
4994
+ function parseMachineRecord(value) {
4995
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
4996
+ const r = value;
4997
+ if (typeof r.id !== "string" || r.id.length === 0) return null;
4998
+ return {
4999
+ id: r.id,
5000
+ name: typeof r.name === "string" && r.name ? r.name : String(r.hostname ?? r.id),
5001
+ hostname: typeof r.hostname === "string" ? r.hostname : "",
5002
+ platform: typeof r.platform === "string" ? r.platform : "",
5003
+ arch: typeof r.arch === "string" ? r.arch : "",
5004
+ daemon: r.daemon && typeof r.daemon === "object" && !Array.isArray(r.daemon) ? r.daemon : null,
5005
+ projects: r.projects && typeof r.projects === "object" && !Array.isArray(r.projects) ? r.projects : {},
5006
+ created_at: typeof r.created_at === "number" ? r.created_at : 0,
5007
+ updated_at: typeof r.updated_at === "number" ? r.updated_at : 0
5008
+ };
5009
+ }
5010
+ async function loadMachines(api) {
5011
+ const entries = await api.userKvList(KEY_PREFIX);
5012
+ return entries.map((e) => parseMachineRecord(e.value)).filter((m) => m !== null);
5013
+ }
5014
+ async function loadMachine(api, machineId) {
5015
+ return parseMachineRecord(await api.userKvGet(machineKey(machineId)));
5016
+ }
5017
+ function daemonOnline(record, now = Date.now()) {
5018
+ return !!record.daemon && now - record.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
5019
+ }
5020
+ function newRecord(identity) {
5021
+ const now = Date.now();
5022
+ return {
5023
+ id: identity.machine_id,
5024
+ name: machineDisplayName(identity),
5025
+ hostname: os9.hostname(),
5026
+ platform: process.platform,
5027
+ arch: process.arch,
5028
+ daemon: null,
5029
+ projects: {},
5030
+ created_at: now,
5031
+ updated_at: now
5032
+ };
5033
+ }
5034
+ async function updateOwnMachineRecord(api, identity, mutate) {
5035
+ const existing = await loadMachine(api, identity.machine_id);
5036
+ const record = existing ?? newRecord(identity);
5037
+ record.name = machineDisplayName(identity);
5038
+ record.hostname = os9.hostname();
5039
+ record.platform = process.platform;
5040
+ record.arch = process.arch;
5041
+ mutate?.(record);
5042
+ record.updated_at = Date.now();
5043
+ await api.userKvSet(machineKey(identity.machine_id), record);
5044
+ return record;
5045
+ }
5046
+ function projectRepository(projectDir) {
5047
+ try {
5048
+ const url = execFileSync("git", ["-C", projectDir, "remote", "get-url", "origin"], {
5049
+ encoding: "utf8",
5050
+ timeout: 3e3,
5051
+ stdio: ["ignore", "pipe", "ignore"]
5052
+ }).trim();
5053
+ return url || null;
5054
+ } catch {
5055
+ return null;
5056
+ }
5057
+ }
5058
+ async function registerProject(api, identity, projectDir) {
5059
+ const repository = projectRepository(projectDir);
5060
+ await updateOwnMachineRecord(api, identity, (record) => {
5061
+ record.projects[projectDir] = {
5062
+ name: projectDir.split("/").filter(Boolean).pop() || projectDir,
5063
+ last_used_at: Date.now(),
5064
+ repository
5065
+ };
5066
+ });
5067
+ }
5068
+ async function unregisterProject(api, identity, projectDir) {
5069
+ await updateOwnMachineRecord(api, identity, (record) => {
5070
+ delete record.projects[projectDir];
5071
+ });
5072
+ }
5073
+ async function touchDaemon(api, identity, version) {
5074
+ await updateOwnMachineRecord(api, identity, (record) => {
5075
+ const now = Date.now();
5076
+ record.daemon = {
5077
+ version,
5078
+ installed_at: record.daemon?.installed_at ?? now,
5079
+ last_seen_at: now,
5080
+ pid: process.pid
5081
+ };
5082
+ });
5083
+ }
5084
+ async function clearDaemon(api, identity) {
5085
+ await updateOwnMachineRecord(api, identity, (record) => {
5086
+ record.daemon = null;
5087
+ });
5088
+ }
5089
+
5090
+ // src/relay.ts
5091
+ var APPROVAL_REQUEST_KEY = "approval_request";
5092
+ var APPROVAL_RESPONSE_KEY = "approval_response";
5093
+ function parseApprovalRequest(value) {
5094
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
5095
+ const r = value;
5096
+ if (typeof r.tool_call_id !== "string" || typeof r.tool !== "string") return null;
5097
+ return {
5098
+ tool_call_id: r.tool_call_id,
5099
+ tool: r.tool,
5100
+ summary: typeof r.summary === "string" ? r.summary : r.tool,
5101
+ permission: typeof r.permission === "string" ? r.permission : null,
5102
+ risk: typeof r.risk === "number" ? r.risk : 3,
5103
+ machine: typeof r.machine === "string" ? r.machine : "",
5104
+ requested_at: typeof r.requested_at === "number" ? r.requested_at : 0
5105
+ };
5106
+ }
5107
+ function parseApprovalResponse(value) {
5108
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
5109
+ const r = value;
5110
+ if (typeof r.tool_call_id !== "string" || typeof r.choice !== "string") return null;
5111
+ if (!["allow", "deny", "always", "always_risk"].includes(r.choice)) return null;
5112
+ return {
5113
+ tool_call_id: r.tool_call_id,
5114
+ choice: r.choice,
5115
+ reason: typeof r.reason === "string" ? r.reason : void 0,
5116
+ decided_at: typeof r.decided_at === "number" ? r.decided_at : 0
5117
+ };
5118
+ }
5119
+ async function writeApprovalResponse(api, threadId, response) {
5120
+ await api.kvSet(threadId, APPROVAL_RESPONSE_KEY, response);
5121
+ }
5122
+ async function awaitApprovalViaRelay(api, threadId, request, options = {}) {
5123
+ const pollMs = options.pollMs ?? 2e3;
5124
+ const timeoutMs = options.timeoutMs ?? 24 * 60 * 60 * 1e3;
5125
+ await api.kvSet(threadId, APPROVAL_REQUEST_KEY, request);
5126
+ const deadline = Date.now() + timeoutMs;
5127
+ try {
5128
+ while (Date.now() < deadline) {
5129
+ await new Promise((r) => setTimeout(r, pollMs));
5130
+ const response = parseApprovalResponse(await api.kvGet(threadId, APPROVAL_RESPONSE_KEY));
5131
+ if (response && response.tool_call_id === request.tool_call_id) {
5132
+ return response;
5133
+ }
5134
+ }
5135
+ return null;
5136
+ } finally {
5137
+ await api.kvSet(threadId, APPROVAL_REQUEST_KEY, null).catch(() => {
5138
+ });
5139
+ await api.kvSet(threadId, APPROVAL_RESPONSE_KEY, null).catch(() => {
5140
+ });
5141
+ }
5142
+ }
5143
+ var PKG_NAME = "@standardagents/code";
5144
+ var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}`;
5145
+ var CACHE_REL_DIR = ".config/standardagents-cli";
5146
+ var CACHE_FILE = "update-check.json";
5147
+ var STATE_FILE = "auto-update.json";
5148
+ var CACHE_TTL_MS = 1e3 * 60 * 60 * 6;
5149
+ var CHECK_TIMEOUT_MS = 4e3;
5150
+ var IN_FLIGHT_TTL_MS = 15 * 60 * 1e3;
5151
+ function cacheDir() {
5152
+ return path3.join(homedir(), CACHE_REL_DIR);
5153
+ }
5154
+ function cachePath() {
5155
+ return path3.join(cacheDir(), CACHE_FILE);
5156
+ }
5157
+ function readCache() {
5158
+ try {
5159
+ const raw = fs4.readFileSync(cachePath(), "utf-8");
5160
+ return JSON.parse(raw);
5161
+ } catch {
5162
+ return null;
5163
+ }
5164
+ }
5165
+ function writeCache(latest) {
5166
+ try {
5167
+ const dir = cacheDir();
5168
+ if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
5169
+ fs4.writeFileSync(cachePath(), JSON.stringify({ latest, timestamp: Date.now() }));
5170
+ } catch {
5171
+ }
5172
+ }
5173
+ function readAutoUpdateState(dir = cacheDir()) {
5174
+ try {
5175
+ const raw = fs4.readFileSync(path3.join(dir, STATE_FILE), "utf-8");
5176
+ const state = JSON.parse(raw);
5177
+ return typeof state?.version === "string" ? state : null;
5178
+ } catch {
5179
+ return null;
5180
+ }
5181
+ }
5182
+ function writeAutoUpdateState(state, dir = cacheDir()) {
5183
+ try {
5184
+ if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
5185
+ fs4.writeFileSync(path3.join(dir, STATE_FILE), JSON.stringify(state));
4427
5186
  } catch {
4428
5187
  }
4429
5188
  }
@@ -4558,11 +5317,728 @@ function runUpdate(pm) {
4558
5317
  });
4559
5318
  }
4560
5319
 
5320
+ // src/daemon.ts
5321
+ var HEARTBEAT_MS = 3e4;
5322
+ var RECLAIM_PROBE_MS = 2 * 6e4;
5323
+ var UPDATE_CHECK_MS = 6 * 60 * 6e4;
5324
+ var SWEEP_MS = 10 * 6e4;
5325
+ var MAX_WORKERS = 30;
5326
+ var LOG_MAX_BYTES = 1e6;
5327
+ var LOG_FILE = path3.join(os9.homedir(), ".standardagents", "daemon.log");
5328
+ function daemonLog(line) {
5329
+ try {
5330
+ fs4.mkdirSync(path3.dirname(LOG_FILE), { recursive: true });
5331
+ try {
5332
+ if (fs4.statSync(LOG_FILE).size > LOG_MAX_BYTES) {
5333
+ fs4.renameSync(LOG_FILE, `${LOG_FILE}.old`);
5334
+ }
5335
+ } catch {
5336
+ }
5337
+ fs4.appendFileSync(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
5338
+ `);
5339
+ } catch {
5340
+ }
5341
+ }
5342
+ function pathFromTags(tags) {
5343
+ const tag = tags.find((t) => t.startsWith("path:"));
5344
+ if (!tag) return null;
5345
+ const raw = tag.slice("path:".length);
5346
+ return raw.replace(/^~(?=\/|$)/, os9.homedir());
5347
+ }
5348
+ var ThreadWorker = class {
5349
+ constructor(api, identity, threadId, projectDir, createdAt) {
5350
+ this.api = api;
5351
+ this.identity = identity;
5352
+ this.threadId = threadId;
5353
+ this.projectDir = projectDir;
5354
+ this.createdAt = createdAt;
5355
+ const machineName = machineDisplayName(identity);
5356
+ const registry = new ProcessRegistry(api, threadId, machineName);
5357
+ const mcp = new McpManager(projectDir);
5358
+ const publishCatalog = () => void api.kvSet(threadId, "mcp_catalog", mcp.catalog()).catch(() => {
5359
+ });
5360
+ const host = new HostTools(projectDir, registry, threadId, machineName, mcp, publishCatalog, api);
5361
+ this.perm = { level: 1, alwaysAllow: /* @__PURE__ */ new Set(), allowRisk: /* @__PURE__ */ new Set() };
5362
+ this.bridge = new Bridge(
5363
+ api,
5364
+ threadId,
5365
+ host,
5366
+ this.perm,
5367
+ {
5368
+ onActivity: (line) => daemonLog(`[${threadId.slice(0, 8)}] ${line}`),
5369
+ onStatus: (_id, summary) => {
5370
+ this.inFlight += summary ? 1 : -1;
5371
+ if (this.inFlight < 0) this.inFlight = 0;
5372
+ },
5373
+ onConnection: (state, attempt) => {
5374
+ if (state !== "reconnecting" || attempt === 1 || attempt % 10 === 0) {
5375
+ daemonLog(`[${threadId.slice(0, 8)}] bridge ${state}${attempt ? ` (attempt ${attempt})` : ""}`);
5376
+ }
5377
+ },
5378
+ onOwnership: (isOwner, owner) => {
5379
+ this.isOwner = isOwner;
5380
+ daemonLog(
5381
+ `[${threadId.slice(0, 8)}] ownership: ${isOwner ? "OWNER" : "watcher"}` + (owner ? ` (owner: ${owner.client_name || owner.client_id})` : "")
5382
+ );
5383
+ },
5384
+ onSuperseded: () => {
5385
+ daemonLog(`[${threadId.slice(0, 8)}] superseded by another process with this identity \u2014 standing down`);
5386
+ this.onEvicted?.(threadId);
5387
+ },
5388
+ // The daemon's approvals are edited by OTHER clients (the watching
5389
+ // CLI's level menu writes the thread KV) — re-sync before every
5390
+ // permission decision so a level change applies to the next call.
5391
+ refreshPermissions: () => this.reloadApprovals(),
5392
+ requestApproval: async (req, summary, effectiveRisk) => {
5393
+ const permKey = permissionKey(req);
5394
+ const fresh = decide(this.perm, permKey, effectiveRisk, !!req.requestPermission);
5395
+ if (fresh === "allow") return { choice: "allow" };
5396
+ if (!req.toolCallId) {
5397
+ return { choice: "deny", reason: "No one is available to approve this right now." };
5398
+ }
5399
+ daemonLog(`[${threadId.slice(0, 8)}] relaying approval: ${summary} (risk ${effectiveRisk})`);
5400
+ const response = await awaitApprovalViaRelay(api, threadId, {
5401
+ tool_call_id: req.toolCallId,
5402
+ tool: req.tool,
5403
+ summary,
5404
+ permission: req.requestPermission,
5405
+ risk: effectiveRisk,
5406
+ machine: machineName,
5407
+ requested_at: Date.now()
5408
+ });
5409
+ if (!response) {
5410
+ return { choice: "deny", reason: "The approval request timed out with no one to approve it." };
5411
+ }
5412
+ if (response.choice === "always") this.perm.alwaysAllow.add(permKey);
5413
+ if (response.choice === "always_risk") this.perm.allowRisk.add(effectiveRisk);
5414
+ if (response.choice === "always" || response.choice === "always_risk") {
5415
+ saveApprovals(api, threadId, this.perm);
5416
+ }
5417
+ return { choice: response.choice, reason: response.reason };
5418
+ }
5419
+ },
5420
+ {
5421
+ clientId: daemonClientId(identity),
5422
+ clientName: machineName,
5423
+ clientKind: "daemon",
5424
+ claim: "if_unowned"
5425
+ }
5426
+ );
5427
+ void (async () => {
5428
+ for (const s of listMcpServers().filter((s2) => s2.enabled)) {
5429
+ try {
5430
+ await mcp.connect(s);
5431
+ } catch (e) {
5432
+ daemonLog(`[${threadId.slice(0, 8)}] MCP "${s.name}" failed: ${e instanceof Error ? e.message : String(e)}`);
5433
+ }
5434
+ }
5435
+ publishCatalog();
5436
+ })();
5437
+ }
5438
+ api;
5439
+ identity;
5440
+ threadId;
5441
+ projectDir;
5442
+ createdAt;
5443
+ bridge;
5444
+ perm;
5445
+ inFlight = 0;
5446
+ isOwner = false;
5447
+ /** Set by the daemon so a superseded worker can remove itself. */
5448
+ onEvicted;
5449
+ get busy() {
5450
+ return this.inFlight > 0;
5451
+ }
5452
+ async reloadApprovals() {
5453
+ try {
5454
+ const saved = await loadApprovals(this.api, this.threadId);
5455
+ this.perm.alwaysAllow = new Set(saved.allowTools);
5456
+ this.perm.allowRisk = new Set(saved.allowRisk);
5457
+ if (saved.level) this.perm.level = saved.level;
5458
+ } catch {
5459
+ }
5460
+ }
5461
+ async start() {
5462
+ await this.reloadApprovals();
5463
+ void this.api.kvSet(this.threadId, "session_info", {
5464
+ cwd: this.projectDir,
5465
+ machine: machineDisplayName(this.identity)
5466
+ }).catch(() => {
5467
+ });
5468
+ await this.bridge.connect();
5469
+ }
5470
+ stop() {
5471
+ this.bridge.close();
5472
+ }
5473
+ };
5474
+ async function runDaemon(options = {}) {
5475
+ const endpoint = normalizeEndpoint(options.endpoint || defaultEndpoint() || PRODUCTION_ENDPOINT);
5476
+ relaxTlsForLocalEndpoint(endpoint);
5477
+ const version = readVersion();
5478
+ const identity = loadMachineIdentity();
5479
+ const runnerTag = `runner:${identity.machine_id}`;
5480
+ const cred = getCredential(endpoint);
5481
+ if (!cred) {
5482
+ process.stderr.write(
5483
+ `No saved sign-in for ${endpoint}.
5484
+ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5485
+ `
5486
+ );
5487
+ process.exit(1);
5488
+ }
5489
+ const api = new ApiClient(endpoint, cred.access_token);
5490
+ const check = await api.verifyDetailed();
5491
+ if (!check.ok) {
5492
+ daemonLog(`startup: credential check failed: ${check.reason}`);
5493
+ process.stderr.write(`Sign-in check failed: ${check.reason}
5494
+ `);
5495
+ process.exit(1);
5496
+ }
5497
+ const applied = consumeAppliedUpdate(version);
5498
+ daemonLog(
5499
+ `daemon starting \u2014 v${version}${applied ? " (freshly auto-updated)" : ""}, machine ${identity.machine_id} (${machineDisplayName(identity)}), endpoint ${endpoint}`
5500
+ );
5501
+ process.on("uncaughtException", (err) => {
5502
+ daemonLog(`FATAL uncaughtException: ${err?.stack || err}`);
5503
+ process.exit(1);
5504
+ });
5505
+ process.on("unhandledRejection", (err) => {
5506
+ daemonLog(`unhandledRejection: ${err instanceof Error ? err.stack : String(err)}`);
5507
+ });
5508
+ const workers = /* @__PURE__ */ new Map();
5509
+ const attach = async (threadId, tags, createdAt = 0) => {
5510
+ if (workers.has(threadId)) return;
5511
+ const projectDir = pathFromTags(tags);
5512
+ if (!projectDir) return;
5513
+ if (workers.size >= MAX_WORKERS) {
5514
+ const oldest = [...workers.values()].filter((w) => !w.busy).sort((a, b) => a.createdAt - b.createdAt)[0];
5515
+ if (!oldest) return;
5516
+ oldest.stop();
5517
+ workers.delete(oldest.threadId);
5518
+ daemonLog(`detached ${oldest.threadId.slice(0, 8)} (worker cap)`);
5519
+ }
5520
+ try {
5521
+ fs4.mkdirSync(projectDir, { recursive: true });
5522
+ } catch (e) {
5523
+ daemonLog(`cannot prepare project dir ${projectDir}: ${e instanceof Error ? e.message : String(e)}`);
5524
+ return;
5525
+ }
5526
+ const worker = new ThreadWorker(api, identity, threadId, projectDir, createdAt || Date.now());
5527
+ worker.onEvicted = (id) => detach(id);
5528
+ workers.set(threadId, worker);
5529
+ daemonLog(`attached ${threadId.slice(0, 8)} \u2192 ${projectDir}`);
5530
+ await worker.start();
5531
+ void registerProject(api, identity, projectDir).catch(() => {
5532
+ });
5533
+ };
5534
+ const detach = (threadId) => {
5535
+ const worker = workers.get(threadId);
5536
+ if (!worker) return;
5537
+ worker.stop();
5538
+ workers.delete(threadId);
5539
+ daemonLog(`detached ${threadId.slice(0, 8)}`);
5540
+ };
5541
+ const sweep = async () => {
5542
+ try {
5543
+ const threads = await api.listThreads(AGENT_ID_VARIANTS, [runnerTag]);
5544
+ const recent = threads.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)).slice(0, MAX_WORKERS);
5545
+ for (const t of recent) {
5546
+ await attach(t.id, t.tags, (t.created_at ?? 0) * 1e3);
5547
+ }
5548
+ } catch (e) {
5549
+ daemonLog(`sweep failed: ${e instanceof Error ? e.message : String(e)}`);
5550
+ }
5551
+ };
5552
+ const events = new SystemEvents(api, {
5553
+ onOpen: () => void sweep(),
5554
+ onThreadCreated: (t) => {
5555
+ if (t.tags?.includes(runnerTag)) void attach(t.id, t.tags ?? [], (t.created_at ?? 0) * 1e3);
5556
+ },
5557
+ onThreadUpdated: (t) => {
5558
+ if (t.terminated) detach(t.id);
5559
+ else if (t.tags?.includes(runnerTag)) void attach(t.id, t.tags ?? [], (t.created_at ?? 0) * 1e3);
5560
+ },
5561
+ onThreadDeleted: (id) => detach(id)
5562
+ });
5563
+ events.connect();
5564
+ await sweep();
5565
+ await touchDaemon(api, identity, version).catch(
5566
+ (e) => daemonLog(`heartbeat failed: ${e instanceof Error ? e.message : String(e)}`)
5567
+ );
5568
+ const heartbeat = setInterval(() => {
5569
+ void touchDaemon(api, identity, version).catch(() => {
5570
+ });
5571
+ }, HEARTBEAT_MS);
5572
+ const reclaim = setInterval(() => {
5573
+ for (const worker of workers.values()) {
5574
+ if (!worker.isOwner) {
5575
+ worker.bridge.setClaim("if_stale");
5576
+ worker.bridge.refresh();
5577
+ } else {
5578
+ worker.bridge.setClaim("if_unowned");
5579
+ }
5580
+ }
5581
+ }, RECLAIM_PROBE_MS);
5582
+ let updateReady = false;
5583
+ const checkUpdates = async () => {
5584
+ try {
5585
+ const info = await checkForUpdate(version);
5586
+ if (!info) return;
5587
+ const pm = detectPackageManager();
5588
+ const decision = decideAutoUpdate(info, { state: readAutoUpdateState(), pm });
5589
+ if (decision === "start" && pm) {
5590
+ daemonLog(`auto-update: installing v${info.latest} in the background`);
5591
+ startBackgroundUpdate(info.latest, pm);
5592
+ }
5593
+ const state = readAutoUpdateState();
5594
+ if (state && state.version === info.latest && state.exitCode === 0) {
5595
+ daemonLog(`auto-update: v${info.latest} installed \u2014 restarting when idle`);
5596
+ updateReady = true;
5597
+ }
5598
+ } catch {
5599
+ }
5600
+ };
5601
+ void checkUpdates();
5602
+ const updateTimer = setInterval(() => void checkUpdates(), UPDATE_CHECK_MS);
5603
+ const sweeper = setInterval(() => {
5604
+ if (updateReady && ![...workers.values()].some((w) => w.busy)) {
5605
+ daemonLog("restarting to apply the installed update");
5606
+ shutdown(0);
5607
+ return;
5608
+ }
5609
+ void sweep();
5610
+ }, SWEEP_MS);
5611
+ const shutdown = (code) => {
5612
+ clearInterval(heartbeat);
5613
+ clearInterval(reclaim);
5614
+ clearInterval(updateTimer);
5615
+ clearInterval(sweeper);
5616
+ for (const worker of workers.values()) worker.stop();
5617
+ events.close();
5618
+ daemonLog(`daemon exiting (code ${code})`);
5619
+ process.exit(code);
5620
+ };
5621
+ process.on("SIGTERM", () => shutdown(0));
5622
+ process.on("SIGINT", () => shutdown(0));
5623
+ daemonLog(`daemon ready \u2014 watching for threads tagged ${runnerTag}`);
5624
+ await new Promise(() => {
5625
+ });
5626
+ }
5627
+ var SERVICE_LABEL = "ai.standardcode.daemon";
5628
+ var SYSTEMD_UNIT = "standardcode-daemon.service";
5629
+ function resolveDaemonCommand(extraArgs = []) {
5630
+ const entry = path3.resolve(process.argv[1] ?? "");
5631
+ if (!entry) throw new Error("Cannot determine how this CLI was launched.");
5632
+ const argv = [process.execPath];
5633
+ if (entry.endsWith(".ts")) {
5634
+ let dir = path3.dirname(entry);
5635
+ let tsx = null;
5636
+ for (let i = 0; i < 6 && dir !== path3.dirname(dir); i++) {
5637
+ const candidate = path3.join(dir, "node_modules", "tsx", "dist", "cli.mjs");
5638
+ if (fs4.existsSync(candidate)) {
5639
+ tsx = candidate;
5640
+ break;
5641
+ }
5642
+ dir = path3.dirname(dir);
5643
+ }
5644
+ if (!tsx) {
5645
+ throw new Error(
5646
+ "This is a source checkout and tsx wasn't found \u2014 run `pnpm install` in the repo, or install the CLI globally and re-run daemon install."
5647
+ );
5648
+ }
5649
+ argv.push(tsx);
5650
+ }
5651
+ argv.push(entry, "daemon", "run", ...extraArgs);
5652
+ return { argv };
5653
+ }
5654
+ function servicePath() {
5655
+ const parts = [
5656
+ path3.dirname(process.execPath),
5657
+ "/opt/homebrew/bin",
5658
+ "/usr/local/bin",
5659
+ "/usr/bin",
5660
+ "/bin",
5661
+ "/usr/sbin",
5662
+ "/sbin"
5663
+ ];
5664
+ const current = (process.env.PATH || "").split(":").filter(Boolean);
5665
+ return [.../* @__PURE__ */ new Set([...current, ...parts])].join(":");
5666
+ }
5667
+ function run2(cmd, args) {
5668
+ const res = spawnSync(cmd, args, { encoding: "utf8" });
5669
+ const output4 = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
5670
+ return { ok: res.status === 0, output: output4 };
5671
+ }
5672
+ var plistPath = () => path3.join(os9.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
5673
+ var unitPath = () => path3.join(os9.homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
5674
+ var xmlEscape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5675
+ function installService(command, endpoint) {
5676
+ if (process.platform === "darwin") return installLaunchd(command, endpoint);
5677
+ if (process.platform === "linux") return installSystemd(command, endpoint);
5678
+ return {
5679
+ ok: false,
5680
+ detail: `Unsupported platform for the daemon service: ${process.platform}`,
5681
+ manualHint: "Run `standardcode daemon run` under your own supervisor."
5682
+ };
5683
+ }
5684
+ function installLaunchd(command, endpoint) {
5685
+ const logDir = path3.join(os9.homedir(), ".standardagents");
5686
+ fs4.mkdirSync(logDir, { recursive: true });
5687
+ fs4.mkdirSync(path3.dirname(plistPath()), { recursive: true });
5688
+ const envEntries = [
5689
+ ` <key>PATH</key><string>${xmlEscape(servicePath())}</string>`,
5690
+ ...endpoint ? [` <key>STANDARD_CODE_DAEMON_ENDPOINT</key><string>${xmlEscape(endpoint)}</string>`] : []
5691
+ ].join("\n");
5692
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
5693
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
5694
+ <plist version="1.0">
5695
+ <dict>
5696
+ <key>Label</key><string>${SERVICE_LABEL}</string>
5697
+ <key>ProgramArguments</key>
5698
+ <array>
5699
+ ${command.argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n")}
5700
+ </array>
5701
+ <key>RunAtLoad</key><true/>
5702
+ <key>KeepAlive</key><true/>
5703
+ <key>ThrottleInterval</key><integer>5</integer>
5704
+ <key>StandardOutPath</key><string>${xmlEscape(path3.join(logDir, "daemon.out.log"))}</string>
5705
+ <key>StandardErrorPath</key><string>${xmlEscape(path3.join(logDir, "daemon.err.log"))}</string>
5706
+ <key>EnvironmentVariables</key>
5707
+ <dict>
5708
+ ${envEntries}
5709
+ </dict>
5710
+ </dict>
5711
+ </plist>
5712
+ `;
5713
+ fs4.writeFileSync(plistPath(), plist);
5714
+ const uid = typeof process.getuid === "function" ? process.getuid() : 501;
5715
+ run2("launchctl", ["bootout", `gui/${uid}`, plistPath()]);
5716
+ const boot = run2("launchctl", ["bootstrap", `gui/${uid}`, plistPath()]);
5717
+ if (!boot.ok) {
5718
+ const load = run2("launchctl", ["load", "-w", plistPath()]);
5719
+ if (!load.ok) {
5720
+ return {
5721
+ ok: false,
5722
+ detail: `launchctl failed: ${boot.output || load.output}`,
5723
+ manualHint: `Load it manually: launchctl bootstrap gui/${uid} ${plistPath()}`
5724
+ };
5725
+ }
5726
+ }
5727
+ return { ok: true, detail: `LaunchAgent installed (${plistPath()})` };
5728
+ }
5729
+ function installSystemd(command, endpoint) {
5730
+ fs4.mkdirSync(path3.dirname(unitPath()), { recursive: true });
5731
+ const unit = `[Unit]
5732
+ Description=Standard Code daemon (headless coding-agent execution client)
5733
+ After=network-online.target
5734
+
5735
+ [Service]
5736
+ ExecStart=${command.argv.map((a) => a.includes(" ") ? `"${a}"` : a).join(" ")}
5737
+ Restart=always
5738
+ RestartSec=5
5739
+ Environment=PATH=${servicePath()}
5740
+ ${endpoint ? `Environment=STANDARD_CODE_DAEMON_ENDPOINT=${endpoint}
5741
+ ` : ""}
5742
+ [Install]
5743
+ WantedBy=default.target
5744
+ `;
5745
+ fs4.writeFileSync(unitPath(), unit);
5746
+ const reload = run2("systemctl", ["--user", "daemon-reload"]);
5747
+ if (!reload.ok) {
5748
+ return {
5749
+ ok: false,
5750
+ detail: `systemd user manager unavailable: ${reload.output}`,
5751
+ manualHint: `Unit written to ${unitPath()}. On a server without a user manager, copy it to /etc/systemd/system/ (sudo), change ExecStart's user if needed, then: sudo systemctl enable --now ${SYSTEMD_UNIT}`
5752
+ };
5753
+ }
5754
+ const enable = run2("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
5755
+ if (!enable.ok) {
5756
+ return { ok: false, detail: `systemctl enable failed: ${enable.output}` };
5757
+ }
5758
+ const linger = run2("loginctl", ["enable-linger", os9.userInfo().username]);
5759
+ return {
5760
+ ok: true,
5761
+ detail: `systemd user unit installed (${unitPath()})` + (linger.ok ? ", lingering enabled" : ` \u2014 enable lingering manually: sudo loginctl enable-linger ${os9.userInfo().username}`)
5762
+ };
5763
+ }
5764
+ function uninstallService() {
5765
+ if (process.platform === "darwin") {
5766
+ const uid = typeof process.getuid === "function" ? process.getuid() : 501;
5767
+ run2("launchctl", ["bootout", `gui/${uid}`, plistPath()]);
5768
+ try {
5769
+ fs4.unlinkSync(plistPath());
5770
+ } catch {
5771
+ }
5772
+ return { ok: true, detail: "LaunchAgent removed" };
5773
+ }
5774
+ if (process.platform === "linux") {
5775
+ run2("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
5776
+ try {
5777
+ fs4.unlinkSync(unitPath());
5778
+ } catch {
5779
+ }
5780
+ run2("systemctl", ["--user", "daemon-reload"]);
5781
+ return { ok: true, detail: "systemd user unit removed" };
5782
+ }
5783
+ return { ok: false, detail: `Unsupported platform: ${process.platform}` };
5784
+ }
5785
+ function serviceStatus() {
5786
+ if (process.platform === "darwin") {
5787
+ const installed = fs4.existsSync(plistPath());
5788
+ const list = run2("launchctl", ["list", SERVICE_LABEL]);
5789
+ const pidMatch = list.output.match(/"PID"\s*=\s*(\d+)/);
5790
+ return {
5791
+ installed,
5792
+ running: list.ok && !!pidMatch,
5793
+ detail: installed ? list.ok ? pidMatch ? `running (pid ${pidMatch[1]})` : "loaded, not running" : "installed, not loaded" : "not installed"
5794
+ };
5795
+ }
5796
+ if (process.platform === "linux") {
5797
+ const installed = fs4.existsSync(unitPath());
5798
+ const active = run2("systemctl", ["--user", "is-active", SYSTEMD_UNIT]);
5799
+ return {
5800
+ installed,
5801
+ running: active.output.trim() === "active",
5802
+ detail: installed ? active.output.trim() || "unknown" : "not installed"
5803
+ };
5804
+ }
5805
+ return { installed: false, running: false, detail: `unsupported platform ${process.platform}` };
5806
+ }
5807
+
5808
+ // src/daemon-cli.ts
5809
+ var c2 = {
5810
+ reset: "\x1B[0m",
5811
+ dim: "\x1B[2m",
5812
+ bold: "\x1B[1m",
5813
+ green: "\x1B[32m",
5814
+ red: "\x1B[31m",
5815
+ yellow: "\x1B[33m",
5816
+ teal: "\x1B[38;5;37m"
5817
+ };
5818
+ function usage() {
5819
+ stdout.write(
5820
+ [
5821
+ "",
5822
+ `${c2.bold}standardcode daemon${c2.reset} \u2014 headless execution client for this machine`,
5823
+ "",
5824
+ `${c2.bold}Commands${c2.reset}`,
5825
+ " install [--endpoint url] Sign in (if needed), name this machine, and install",
5826
+ " the always-on service (launchd / systemd).",
5827
+ " uninstall Stop and remove the service.",
5828
+ " status Service + registry status for this machine.",
5829
+ " run [--endpoint url] Run the daemon in the foreground (what the service runs).",
5830
+ " add-project <path> Register a project directory for remote sessions.",
5831
+ " remove-project <path> Remove a registered project directory.",
5832
+ ""
5833
+ ].join("\n")
5834
+ );
5835
+ }
5836
+ function parseEndpointFlag(args) {
5837
+ const rest = [];
5838
+ let endpoint;
5839
+ for (let i = 0; i < args.length; i++) {
5840
+ const arg = args[i];
5841
+ if (arg === "--endpoint" || arg === "-e") {
5842
+ endpoint = args[++i];
5843
+ } else if (arg.startsWith("--endpoint=")) {
5844
+ endpoint = arg.slice("--endpoint=".length);
5845
+ } else {
5846
+ rest.push(arg);
5847
+ }
5848
+ }
5849
+ return { endpoint, rest };
5850
+ }
5851
+ function resolveEndpoint(flag) {
5852
+ return normalizeEndpoint(
5853
+ flag || process.env.STANDARD_CODE_DAEMON_ENDPOINT || defaultEndpoint() || PRODUCTION_ENDPOINT
5854
+ );
5855
+ }
5856
+ async function ensureSignedIn(endpoint) {
5857
+ relaxTlsForLocalEndpoint(endpoint);
5858
+ const host = endpoint.replace(/^https?:\/\//, "");
5859
+ const stored = getCredential(endpoint);
5860
+ if (stored) {
5861
+ const api2 = new ApiClient(endpoint, stored.access_token);
5862
+ const check2 = await api2.verifyDetailed();
5863
+ if (check2.ok) return api2;
5864
+ stdout.write(`${c2.yellow}Saved sign-in for ${host} failed:${c2.reset} ${check2.reason}
5865
+
5866
+ `);
5867
+ }
5868
+ stdout.write(`${c2.bold}Sign in to Standard Code${c2.reset} ${c2.dim}(${host})${c2.reset}
5869
+
5870
+ `);
5871
+ const token = await deviceLogin(endpoint);
5872
+ const api = new ApiClient(endpoint, token);
5873
+ const check = await api.verifyDetailed();
5874
+ if (!check.ok) throw new Error(`Sign-in didn't verify: ${check.reason}`);
5875
+ saveCredential(
5876
+ { endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
5877
+ { updateDefault: false }
5878
+ );
5879
+ stdout.write(`${c2.green}\u2713${c2.reset} Signed in to ${c2.teal}${host}${c2.reset}
5880
+
5881
+ `);
5882
+ return api;
5883
+ }
5884
+ async function installCommand(endpointFlag) {
5885
+ const endpoint = resolveEndpoint(endpointFlag);
5886
+ const api = await ensureSignedIn(endpoint);
5887
+ const identity = loadMachineIdentity();
5888
+ const rl = readline2.createInterface({ input: stdin, output: stdout });
5889
+ const suggested = machineDisplayName(identity);
5890
+ const answer = (await rl.question(
5891
+ `${c2.bold}Machine name${c2.reset} ${c2.dim}(shown in the session picker)${c2.reset} [${suggested}]: `
5892
+ )).trim();
5893
+ rl.close();
5894
+ if (answer) setMachineName(answer);
5895
+ const named = loadMachineIdentity();
5896
+ await updateOwnMachineRecord(api, named);
5897
+ stdout.write(`${c2.dim}Installing the always-on service\u2026${c2.reset}
5898
+ `);
5899
+ const extra = endpointFlag ? ["--endpoint", endpoint] : [];
5900
+ const result = installService(resolveDaemonCommand(extra), endpointFlag ? endpoint : void 0);
5901
+ if (!result.ok) {
5902
+ stdout.write(`${c2.red}\u2717${c2.reset} ${result.detail}
5903
+ `);
5904
+ if (result.manualHint) stdout.write(`${c2.dim}${result.manualHint}${c2.reset}
5905
+ `);
5906
+ process.exit(1);
5907
+ }
5908
+ stdout.write(`${c2.green}\u2713${c2.reset} ${result.detail}
5909
+ `);
5910
+ stdout.write(`${c2.dim}Waiting for the daemon's first heartbeat\u2026${c2.reset}
5911
+ `);
5912
+ const deadline = Date.now() + 3e4;
5913
+ let alive = false;
5914
+ while (Date.now() < deadline) {
5915
+ await new Promise((r) => setTimeout(r, 2e3));
5916
+ const record = await loadMachine(api, named.machine_id);
5917
+ if (record && daemonOnline(record)) {
5918
+ alive = true;
5919
+ break;
5920
+ }
5921
+ }
5922
+ if (alive) {
5923
+ stdout.write(
5924
+ `${c2.green}\u2713${c2.reset} ${c2.bold}${machineDisplayName(named)}${c2.reset} is online.
5925
+
5926
+ Sessions started elsewhere can now run on this machine.
5927
+ ${c2.dim}Projects register automatically when you run standardcode in a directory here,
5928
+ or add one now: standardcode daemon add-project <path>${c2.reset}
5929
+ `
5930
+ );
5931
+ } else {
5932
+ stdout.write(
5933
+ `${c2.yellow}\u26A0${c2.reset} The service installed but no heartbeat arrived yet.
5934
+ ${c2.dim}Check ~/.standardagents/daemon.log and \`standardcode daemon status\`.${c2.reset}
5935
+ `
5936
+ );
5937
+ }
5938
+ }
5939
+ async function statusCommand() {
5940
+ const status = serviceStatus();
5941
+ const identity = loadMachineIdentity();
5942
+ stdout.write(`${c2.bold}Service:${c2.reset} ${status.detail}
5943
+ `);
5944
+ stdout.write(`${c2.bold}Machine:${c2.reset} ${machineDisplayName(identity)} ${c2.dim}(${identity.machine_id})${c2.reset}
5945
+ `);
5946
+ const endpoint = resolveEndpoint();
5947
+ const cred = getCredential(endpoint);
5948
+ if (!cred) {
5949
+ stdout.write(`${c2.bold}Account:${c2.reset} ${c2.yellow}not signed in to ${endpoint}${c2.reset}
5950
+ `);
5951
+ return;
5952
+ }
5953
+ relaxTlsForLocalEndpoint(endpoint);
5954
+ const api = new ApiClient(endpoint, cred.access_token);
5955
+ const record = await loadMachine(api, identity.machine_id).catch(() => null);
5956
+ if (!record) {
5957
+ stdout.write(`${c2.bold}Registry:${c2.reset} not registered yet
5958
+ `);
5959
+ return;
5960
+ }
5961
+ const online = daemonOnline(record);
5962
+ const seen = record.daemon ? `${Math.round((Date.now() - record.daemon.last_seen_at) / 1e3)}s ago (v${record.daemon.version})` : "never";
5963
+ stdout.write(`${c2.bold}Registry:${c2.reset} ${online ? `${c2.green}online${c2.reset}` : `${c2.yellow}offline${c2.reset}`} \xB7 last heartbeat ${seen}
5964
+ `);
5965
+ const projects = Object.keys(record.projects);
5966
+ stdout.write(`${c2.bold}Projects:${c2.reset} ${projects.length ? "" : c2.dim + "none registered" + c2.reset}
5967
+ `);
5968
+ for (const p of projects.sort()) stdout.write(` ${c2.dim}${p}${c2.reset}
5969
+ `);
5970
+ }
5971
+ async function projectCommand(action, target) {
5972
+ if (!target) {
5973
+ stdout.write(`${c2.red}\u2717${c2.reset} Expected a project path.
5974
+ `);
5975
+ process.exit(1);
5976
+ }
5977
+ const dir = path3.resolve(target);
5978
+ if (action === "add" && !fs4.existsSync(dir)) {
5979
+ stdout.write(`${c2.red}\u2717${c2.reset} ${dir} does not exist on this machine.
5980
+ `);
5981
+ process.exit(1);
5982
+ }
5983
+ const endpoint = resolveEndpoint();
5984
+ const api = await ensureSignedIn(endpoint);
5985
+ const identity = loadMachineIdentity();
5986
+ if (action === "add") {
5987
+ await registerProject(api, identity, dir);
5988
+ stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir} for remote sessions on ${machineDisplayName(identity)}.
5989
+ `);
5990
+ } else {
5991
+ await unregisterProject(api, identity, dir);
5992
+ stdout.write(`${c2.green}\u2713${c2.reset} Removed ${dir} from this machine's projects.
5993
+ `);
5994
+ }
5995
+ }
5996
+ async function runDaemonCommand(argv) {
5997
+ const [command, ...restArgs] = argv;
5998
+ const { endpoint, rest } = parseEndpointFlag(restArgs);
5999
+ switch (command) {
6000
+ case "run":
6001
+ await runDaemon({ endpoint: endpoint || process.env.STANDARD_CODE_DAEMON_ENDPOINT });
6002
+ return;
6003
+ case "install":
6004
+ await installCommand(endpoint);
6005
+ return;
6006
+ case "uninstall": {
6007
+ const result = uninstallService();
6008
+ stdout.write(`${result.ok ? c2.green + "\u2713" : c2.red + "\u2717"}${c2.reset} ${result.detail}
6009
+ `);
6010
+ const ep = resolveEndpoint(endpoint);
6011
+ const cred = getCredential(ep);
6012
+ if (cred) {
6013
+ relaxTlsForLocalEndpoint(ep);
6014
+ await clearDaemon(new ApiClient(ep, cred.access_token), loadMachineIdentity()).catch(() => {
6015
+ });
6016
+ }
6017
+ return;
6018
+ }
6019
+ case "status":
6020
+ await statusCommand();
6021
+ return;
6022
+ case "add-project":
6023
+ await projectCommand("add", rest[0]);
6024
+ return;
6025
+ case "remove-project":
6026
+ await projectCommand("remove", rest[0]);
6027
+ return;
6028
+ case "version":
6029
+ stdout.write(`standardcode daemon v${readVersion()}
6030
+ `);
6031
+ return;
6032
+ default:
6033
+ usage();
6034
+ if (command && command !== "help" && command !== "--help" && command !== "-h") {
6035
+ process.exit(1);
6036
+ }
6037
+ }
6038
+ }
6039
+
4561
6040
  // src/index.ts
4562
- var AGENT_ID = "standard_code_agent";
4563
- var AGENT_ID_VARIANTS = [AGENT_ID, "standard_code_high_agent"];
4564
- var PRODUCTION_ENDPOINT = "https://api.standardcode.ai";
4565
- var c = {
6041
+ var c3 = {
4566
6042
  reset: "\x1B[0m",
4567
6043
  dim: "\x1B[2m",
4568
6044
  bold: "\x1B[1m",
@@ -4591,10 +6067,10 @@ function printUsage() {
4591
6067
  stdout.write(
4592
6068
  [
4593
6069
  "",
4594
- `${c.bold}Usage${c.reset}`,
6070
+ `${c3.bold}Usage${c3.reset}`,
4595
6071
  " standardcode [options] [dir]",
4596
6072
  "",
4597
- `${c.bold}Options${c.reset}`,
6073
+ `${c3.bold}Options${c3.reset}`,
4598
6074
  " -e, --endpoint [url] Use a different Standard Agents instance for this run",
4599
6075
  " (default: https://api.standardcode.ai).",
4600
6076
  " If url is omitted, prompt for it.",
@@ -4652,7 +6128,7 @@ function printAssistant(tui, text) {
4652
6128
  let dotted = false;
4653
6129
  for (const line of renderMarkdown(text, cols2)) {
4654
6130
  if (!dotted && line.trim()) {
4655
- tui.print(`${c.gray}\u2022${c.reset} ${line}`);
6131
+ tui.print(`${c3.gray}\u2022${c3.reset} ${line}`);
4656
6132
  dotted = true;
4657
6133
  } else {
4658
6134
  tui.print(` ${line}`);
@@ -4667,7 +6143,7 @@ function startLoader(label) {
4667
6143
  const draw = () => {
4668
6144
  const now = Date.now();
4669
6145
  const f = frames[Math.floor(now / 70) % frames.length];
4670
- stdout.write(`\r\x1B[K${pad}${brandCycleColor(now)}${f}${c.reset} ${c.dim}${label}\u2026${c.reset}`);
6146
+ stdout.write(`\r\x1B[K${pad}${brandCycleColor(now)}${f}${c3.reset} ${c3.dim}${label}\u2026${c3.reset}`);
4671
6147
  };
4672
6148
  draw();
4673
6149
  const timer = setInterval(draw, 70);
@@ -4683,53 +6159,25 @@ function farewell(stoppedProcs = 0) {
4683
6159
  if (stoppedProcs > 0) {
4684
6160
  stdout.write(
4685
6161
  `
4686
- ${c.cyan}\u2699${c.reset} Stopped ${stoppedProcs} background process${stoppedProcs === 1 ? "" : "es"}.
6162
+ ${c3.cyan}\u2699${c3.reset} Stopped ${stoppedProcs} background process${stoppedProcs === 1 ? "" : "es"}.
4687
6163
  `
4688
6164
  );
4689
6165
  }
4690
6166
  stdout.write(`
4691
- ${c.teal}\u25C7${c.reset} ${c.dim}Standard Code \u2014 see you soon.${c.reset}
6167
+ ${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.reset}
4692
6168
  `);
4693
6169
  }
4694
- function isLocalHost(host) {
4695
- return host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local") || host.endsWith(".localhost") || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
4696
- }
4697
- function relaxTlsForLocalEndpoint(endpoint) {
4698
- let host = "";
4699
- try {
4700
- host = new URL(endpoint).hostname;
4701
- } catch {
4702
- return false;
4703
- }
4704
- if (!endpoint.startsWith("https:") || !isLocalHost(host)) return false;
4705
- const origEmit = process.emitWarning.bind(process);
4706
- process.emitWarning = ((warning, ...args) => {
4707
- const msg = typeof warning === "string" ? warning : warning?.message ?? "";
4708
- if (msg.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
4709
- return origEmit(warning, ...args);
4710
- });
4711
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
4712
- return true;
4713
- }
4714
- function readVersion() {
4715
- try {
4716
- const pkg = JSON.parse(fs4.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
4717
- return typeof pkg.version === "string" ? pkg.version : "";
4718
- } catch {
4719
- return "";
4720
- }
4721
- }
4722
6170
  function printWelcome(endpoint, projectDir) {
4723
- const home = os6.homedir();
6171
+ const home = os9.homedir();
4724
6172
  const dir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
4725
6173
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
4726
6174
  const version = readVersion();
4727
6175
  const pad = " ";
4728
6176
  const meta = [
4729
- `${c.bold}${gradientText("Standard Code")}${c.reset}${version ? ` ${c.dim}v${version}${c.reset}` : ""}`,
4730
- `${c.dim}terminal coding agent${c.reset}`,
4731
- ...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c.teal}${host}${c.reset}`],
4732
- `${c.dim}${dir}${c.reset}`
6177
+ `${c3.bold}${gradientText("Standard Code")}${c3.reset}${version ? ` ${c3.dim}v${version}${c3.reset}` : ""}`,
6178
+ `${c3.dim}terminal coding agent${c3.reset}`,
6179
+ ...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c3.teal}${host}${c3.reset}`],
6180
+ `${c3.dim}${dir}${c3.reset}`
4733
6181
  ];
4734
6182
  const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
4735
6183
  const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
@@ -4744,11 +6192,11 @@ function printWelcome(endpoint, projectDir) {
4744
6192
  }
4745
6193
  function colorActivity(line) {
4746
6194
  const m = line.match(/^(\s*)([✓✗⛔])\s?([\s\S]*)$/);
4747
- if (!m) return `${c.dim}${line}${c.reset}`;
6195
+ if (!m) return `${c3.dim}${line}${c3.reset}`;
4748
6196
  const [, indent, glyph, rest] = m;
4749
6197
  if (glyph === "\u2713") {
4750
- const body = rest.replace(/\s(\([^()]*\))\s*$/, ` ${c.dim}$1${c.reset}`);
4751
- return `${indent}${c.green}\u2713${c.reset} ${body}`;
6198
+ const body = rest.replace(/\s(\([^()]*\))\s*$/, ` ${c3.dim}$1${c3.reset}`);
6199
+ return `${indent}${c3.green}\u2713${c3.reset} ${body}`;
4752
6200
  }
4753
6201
  if (glyph === "\u2717") {
4754
6202
  const ERR_MAX_LINES = 7;
@@ -4756,22 +6204,26 @@ function colorActivity(line) {
4756
6204
  const shown = lines.slice(0, ERR_MAX_LINES);
4757
6205
  const hidden = lines.length - shown.length;
4758
6206
  const body = shown.map(
4759
- (l, i) => i === 0 ? `${indent}${c.red}\u2717 ${l}${c.reset}` : `${indent}${c.red}${c.dim}${l}${c.reset}`
6207
+ (l, i) => i === 0 ? `${indent}${c3.red}\u2717 ${l}${c3.reset}` : `${indent}${c3.red}${c3.dim}${l}${c3.reset}`
4760
6208
  ).join("\n");
4761
6209
  if (hidden > 0) {
4762
6210
  return `${body}
4763
- ${indent}${c.dim}\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}${c.reset}`;
6211
+ ${indent}${c3.dim}\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}${c3.reset}`;
4764
6212
  }
4765
6213
  return body;
4766
6214
  }
4767
- return `${indent}${c.yellow}\u26D4 ${rest}${c.reset}`;
6215
+ return `${indent}${c3.yellow}\u26D4 ${rest}${c3.reset}`;
4768
6216
  }
4769
6217
  async function main() {
6218
+ if (process.argv[2] === "daemon") {
6219
+ await runDaemonCommand(process.argv.slice(3));
6220
+ return;
6221
+ }
4770
6222
  let cliArgs;
4771
6223
  try {
4772
6224
  cliArgs = parseArgs2(process.argv.slice(2));
4773
6225
  } catch (error) {
4774
- stdout.write(`${c.red}error:${c.reset} ${error instanceof Error ? error.message : String(error)}
6226
+ stdout.write(`${c3.red}error:${c3.reset} ${error instanceof Error ? error.message : String(error)}
4775
6227
  `);
4776
6228
  printUsage();
4777
6229
  process.exit(1);
@@ -4784,7 +6236,7 @@ async function main() {
4784
6236
  const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
4785
6237
  const dirArg = cliArgs.dir;
4786
6238
  const projectDir = path3.resolve(dirArg || process.cwd());
4787
- const machine = os6.hostname();
6239
+ const machine = os9.hostname();
4788
6240
  const reader = { rl: null };
4789
6241
  let handoffClosing = false;
4790
6242
  let preflightArmed = false;
@@ -4798,7 +6250,7 @@ async function main() {
4798
6250
  }
4799
6251
  preflightArmed = true;
4800
6252
  stdout.write(`
4801
- ${c.dim}Press Control-C again to exit${c.reset}
6253
+ ${c3.dim}Press Control-C again to exit${c3.reset}
4802
6254
  `);
4803
6255
  preflightTimer = setTimeout(() => {
4804
6256
  preflightArmed = false;
@@ -4820,10 +6272,10 @@ ${c.dim}Press Control-C again to exit${c.reset}
4820
6272
  const askEndpoint = async () => {
4821
6273
  for (; ; ) {
4822
6274
  const answer = (await ask(
4823
- `${c.cyan}Standard Agents instance URL${c.reset} (e.g. http://localhost:5178): `
6275
+ `${c3.cyan}Standard Agents instance URL${c3.reset} (e.g. http://localhost:5178): `
4824
6276
  )).trim();
4825
6277
  if (answer) return answer;
4826
- stdout.write(`${c.dim}An endpoint URL is required.${c.reset}
6278
+ stdout.write(`${c3.dim}An endpoint URL is required.${c3.reset}
4827
6279
  `);
4828
6280
  }
4829
6281
  };
@@ -4838,7 +6290,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
4838
6290
  const tlsRelaxed = relaxTlsForLocalEndpoint(endpoint);
4839
6291
  printWelcome(endpoint, projectDir);
4840
6292
  if (tlsRelaxed) {
4841
- stdout.write(`${c.dim} TLS verification relaxed for local endpoint.${c.reset}
6293
+ stdout.write(`${c3.dim} TLS verification relaxed for local endpoint.${c3.reset}
4842
6294
 
4843
6295
  `);
4844
6296
  }
@@ -4850,7 +6302,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
4850
6302
  loading.stop();
4851
6303
  const applied = consumeAppliedUpdate(version);
4852
6304
  if (applied) {
4853
- stdout.write(` ${c.green}\u2713${c.reset} ${c.dim}Standard Code updated to v${version}.${c.reset}
6305
+ stdout.write(` ${c3.green}\u2713${c3.reset} ${c3.dim}Standard Code updated to v${version}.${c3.reset}
4854
6306
 
4855
6307
  `);
4856
6308
  }
@@ -4859,21 +6311,21 @@ ${c.dim}Press Control-C again to exit${c.reset}
4859
6311
  const decision = decideAutoUpdate(updateAvailable, { state: readAutoUpdateState(), pm });
4860
6312
  if (decision === "start" && pm && startBackgroundUpdate(updateAvailable.latest, pm)) {
4861
6313
  stdout.write(
4862
- ` ${c.teal}\u27F3${c.reset} ${c.dim}Standard Code ${c.reset}${c.bold}v${updateAvailable.latest}${c.reset}${c.dim} is installing in the background \u2014 it applies on your next launch.${c.reset}
6314
+ ` ${c3.teal}\u27F3${c3.reset} ${c3.dim}Standard Code ${c3.reset}${c3.bold}v${updateAvailable.latest}${c3.reset}${c3.dim} is installing in the background \u2014 it applies on your next launch.${c3.reset}
4863
6315
 
4864
6316
  `
4865
6317
  );
4866
6318
  } else if (decision === "in_flight") {
4867
6319
  stdout.write(
4868
- ` ${c.teal}\u27F3${c.reset} ${c.dim}Standard Code v${updateAvailable.latest} is still installing in the background.${c.reset}
6320
+ ` ${c3.teal}\u27F3${c3.reset} ${c3.dim}Standard Code v${updateAvailable.latest} is still installing in the background.${c3.reset}
4869
6321
 
4870
6322
  `
4871
6323
  );
4872
6324
  } else {
4873
6325
  const display = updateCommand(pm ?? "npm").display;
4874
6326
  stdout.write(
4875
- ` ${c.teal}\u25C7${c.reset} ${c.dim}Update available:${c.reset} ${c.dim}v${updateAvailable.current}${c.reset} \u2192 ${c.bold}v${updateAvailable.latest}${c.reset}
4876
- ${c.dim}Run ${c.reset}${c.bold}${display}${c.reset}${c.dim} to update${c.reset}
6327
+ ` ${c3.teal}\u25C7${c3.reset} ${c3.dim}Update available:${c3.reset} ${c3.dim}v${updateAvailable.current}${c3.reset} \u2192 ${c3.bold}v${updateAvailable.latest}${c3.reset}
6328
+ ${c3.dim}Run ${c3.reset}${c3.bold}${display}${c3.reset}${c3.dim} to update${c3.reset}
4877
6329
 
4878
6330
  `
4879
6331
  );
@@ -4891,39 +6343,39 @@ ${c.dim}Press Control-C again to exit${c.reset}
4891
6343
  if (!api || !storedCheck?.ok) {
4892
6344
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
4893
6345
  if (storedCheck && !storedCheck.ok) {
4894
- stdout.write(`${c.red}\u2717${c.reset} ${c.dim}Saved sign-in for this endpoint failed:${c.reset} ${storedCheck.reason}
6346
+ stdout.write(`${c3.red}\u2717${c3.reset} ${c3.dim}Saved sign-in for this endpoint failed:${c3.reset} ${storedCheck.reason}
4895
6347
  `);
4896
- if (storedCheck.hint) stdout.write(` ${c.dim}${storedCheck.hint}${c.reset}
6348
+ if (storedCheck.hint) stdout.write(` ${c3.dim}${storedCheck.hint}${c3.reset}
4897
6349
  `);
4898
6350
  stdout.write("\n");
4899
6351
  }
4900
6352
  const explainFailure = (result, prefix) => {
4901
- stdout.write(`${c.red}\u2717${c.reset} ${prefix}${result.reason}
6353
+ stdout.write(`${c3.red}\u2717${c3.reset} ${prefix}${result.reason}
4902
6354
  `);
4903
- if (result.hint) stdout.write(` ${c.dim}${result.hint}${c.reset}
6355
+ if (result.hint) stdout.write(` ${c3.dim}${result.hint}${c3.reset}
4904
6356
  `);
4905
6357
  };
4906
- stdout.write(`${c.bold}${gradientText("Sign in to Standard Code")}${c.reset}
6358
+ stdout.write(`${c3.bold}${gradientText("Sign in to Standard Code")}${c3.reset}
4907
6359
  `);
4908
6360
  if (`https://${host}` !== PRODUCTION_ENDPOINT) {
4909
- stdout.write(`${c.dim}Connecting to${c.reset} ${c.teal}${host}${c.reset}
6361
+ stdout.write(`${c3.dim}Connecting to${c3.reset} ${c3.teal}${host}${c3.reset}
4910
6362
  `);
4911
6363
  }
4912
6364
  stdout.write(
4913
- `${c.dim}You'll only need to do this once on this machine.${c.reset}
6365
+ `${c3.dim}You'll only need to do this once on this machine.${c3.reset}
4914
6366
 
4915
6367
  `
4916
6368
  );
4917
6369
  stdout.write(
4918
- `${c.white}Press ${c.bold}Enter${c.reset}${c.white} to open your browser and sign in.${c.reset} ${c.dim}(or paste an API token)${c.reset}
6370
+ `${c3.white}Press ${c3.bold}Enter${c3.reset}${c3.white} to open your browser and sign in.${c3.reset} ${c3.dim}(or paste an API token)${c3.reset}
4919
6371
 
4920
6372
  `
4921
6373
  );
4922
6374
  for (; ; ) {
4923
- const token = (await ask(`${c.teal}\u276F${c.reset} `)).trim();
6375
+ const token = (await ask(`${c3.teal}\u276F${c3.reset} `)).trim();
4924
6376
  if (!token) {
4925
6377
  const got = await deviceLogin(endpoint).catch((e) => {
4926
- stdout.write(`${c.red}\u2717${c.reset} ${c.dim}${e instanceof Error ? e.message : String(e)}${c.reset}
6378
+ stdout.write(`${c3.red}\u2717${c3.reset} ${c3.dim}${e instanceof Error ? e.message : String(e)}${c3.reset}
4927
6379
  `);
4928
6380
  return null;
4929
6381
  });
@@ -4937,7 +6389,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
4937
6389
  { endpoint, access_token: got, token_type: "Bearer", saved_at: Date.now() },
4938
6390
  { updateDefault: !endpointOverride }
4939
6391
  );
4940
- stdout.write(`${c.green}\u2713${c.reset} Connected to ${c.teal}${host}${c.reset}
6392
+ stdout.write(`${c3.green}\u2713${c3.reset} Connected to ${c3.teal}${host}${c3.reset}
4941
6393
  `);
4942
6394
  break;
4943
6395
  }
@@ -4953,7 +6405,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
4953
6405
  { endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
4954
6406
  { updateDefault: !endpointOverride }
4955
6407
  );
4956
- stdout.write(`${c.green}\u2713${c.reset} Connected to ${c.teal}${host}${c.reset}
6408
+ stdout.write(`${c3.green}\u2713${c3.reset} Connected to ${c3.teal}${host}${c3.reset}
4957
6409
  `);
4958
6410
  break;
4959
6411
  }
@@ -4965,17 +6417,72 @@ ${c.dim}Press Control-C again to exit${c.reset}
4965
6417
  if (!api) process.exit(1);
4966
6418
  handoffClosing = true;
4967
6419
  reader.rl?.close();
4968
- const tags = [`path:${projectDir}`, `machine:${machine}`];
6420
+ const identity = loadMachineIdentity();
6421
+ void registerProject(api, identity, projectDir).catch(() => {
6422
+ });
6423
+ const tui = new Tui(1);
6424
+ const home = os9.homedir();
6425
+ const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
6426
+ const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
6427
+ const session = { mode: "local", identity };
6428
+ {
6429
+ const loadingMachines = startLoader("Checking your machines");
6430
+ const machines = await loadMachines(api).catch(() => []);
6431
+ loadingMachines.stop();
6432
+ session.suggestDaemonInstall = machines.every((m) => !m.daemon);
6433
+ const remoteTargets = machines.filter((m) => m.id !== identity.machine_id && daemonOnline(m));
6434
+ if (remoteTargets.length > 0) {
6435
+ const where = await tui.select(
6436
+ `${c3.bold}${gradientText("Where should this session run?")}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
6437
+ [
6438
+ { label: `This machine \u2014 ${shortDir}`, hint: "tools run locally", value: null },
6439
+ ...remoteTargets.map((m) => ({
6440
+ label: m.name,
6441
+ hint: `${m.hostname} \xB7 daemon online${m.daemon ? ` \xB7 v${m.daemon.version}` : ""}`,
6442
+ value: m
6443
+ }))
6444
+ ]
6445
+ );
6446
+ if (where) {
6447
+ const remotePath = await pickRemoteProject(tui, where);
6448
+ if (remotePath) {
6449
+ session.mode = "remote";
6450
+ session.runner = where;
6451
+ session.remotePath = remotePath;
6452
+ }
6453
+ }
6454
+ }
6455
+ }
6456
+ let tags;
6457
+ let resumeTags;
6458
+ if (session.mode === "remote" && session.runner && session.remotePath) {
6459
+ tags = [
6460
+ `path:${session.remotePath}`,
6461
+ `machine:${session.runner.hostname || session.runner.name}`,
6462
+ `runner:${session.runner.id}`
6463
+ ];
6464
+ resumeTags = [`path:${session.remotePath}`, `runner:${session.runner.id}`];
6465
+ } else {
6466
+ tags = [`path:${projectDir}`, `machine:${machine}`, `runner:${identity.machine_id}`];
6467
+ resumeTags = [`path:${projectDir}`, `machine:${machine}`];
6468
+ }
4969
6469
  const loadingSessions = startLoader("Loading sessions");
4970
6470
  let existing = [];
4971
6471
  try {
4972
- existing = await api.listThreads(AGENT_ID_VARIANTS, tags);
6472
+ existing = await api.listThreads(AGENT_ID_VARIANTS, resumeTags);
4973
6473
  } catch {
4974
6474
  existing = [];
4975
6475
  }
4976
6476
  const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
4977
6477
  loadingSessions.stop();
4978
- const tui = new Tui(1);
6478
+ const createSessionThread = async () => {
6479
+ const id = await api.createThread(AGENT_ID, tags);
6480
+ if (session.mode === "remote" && session.runner && session.remotePath) {
6481
+ await api.kvSet(id, "session_info", { cwd: session.remotePath, machine: session.runner.name }).catch(() => {
6482
+ });
6483
+ }
6484
+ return id;
6485
+ };
4979
6486
  let threadId;
4980
6487
  let resumed = false;
4981
6488
  let historySeed;
@@ -4986,31 +6493,61 @@ ${c.dim}Press Control-C again to exit${c.reset}
4986
6493
  value: s.id
4987
6494
  }));
4988
6495
  items.push({ label: "\uFF0B Start a new session", value: null });
4989
- const home = os6.homedir();
4990
- const tilde = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
4991
- const shortDir = tilde.length > 38 ? "\u2026" + tilde.slice(-37) : tilde;
6496
+ const whereLabel = session.mode === "remote" && session.runner ? `${session.runner.name}:${shortenPath(session.remotePath ?? "")}` : shortDir;
4992
6497
  const picked = await tui.select(
4993
- `${c.bold}${gradientText("Resume a session")}${c.reset} ${c.gray}${shortDir}${c.reset} ${c.dim}\u2191\u2193 \xB7 enter \xB7 esc${c.reset}`,
6498
+ `${c3.bold}${gradientText("Resume a session")}${c3.reset} ${c3.gray}${whereLabel}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
4994
6499
  items
4995
6500
  );
4996
6501
  if (typeof picked === "string") {
4997
6502
  threadId = picked;
4998
6503
  resumed = true;
4999
6504
  } else {
5000
- threadId = await api.createThread(AGENT_ID, tags);
6505
+ threadId = await createSessionThread();
5001
6506
  historySeed = existing[0]?.id;
5002
6507
  }
5003
6508
  } else {
5004
- threadId = await api.createThread(AGENT_ID, tags);
6509
+ threadId = await createSessionThread();
5005
6510
  }
5006
6511
  for (; ; ) {
5007
- await runInteractive(tui, api, threadId, projectDir, machine, resumed, historySeed);
6512
+ await runInteractive(tui, api, threadId, projectDir, machine, resumed, session, historySeed);
5008
6513
  historySeed = threadId;
5009
- threadId = await api.createThread(AGENT_ID, tags);
6514
+ threadId = await createSessionThread();
5010
6515
  await api.kvSet(threadId, "lease_supersedes", historySeed);
5011
6516
  resumed = false;
5012
6517
  }
5013
6518
  }
6519
+ function shortenPath(p, max = 38) {
6520
+ return p.length > max ? "\u2026" + p.slice(-(max - 1)) : p;
6521
+ }
6522
+ async function pickRemoteProject(tui, runner) {
6523
+ const ENTER_PATH = "__enter_path__";
6524
+ const projects = Object.entries(runner.projects).sort(
6525
+ (a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
6526
+ );
6527
+ const items = projects.map(([dir, p]) => ({
6528
+ label: shortenPath(dir, 48),
6529
+ hint: p?.last_used_at ? relativeTime(p.last_used_at / 1e3) : "",
6530
+ value: dir
6531
+ }));
6532
+ items.push({ label: `\uFF0B Another path on ${runner.name}\u2026`, hint: "type a directory", value: ENTER_PATH });
6533
+ const picked = await tui.select(
6534
+ `${c3.bold}${gradientText(`Project on ${runner.name}`)}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
6535
+ items
6536
+ );
6537
+ if (!picked) return null;
6538
+ if (picked !== ENTER_PATH) return picked;
6539
+ const typed = await tui.prompt(
6540
+ `Directory on ${runner.name} (absolute, created if missing)`,
6541
+ "~/projects/my-app"
6542
+ );
6543
+ if (!typed) return null;
6544
+ const trimmed = typed.trim();
6545
+ if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
6546
+ tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
6547
+ return null;
6548
+ }
6549
+ return trimmed;
6550
+ }
5014
6551
  function isSilentMessage(m) {
5015
6552
  return m?.silent === true || m?.metadata?.silent === true;
5016
6553
  }
@@ -5033,46 +6570,10 @@ async function summarizeThreads(api, threads) {
5033
6570
  }
5034
6571
  function subagentLabel(s, titles) {
5035
6572
  const agentName = (s.agent_name || "").trim();
5036
- const title = (s.title || "").trim() || titles.get(agentName) || (agentName ? agentName.replace(/[_-]+/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase()) : "Subagent");
6573
+ const title = (s.title || "").trim() || titles.get(agentName) || (agentName ? agentName.replace(/[_-]+/g, " ").replace(/\b\w/g, (c4) => c4.toUpperCase()) : "Subagent");
5037
6574
  const tagged = (s.threadName || "").trim();
5038
6575
  return tagged ? `${title} \xB7 ${tagged}` : title;
5039
6576
  }
5040
- async function deviceLogin(endpoint) {
5041
- const start = await fetch(`${endpoint}/api/auth/device/start`, { method: "POST" });
5042
- if (!start.ok) throw new Error(`This instance does not support browser sign-in (HTTP ${start.status}). Paste an API token instead.`);
5043
- const info = await start.json();
5044
- stdout.write(`${c.dim}Opening your browser to sign in. If it doesn't open, visit:${c.reset}
5045
- `);
5046
- stdout.write(`
5047
- ${c.teal}${info.verify_url}${c.reset}
5048
-
5049
- `);
5050
- stdout.write(`${c.dim}Waiting for sign-in to complete\u2026 (Ctrl-C to cancel)${c.reset}
5051
- `);
5052
- openUrl(info.verify_url);
5053
- const deadline = Date.now() + (info.expires_in ?? 600) * 1e3;
5054
- const interval = Math.max(2, info.interval ?? 2) * 1e3;
5055
- while (Date.now() < deadline) {
5056
- await new Promise((r) => setTimeout(r, interval));
5057
- const res = await fetch(info.poll_url).catch(() => null);
5058
- if (!res) continue;
5059
- if (res.status === 404) throw new Error("The sign-in link expired. Try again.");
5060
- const body = await res.json().catch(() => ({}));
5061
- if (body.status === "approved" && body.token) return body.token;
5062
- if (body.status === "denied") throw new Error("Sign-in was denied in the browser.");
5063
- }
5064
- throw new Error("Timed out waiting for browser approval. Try again.");
5065
- }
5066
- function openUrl(url) {
5067
- const platform = process.platform;
5068
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
5069
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
5070
- try {
5071
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
5072
- child.unref();
5073
- } catch {
5074
- }
5075
- }
5076
6577
  function relativeTime(unixSeconds) {
5077
6578
  const diff = Date.now() / 1e3 - unixSeconds;
5078
6579
  if (diff < 60) return "just now";
@@ -5115,8 +6616,8 @@ async function printHistory(api, threadId, tui) {
5115
6616
  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));
5116
6617
  if (!convo.length) return;
5117
6618
  const shown = convo.slice(-24);
5118
- tui.print(`${c.dim}\u2500\u2500 resuming session \xB7 ${convo.length} message${convo.length === 1 ? "" : "s"} \u2500\u2500${c.reset}`);
5119
- if (shown.length < convo.length) tui.print(`${c.dim} \u2026 earlier messages omitted${c.reset}`);
6619
+ tui.print(`${c3.dim}\u2500\u2500 resuming session \xB7 ${convo.length} message${convo.length === 1 ? "" : "s"} \u2500\u2500${c3.reset}`);
6620
+ if (shown.length < convo.length) tui.print(`${c3.dim} \u2026 earlier messages omitted${c3.reset}`);
5120
6621
  for (const m of shown) {
5121
6622
  const text = messageText(m.content).trim();
5122
6623
  if (!text) continue;
@@ -5124,12 +6625,16 @@ async function printHistory(api, threadId, tui) {
5124
6625
  else printAssistant(tui, text);
5125
6626
  }
5126
6627
  }
5127
- async function runInteractive(tui, api, threadId, projectDir, machine, resumed, historySeedThreadId) {
6628
+ async function runInteractive(tui, api, threadId, projectDir, machine, resumed, session, historySeedThreadId) {
6629
+ const remote = session.mode === "remote";
6630
+ const runnerName = session.runner?.name ?? "the remote machine";
5128
6631
  const registry = new ProcessRegistry(api, threadId, machine);
5129
- const mcp = new McpManager(projectDir);
5130
- const publishMcpCatalog = () => void api.kvSet(threadId, "mcp_catalog", mcp.catalog()).catch(() => {
5131
- });
5132
- const host = new HostTools(projectDir, registry, threadId, machine, mcp, publishMcpCatalog, api);
6632
+ const mcp = remote ? null : new McpManager(projectDir);
6633
+ const publishMcpCatalog = () => {
6634
+ if (mcp) void api.kvSet(threadId, "mcp_catalog", mcp.catalog()).catch(() => {
6635
+ });
6636
+ };
6637
+ const host = new HostTools(projectDir, registry, threadId, machine, mcp ?? void 0, publishMcpCatalog, api);
5133
6638
  const refreshBgCount = () => {
5134
6639
  void registry.runningCount().then((n) => tui.setBackgroundCount(n)).catch(() => {
5135
6640
  });
@@ -5147,8 +6652,6 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
5147
6652
  saveApprovals(api, threadId, perm);
5148
6653
  });
5149
6654
  saveApprovals(api, threadId, perm);
5150
- void api.kvSet(threadId, "session_info", { cwd: projectDir, machine }).catch(() => {
5151
- });
5152
6655
  const attaching = startLoader("Attaching to thread");
5153
6656
  let busy = false;
5154
6657
  let interrupting = false;
@@ -5169,42 +6672,76 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
5169
6672
  for (const v of activeSteps.values()) label = v;
5170
6673
  tui.setStep(label, liveOut);
5171
6674
  };
5172
- const bridge = new Bridge(api, threadId, host, perm, {
5173
- onActivity: (line, detail) => {
5174
- tui.print(colorActivity(line));
5175
- if (detail) for (const d of detail) tui.print(d);
5176
- refreshBgCount();
5177
- },
5178
- onStatus: (id, summary) => {
5179
- if (summary) {
5180
- if (!activeSteps.has(id)) activeSteps.set(id, summary);
5181
- } else {
5182
- activeSteps.delete(id);
5183
- }
5184
- refreshStatus();
5185
- },
5186
- onConnection: (state, attempt) => {
5187
- if (state === "reconnecting") {
5188
- if (attempt >= 4) tui.setConnected(false);
5189
- } else {
5190
- tui.setConnected(true);
5191
- }
5192
- },
5193
- requestApproval: async (req, summary, risk) => {
5194
- const choice = await tui.approval(
5195
- `${summary}${req.requestPermission ? `
5196
- ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5197
- risk
5198
- );
5199
- if (choice === "deny_with_reason") {
5200
- const reason = await tui.prompt(
5201
- "Why are you denying this? (sent to the agent \u2014 enter to send, esc to skip)"
6675
+ let claim = "if_stale";
6676
+ if (!remote) {
6677
+ const ownerRec = await api.kvGet(threadId, "execution_owner").catch(() => null);
6678
+ const ownerDaemonMachine = machineIdFromDaemonClientId(ownerRec?.client_id);
6679
+ if (ownerDaemonMachine === session.identity.machine_id) claim = "takeover";
6680
+ }
6681
+ const bridge = remote ? null : new Bridge(
6682
+ api,
6683
+ threadId,
6684
+ host,
6685
+ perm,
6686
+ {
6687
+ onActivity: (line, detail) => {
6688
+ tui.print(colorActivity(line));
6689
+ if (detail) for (const d of detail) tui.print(d);
6690
+ refreshBgCount();
6691
+ },
6692
+ onOwnership: (isOwner, owner) => {
6693
+ if (isOwner) {
6694
+ bridge?.setClaim("takeover");
6695
+ void api.kvSet(threadId, "session_info", { cwd: projectDir, machine }).catch(() => {
6696
+ });
6697
+ } else {
6698
+ tui.print(
6699
+ `${c3.yellow}\u26A0 Another client${owner?.client_name ? ` (${owner.client_name})` : ""} is executing tools for this session \u2014 this terminal is watching.${c3.reset}`
6700
+ );
6701
+ }
6702
+ },
6703
+ onClaimRefused: (reason) => {
6704
+ if (reason === "in_flight") {
6705
+ tui.print(
6706
+ `${c3.yellow}\u26A0 The session's current client is mid-operation \u2014 execution can't move here until it finishes. Watching for now.${c3.reset}`
6707
+ );
6708
+ }
6709
+ },
6710
+ onSuperseded: () => {
6711
+ tui.print(
6712
+ `${c3.yellow}\u26A0 Another Standard Code process on this machine took over this session \u2014 this terminal is watching.${c3.reset}`
6713
+ );
6714
+ },
6715
+ onStatus: (id, summary) => {
6716
+ if (summary) {
6717
+ if (!activeSteps.has(id)) activeSteps.set(id, summary);
6718
+ } else {
6719
+ activeSteps.delete(id);
6720
+ }
6721
+ refreshStatus();
6722
+ },
6723
+ onConnection: (state, attempt) => {
6724
+ if (state === "reconnecting") {
6725
+ if (attempt >= 4) tui.setConnected(false);
6726
+ } else {
6727
+ tui.setConnected(true);
6728
+ }
6729
+ },
6730
+ requestApproval: async (req, summary, risk) => {
6731
+ return tui.approval(
6732
+ `${summary}${req.requestPermission ? `
6733
+ why: ${req.requestPermission}` : ""}`,
6734
+ risk
5202
6735
  );
5203
- return { choice: "deny", reason: reason ?? void 0 };
5204
6736
  }
5205
- return { choice };
6737
+ },
6738
+ {
6739
+ clientId: interactiveClientId(session.identity),
6740
+ clientName: `${machineDisplayName(session.identity)} (terminal)`,
6741
+ clientKind: "interactive",
6742
+ claim
5206
6743
  }
5207
- });
6744
+ );
5208
6745
  const stream = new MessageStream(api, threadId, {
5209
6746
  // Live streaming preview: answer text and (opt-in) internal reasoning feed
5210
6747
  // the TUI's ephemeral preview; the committed message still renders from
@@ -5301,14 +6838,14 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5301
6838
  const sessionEnded = new Promise((r) => endSession = r);
5302
6839
  const quit = async () => {
5303
6840
  tui.end();
5304
- const stopped2 = api.stop(threadId).catch(() => {
5305
- });
6841
+ const stopped2 = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
6842
+ }) : Promise.resolve();
5306
6843
  const procsStopped2 = host.stopAllLocalProcesses().catch(() => 0);
5307
- bridge.close();
6844
+ bridge?.close();
5308
6845
  stream.close();
5309
6846
  events.close();
5310
6847
  subActivity.closeAll();
5311
- mcp.closeAll();
6848
+ mcp?.closeAll();
5312
6849
  const [, killed2] = await Promise.race([
5313
6850
  Promise.all([stopped2, procsStopped2]),
5314
6851
  new Promise((r) => setTimeout(() => r([void 0, 0]), 1500))
@@ -5320,21 +6857,28 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5320
6857
  const logout = async () => {
5321
6858
  deleteCredential(api.origin);
5322
6859
  const instanceHost = api.origin.replace(/^https?:\/\//, "");
5323
- tui.print(`${c.gray}Signed out \u2014 removed the saved token for ${c.teal}${instanceHost}${c.reset}${c.gray}. Run standardcode to sign in again.${c.reset}`);
6860
+ tui.print(`${c3.gray}Signed out \u2014 removed the saved token for ${c3.teal}${instanceHost}${c3.reset}${c3.gray}. Run standardcode to sign in again.${c3.reset}`);
5324
6861
  await quit();
5325
6862
  };
5326
6863
  const bgMgr = {
5327
6864
  list: () => registry.list(),
5328
6865
  stop: async (id) => {
6866
+ if (remote) {
6867
+ tui.print(
6868
+ `${c3.gray}That process runs on ${runnerName} \u2014 ask the agent to stop it (it manages processes there).${c3.reset}`
6869
+ );
6870
+ return;
6871
+ }
5329
6872
  await host.execute("background_process", { action: "stop", id });
5330
6873
  refreshBgCount();
5331
6874
  }
5332
6875
  };
5333
6876
  const mcpCtl = {
5334
6877
  configured: () => listMcpServers(),
5335
- connectedNames: () => mcp.connectedNames(),
5336
- catalog: () => mcp.catalog(),
6878
+ connectedNames: () => mcp?.connectedNames() ?? [],
6879
+ catalog: () => mcp?.catalog() ?? { servers: [] },
5337
6880
  connect: async (cfg) => {
6881
+ if (!mcp) return { ok: false, error: "MCP servers run on the session's machine." };
5338
6882
  try {
5339
6883
  const client = await mcp.connect(cfg);
5340
6884
  publishMcpCatalog();
@@ -5345,7 +6889,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5345
6889
  }
5346
6890
  },
5347
6891
  disconnect: (name) => {
5348
- mcp.disconnect(name);
6892
+ mcp?.disconnect(name);
5349
6893
  publishMcpCatalog();
5350
6894
  },
5351
6895
  add: async (cfg) => {
@@ -5361,7 +6905,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5361
6905
  );
5362
6906
  },
5363
6907
  remove: (name) => {
5364
- mcp.disconnect(name);
6908
+ mcp?.disconnect(name);
5365
6909
  removeMcpServer(name);
5366
6910
  publishMcpCatalog();
5367
6911
  },
@@ -5380,7 +6924,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5380
6924
  const n = (pendingSent.get(key) ?? 1) - 1;
5381
6925
  if (n > 0) pendingSent.set(key, n);
5382
6926
  else pendingSent.delete(key);
5383
- tui.print(`${c.dim}failed to send: ${e instanceof Error ? e.message : String(e)}${c.reset}`);
6927
+ tui.print(`${c3.dim}failed to send: ${e instanceof Error ? e.message : String(e)}${c3.reset}`);
5384
6928
  return;
5385
6929
  }
5386
6930
  interrupting = false;
@@ -5397,16 +6941,16 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5397
6941
  try {
5398
6942
  await api.compact(threadId);
5399
6943
  } catch (err) {
5400
- tui.print(`${c.red}\u2717${c.reset} couldn't start compaction: ${err.message}`);
6944
+ tui.print(`${c3.red}\u2717${c3.reset} couldn't start compaction: ${err.message}`);
5401
6945
  }
5402
6946
  };
5403
6947
  const runAccountCommand = async () => {
5404
- tui.print(`${c.gray}Opening your account\u2026${c.reset}`);
6948
+ tui.print(`${c3.gray}Opening your account\u2026${c3.reset}`);
5405
6949
  const link = await api.accountLink(threadId).catch(() => null);
5406
6950
  const target = link?.url ?? "https://standardcode.ai/account";
5407
6951
  openUrl(target);
5408
6952
  tui.print(
5409
- link?.preauthed ? `${c.gray}\u2192 account dashboard opened in your browser (signed in)${c.reset}` : `${c.gray}\u2192 opened ${target} \u2014 sign in with your account email${c.reset}`
6953
+ link?.preauthed ? `${c3.gray}\u2192 account dashboard opened in your browser (signed in)${c3.reset}` : `${c3.gray}\u2192 opened ${target} \u2014 sign in with your account email${c3.reset}`
5410
6954
  );
5411
6955
  };
5412
6956
  const ordinal = (n) => {
@@ -5417,24 +6961,24 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5417
6961
  const renderUpgradePanel = (q) => {
5418
6962
  const dots = [];
5419
6963
  for (let i = 0; i < q.max; i++) {
5420
- if (i < q.current) dots.push(`${c.teal}\u25CF${c.reset}`);
5421
- else if (i === q.current) dots.push(`${c.bold}${gradientText("\uFF0B")}${c.reset}`);
5422
- else dots.push(`${c.dim}\xB7${c.reset}`);
6964
+ if (i < q.current) dots.push(`${c3.teal}\u25CF${c3.reset}`);
6965
+ else if (i === q.current) dots.push(`${c3.bold}${gradientText("\uFF0B")}${c3.reset}`);
6966
+ else dots.push(`${c3.dim}\xB7${c3.reset}`);
5423
6967
  }
5424
6968
  const cost = fmtCost(q);
5425
6969
  const lines = [
5426
6970
  "",
5427
- `${c.bold}${gradientText("\u2726 Add a parallel session")}${c.reset}`,
6971
+ `${c3.bold}${gradientText("\u2726 Add a parallel session")}${c3.reset}`,
5428
6972
  "",
5429
- `${dots.join(" ")} ${c.dim}${q.current} of ${q.current} session${q.current === 1 ? "" : "s"} in use${c.reset}`
6973
+ `${dots.join(" ")} ${c3.dim}${q.current} of ${q.current} session${q.current === 1 ? "" : "s"} in use${c3.reset}`
5430
6974
  ];
5431
6975
  if (q.ends_trial) {
5432
6976
  lines.push(
5433
- `${c.yellow}Your $5 trial covers 1 session. Adding a ${ordinal(q.sessions)} ends the trial${c.reset}`,
5434
- `${c.yellow}and starts your $49/mo plan now${cost ? ` \u2014 ${c.bold}${cost} charged today${c.reset}${c.yellow}` : ""}.${c.reset}`
6977
+ `${c3.yellow}Your $5 trial covers 1 session. Adding a ${ordinal(q.sessions)} ends the trial${c3.reset}`,
6978
+ `${c3.yellow}and starts your $49/mo plan now${cost ? ` \u2014 ${c3.bold}${cost} charged today${c3.reset}${c3.yellow}` : ""}.${c3.reset}`
5435
6979
  );
5436
6980
  } else if (cost) {
5437
- lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 ${c.bold}${cost} charged now${c.reset}.`);
6981
+ lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 ${c3.bold}${cost} charged now${c3.reset}.`);
5438
6982
  } else {
5439
6983
  lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 prorated on your next invoice.`);
5440
6984
  }
@@ -5447,7 +6991,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5447
6991
  try {
5448
6992
  if (opts.auto) {
5449
6993
  tui.print(
5450
- `${c.yellow}You're out of simultaneous sessions \u2014 another Standard Code session is using your slot.${c.reset}`
6994
+ `${c3.yellow}You're out of simultaneous sessions \u2014 another Standard Code session is using your slot.${c3.reset}`
5451
6995
  );
5452
6996
  }
5453
6997
  const quote = await api.sessionsQuote(threadId);
@@ -5455,16 +6999,16 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5455
6999
  const link = await api.accountLink(threadId).catch(() => null);
5456
7000
  const target = link?.url ?? "https://standardcode.ai/account";
5457
7001
  tui.print(
5458
- `${c.gray}Close the other session (its slot frees within ~90s) \u2014 or add another simultaneous session to your plan, then resend your message.${c.reset}`
7002
+ `${c3.gray}Close the other session (its slot frees within ~90s) \u2014 or add another simultaneous session to your plan, then resend your message.${c3.reset}`
5459
7003
  );
5460
7004
  openUrl(target);
5461
- tui.print(`${c.gray}\u2192 opened ${target} to manage your plan${c.reset}`);
7005
+ tui.print(`${c3.gray}\u2192 opened ${target} to manage your plan${c3.reset}`);
5462
7006
  return;
5463
7007
  }
5464
7008
  if (quote.current >= quote.max) {
5465
7009
  tui.print(
5466
- `${c.yellow}You're at the maximum of ${quote.max} parallel session${quote.max === 1 ? "" : "s"}.${c.reset}
5467
- ${c.gray}Close another session (its slot frees within ~90s), then resend your message.${c.reset}`
7010
+ `${c3.yellow}You're at the maximum of ${quote.max} parallel session${quote.max === 1 ? "" : "s"}.${c3.reset}
7011
+ ${c3.gray}Close another session (its slot frees within ~90s), then resend your message.${c3.reset}`
5468
7012
  );
5469
7013
  return;
5470
7014
  }
@@ -5476,25 +7020,25 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5476
7020
  { label: "Not now", value: "no" }
5477
7021
  ]);
5478
7022
  if (choice !== "go") {
5479
- tui.print(`${c.gray}No change made \u2014 you can upgrade anytime with /upgrade.${c.reset}`);
7023
+ tui.print(`${c3.gray}No change made \u2014 you can upgrade anytime with /upgrade.${c3.reset}`);
5480
7024
  return;
5481
7025
  }
5482
- tui.print(`${c.gray}Applying\u2026${c.reset}`);
7026
+ tui.print(`${c3.gray}Applying\u2026${c3.reset}`);
5483
7027
  let applied;
5484
7028
  try {
5485
7029
  applied = await api.sessionsUpgrade(threadId, quote.sessions);
5486
7030
  } catch (e) {
5487
- tui.print(`${c.red}\u2717${c.reset} Upgrade failed: ${e instanceof Error ? e.message : String(e)}`);
7031
+ tui.print(`${c3.red}\u2717${c3.reset} Upgrade failed: ${e instanceof Error ? e.message : String(e)}`);
5488
7032
  return;
5489
7033
  }
5490
7034
  if (!applied?.ok) {
5491
- tui.print(`${c.red}\u2717${c.reset} Upgrade failed: ${applied?.error ?? "unknown error"}`);
7035
+ tui.print(`${c3.red}\u2717${c3.reset} Upgrade failed: ${applied?.error ?? "unknown error"}`);
5492
7036
  return;
5493
7037
  }
5494
7038
  const n = applied.sessions ?? quote.sessions;
5495
- tui.print(`${c.green}\u2713${c.reset} ${c.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c.reset}`);
7039
+ tui.print(`${c3.green}\u2713${c3.reset} ${c3.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c3.reset}`);
5496
7040
  if (opts.auto && lastSent) {
5497
- tui.print(`${c.gray}Continuing\u2026${c.reset}`);
7041
+ tui.print(`${c3.gray}Continuing\u2026${c3.reset}`);
5498
7042
  await sendNow(lastSent.text, lastSent.images);
5499
7043
  }
5500
7044
  } finally {
@@ -5538,14 +7082,24 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5538
7082
  },
5539
7083
  run: () => runApprovalsMenu(tui, perm, () => saveApprovals(api, threadId, perm))
5540
7084
  },
7085
+ // MCP servers are hosted by whichever client executes the tools — hide
7086
+ // the local MCP manager in a remote session (the daemon hosts them there).
7087
+ ...remote ? [] : [
7088
+ {
7089
+ name: "mcp",
7090
+ label: "MCP servers",
7091
+ hint: () => {
7092
+ const n = mcpCtl.connectedNames().length;
7093
+ return n ? `${n} connected` : "none";
7094
+ },
7095
+ run: () => runMcpMenu(tui, mcpCtl)
7096
+ }
7097
+ ],
5541
7098
  {
5542
- name: "mcp",
5543
- label: "MCP servers",
5544
- hint: () => {
5545
- const n = mcpCtl.connectedNames().length;
5546
- return n ? `${n} connected` : "none";
5547
- },
5548
- run: () => runMcpMenu(tui, mcpCtl)
7099
+ name: "daemon",
7100
+ label: "Machine daemon",
7101
+ hint: () => serviceStatus().installed ? "installed on this machine" : "not installed here",
7102
+ run: () => showDaemonInfo(tui, session)
5549
7103
  },
5550
7104
  {
5551
7105
  name: "skills",
@@ -5579,20 +7133,20 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5579
7133
  editingQueued = false;
5580
7134
  queued.push({ text, images });
5581
7135
  tui.setQueuedCount(queued.length);
5582
- tui.print(`${c.gray}\u23F3 queued:${c.reset} ${text}`);
7136
+ tui.print(`${c3.gray}\u23F3 queued:${c3.reset} ${text}`);
5583
7137
  return;
5584
7138
  }
5585
7139
  if (busy) {
5586
7140
  queued.push({ text, images });
5587
7141
  tui.setQueuedCount(queued.length);
5588
- tui.print(`${c.gray}\u23F3 queued:${c.reset} ${text} ${c.dim}(esc to steer now)${c.reset}`);
7142
+ tui.print(`${c3.gray}\u23F3 queued:${c3.reset} ${text} ${c3.dim}(esc to steer now)${c3.reset}`);
5589
7143
  } else {
5590
7144
  void sendNow(text, images);
5591
7145
  }
5592
7146
  };
5593
7147
  tui.onInterrupt = () => {
5594
7148
  if (queued.length > 0) {
5595
- tui.print(`${c.yellow}\u21AA steering \u2014 stopping current work and sending your message\u2026${c.reset}`);
7149
+ tui.print(`${c3.yellow}\u21AA steering \u2014 stopping current work and sending your message\u2026${c3.reset}`);
5596
7150
  void api.stop(threadId).catch(() => {
5597
7151
  }).then(() => flushQueued());
5598
7152
  } else if (busy) {
@@ -5602,7 +7156,7 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5602
7156
  liveOut = 0;
5603
7157
  tui.setWorking(false);
5604
7158
  refreshStatus();
5605
- tui.print(`${c.yellow}[interrupted by user]${c.reset}`);
7159
+ tui.print(`${c3.yellow}[interrupted by user]${c3.reset}`);
5606
7160
  void api.stop(threadId).catch(() => {
5607
7161
  });
5608
7162
  }
@@ -5620,15 +7174,27 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5620
7174
  return true;
5621
7175
  };
5622
7176
  events.connect();
5623
- await Promise.all([bridge.connect(), stream.connect()]);
7177
+ await Promise.all([...bridge ? [bridge.connect()] : [], stream.connect()]);
7178
+ const ownsExecution = bridge ? await bridge.whenOwnershipKnown(3e3) : false;
5624
7179
  void api.getGoal(threadId).then((g) => tui.setGoal(g)).catch(() => {
5625
7180
  });
5626
7181
  attaching.stop();
5627
- tui.banner([
5628
- `${c.bold}${c.magenta}Standard Code${c.reset} ${c.dim}\u2014 coding agent${c.reset}`,
5629
- `${c.gray}project:${c.reset} ${projectDir}`,
5630
- `${c.gray}machine:${c.reset} ${machine} ${c.gray}thread:${c.reset} ${threadId.slice(0, 8)}`
5631
- ]);
7182
+ tui.banner(
7183
+ remote ? [
7184
+ `${c3.bold}${c3.magenta}Standard Code${c3.reset} ${c3.dim}\u2014 coding agent${c3.reset}`,
7185
+ `${c3.gray}project:${c3.reset} ${session.remotePath ?? "?"} ${c3.teal}on ${runnerName}${c3.reset}`,
7186
+ `${c3.gray}runs on:${c3.reset} ${runnerName} ${c3.dim}(daemon executes tools; you're watching from ${machine})${c3.reset} ${c3.gray}thread:${c3.reset} ${threadId.slice(0, 8)}`
7187
+ ] : [
7188
+ `${c3.bold}${c3.magenta}Standard Code${c3.reset} ${c3.dim}\u2014 coding agent${c3.reset}`,
7189
+ `${c3.gray}project:${c3.reset} ${projectDir}`,
7190
+ `${c3.gray}machine:${c3.reset} ${machine} ${c3.gray}thread:${c3.reset} ${threadId.slice(0, 8)}`
7191
+ ]
7192
+ );
7193
+ if (!remote && session.suggestDaemonInstall && process.platform !== "win32" && !serviceStatus().installed) {
7194
+ tui.print(
7195
+ `${c3.dim}Tip: install the always-on daemon (${c3.reset}standardcode daemon install${c3.dim}) to start sessions on this machine from anywhere.${c3.reset}`
7196
+ );
7197
+ }
5632
7198
  if (resumed) await printHistory(api, threadId, tui);
5633
7199
  try {
5634
7200
  (await api.getMessages(threadId, 200)).forEach((m) => shownIds.add(m.id));
@@ -5637,22 +7203,50 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5637
7203
  const runningProcs = (await registry.list()).filter((p) => p.status === "running");
5638
7204
  if (runningProcs.length) {
5639
7205
  tui.print(
5640
- `${c.cyan}\u2699 ${runningProcs.length} background process${runningProcs.length === 1 ? "" : "es"} running:${c.reset}`
7206
+ `${c3.cyan}\u2699 ${runningProcs.length} background process${runningProcs.length === 1 ? "" : "es"} running:${c3.reset}`
5641
7207
  );
5642
- for (const p of runningProcs) tui.print(`${c.gray} ${p.id} ${p.description || p.command}${c.reset}`);
7208
+ for (const p of runningProcs) tui.print(`${c3.gray} ${p.id} ${p.description || p.command}${c3.reset}`);
5643
7209
  }
5644
7210
  refreshBgCount();
5645
- const enabledServers = listMcpServers().filter((s) => s.enabled);
5646
- for (const s of enabledServers) {
5647
- const res = await mcpCtl.connect(s);
5648
- if (res.ok) {
5649
- tui.print(`${c.cyan}\u26A1 MCP "${s.name}" connected${c.reset} ${c.gray}(${res.tools} tool${res.tools === 1 ? "" : "s"})${c.reset}`);
5650
- } else {
5651
- tui.print(`${c.red}\u26A0 MCP "${s.name}" failed:${c.reset} ${c.gray}${res.error}${c.reset}`);
7211
+ if (!remote && ownsExecution) {
7212
+ const enabledServers = listMcpServers().filter((s) => s.enabled);
7213
+ for (const s of enabledServers) {
7214
+ const res = await mcpCtl.connect(s);
7215
+ if (res.ok) {
7216
+ tui.print(`${c3.cyan}\u26A1 MCP "${s.name}" connected${c3.reset} ${c3.gray}(${res.tools} tool${res.tools === 1 ? "" : "s"})${c3.reset}`);
7217
+ } else {
7218
+ tui.print(`${c3.red}\u26A0 MCP "${s.name}" failed:${c3.reset} ${c3.gray}${res.error}${c3.reset}`);
7219
+ }
5652
7220
  }
7221
+ publishMcpCatalog();
5653
7222
  }
5654
- publishMcpCatalog();
5655
7223
  tui.start();
7224
+ const answeredApprovals = /* @__PURE__ */ new Set();
7225
+ let approvalPromptOpen = false;
7226
+ const relayApprovals = async () => {
7227
+ const watching = remote || (bridge ? !bridge.isOwner : false);
7228
+ if (!watching || approvalPromptOpen) return;
7229
+ const request = parseApprovalRequest(await api.kvGet(threadId, APPROVAL_REQUEST_KEY));
7230
+ if (!request || answeredApprovals.has(request.tool_call_id)) return;
7231
+ approvalPromptOpen = true;
7232
+ try {
7233
+ const { choice, reason } = await tui.approval(
7234
+ `${request.summary}${request.permission ? `
7235
+ ${c3.bold}why: ${request.permission}${c3.reset}` : ""}
7236
+ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
7237
+ request.risk
7238
+ );
7239
+ answeredApprovals.add(request.tool_call_id);
7240
+ await writeApprovalResponse(api, threadId, {
7241
+ tool_call_id: request.tool_call_id,
7242
+ choice,
7243
+ reason,
7244
+ decided_at: Date.now()
7245
+ });
7246
+ } finally {
7247
+ approvalPromptOpen = false;
7248
+ }
7249
+ };
5656
7250
  const poll = async () => {
5657
7251
  let msgs;
5658
7252
  try {
@@ -5671,7 +7265,7 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5671
7265
  continue;
5672
7266
  }
5673
7267
  if (m.role === "assistant" && text) printAssistant(tui, text);
5674
- else if (m.role === "system" && text) tui.print(`${c.dim}${text}${c.reset}`);
7268
+ else if (m.role === "system" && text) tui.print(`${c3.dim}${text}${c3.reset}`);
5675
7269
  else if (m.role === "user" && text) {
5676
7270
  const pending = pendingSent.get(text) ?? 0;
5677
7271
  if (pending > 0) {
@@ -5697,6 +7291,8 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5697
7291
  if (queued.length > 0 && !editingQueued) await flushQueued();
5698
7292
  }
5699
7293
  refreshBgCount();
7294
+ void relayApprovals().catch(() => {
7295
+ });
5700
7296
  try {
5701
7297
  const logs = await api.getLogs(threadId, 100);
5702
7298
  let landed = 0;
@@ -5733,14 +7329,14 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5733
7329
  await sessionEnded;
5734
7330
  clearInterval(pollTimer);
5735
7331
  clearInterval(heartbeatPoll);
5736
- const stopped = api.stop(threadId).catch(() => {
5737
- });
7332
+ const stopped = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
7333
+ }) : Promise.resolve();
5738
7334
  const procsStopped = host.stopAllLocalProcesses().catch(() => 0);
5739
- bridge.close();
7335
+ bridge?.close();
5740
7336
  stream.close();
5741
7337
  events.close();
5742
7338
  subActivity.closeAll();
5743
- mcp.closeAll();
7339
+ mcp?.closeAll();
5744
7340
  const [, killed] = await Promise.race([
5745
7341
  Promise.all([stopped, procsStopped]),
5746
7342
  new Promise((r) => setTimeout(() => r([void 0, 0]), 1500))
@@ -5753,16 +7349,16 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5753
7349
  tui.setStep(null, 0);
5754
7350
  tui.setBackgroundCount(0);
5755
7351
  if (killed > 0) {
5756
- tui.print(`${c.cyan}\u2699${c.reset} Stopped ${killed} background process${killed === 1 ? "" : "es"}.`);
7352
+ tui.print(`${c3.cyan}\u2699${c3.reset} Stopped ${killed} background process${killed === 1 ? "" : "es"}.`);
5757
7353
  }
5758
- tui.print(`${c.dim}\u2500\u2500 conversation cleared \u2014 starting a fresh session \u2500\u2500${c.reset}`);
7354
+ tui.print(`${c3.dim}\u2500\u2500 conversation cleared \u2014 starting a fresh session \u2500\u2500${c3.reset}`);
5759
7355
  }
5760
7356
  async function runSkillsMenu(tui, skills) {
5761
7357
  let list;
5762
7358
  try {
5763
7359
  list = await skills.list();
5764
7360
  } catch (e) {
5765
- tui.print(`${c.red}\u2717 couldn't load skills:${c.reset} ${c.gray}${e instanceof Error ? e.message : String(e)}${c.reset}`);
7361
+ tui.print(`${c3.red}\u2717 couldn't load skills:${c3.reset} ${c3.gray}${e instanceof Error ? e.message : String(e)}${c3.reset}`);
5766
7362
  return;
5767
7363
  }
5768
7364
  const INSTALL = "__install__";
@@ -5773,7 +7369,7 @@ async function runSkillsMenu(tui, skills) {
5773
7369
  }));
5774
7370
  items.push({ label: "\uFF0B Install a skill\u2026", hint: "find & install", value: INSTALL });
5775
7371
  const picked = await tui.select(
5776
- `${c.bold}Agent skills${c.reset} ${c.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c.reset}`,
7372
+ `${c3.bold}Agent skills${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c3.reset}`,
5777
7373
  items
5778
7374
  );
5779
7375
  if (!picked) return;
@@ -5786,8 +7382,8 @@ async function runSkillsMenu(tui, skills) {
5786
7382
  return;
5787
7383
  }
5788
7384
  const skill = list.find((s) => s.name === picked);
5789
- tui.print(`${c.cyan}${skill.name}${c.reset}${skill.version ? ` ${c.dim}v${skill.version}${c.reset}` : ""} ${c.gray}\u2014 ${skill.description}${c.reset}`);
5790
- const action = await tui.select(`${c.bold}${picked}${c.reset}`, [
7385
+ tui.print(`${c3.cyan}${skill.name}${c3.reset}${skill.version ? ` ${c3.dim}v${skill.version}${c3.reset}` : ""} ${c3.gray}\u2014 ${skill.description}${c3.reset}`);
7386
+ const action = await tui.select(`${c3.bold}${picked}${c3.reset}`, [
5791
7387
  skill.enabled ? { label: "Disable (hide from the agent)", value: "disable" } : { label: "Enable", value: "enable" },
5792
7388
  { label: "View files", value: "files" },
5793
7389
  { label: "Remove this skill", value: "remove" },
@@ -5796,20 +7392,20 @@ async function runSkillsMenu(tui, skills) {
5796
7392
  try {
5797
7393
  if (action === "enable" || action === "disable") {
5798
7394
  await skills.setEnabled(picked, action === "enable");
5799
- tui.print(`${c.gray}${action}d ${picked}${c.reset}`);
7395
+ tui.print(`${c3.gray}${action}d ${picked}${c3.reset}`);
5800
7396
  } else if (action === "files") {
5801
- for (const f of skill.files) tui.print(` ${c.gray}${f}${c.reset}`);
7397
+ for (const f of skill.files) tui.print(` ${c3.gray}${f}${c3.reset}`);
5802
7398
  } else if (action === "remove") {
5803
7399
  await skills.remove(picked);
5804
- tui.print(`${c.gray}removed ${picked}${c.reset}`);
7400
+ tui.print(`${c3.gray}removed ${picked}${c3.reset}`);
5805
7401
  }
5806
7402
  } catch (e) {
5807
- tui.print(`${c.red}\u2717 ${e instanceof Error ? e.message : String(e)}${c.reset}`);
7403
+ tui.print(`${c3.red}\u2717 ${e instanceof Error ? e.message : String(e)}${c3.reset}`);
5808
7404
  }
5809
7405
  }
5810
7406
  async function runLevelMenu(tui, perm) {
5811
7407
  const picked = await tui.select(
5812
- `${c.bold}Auto-accept level${c.reset} ${c.dim}(\u2191/\u2193 \xB7 enter \xB7 shift-tab cycles)${c.reset}`,
7408
+ `${c3.bold}Auto-accept level${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 shift-tab cycles)${c3.reset}`,
5813
7409
  LEVELS.map((l) => ({
5814
7410
  label: levelLabel(l),
5815
7411
  hint: l === tui.level ? "current" : "",
@@ -5821,28 +7417,51 @@ async function runLevelMenu(tui, perm) {
5821
7417
  perm.level = picked;
5822
7418
  }
5823
7419
  }
7420
+ function showDaemonInfo(tui, session) {
7421
+ if (session.mode === "remote" && session.runner) {
7422
+ tui.print(
7423
+ `${c3.gray}This session runs on${c3.reset} ${c3.bold}${session.runner.name}${c3.reset} ${c3.gray}(${session.runner.hostname}) \u2014 its daemon executes the tools.${c3.reset}`
7424
+ );
7425
+ } else {
7426
+ tui.print(`${c3.gray}This session runs on this machine.${c3.reset}`);
7427
+ }
7428
+ const status = serviceStatus();
7429
+ tui.print(
7430
+ `${c3.gray}Daemon on this machine:${c3.reset} ${status.installed ? status.detail : "not installed"}`
7431
+ );
7432
+ if (!status.installed) {
7433
+ tui.print(
7434
+ `${c3.gray}Install it to start sessions on this machine from anywhere:${c3.reset} ${c3.bold}standardcode daemon install${c3.reset}`
7435
+ );
7436
+ tui.print(
7437
+ `${c3.dim}The daemon keeps running after you close the terminal \u2014 it self-restarts, self-updates, and executes sessions you start from other machines.${c3.reset}`
7438
+ );
7439
+ } else {
7440
+ tui.print(`${c3.dim}Manage it with standardcode daemon status | uninstall | add-project <path>.${c3.reset}`);
7441
+ }
7442
+ }
5824
7443
  function showKeybindings(tui) {
5825
- tui.print(`${c.gray}shortcuts:${c.reset}`);
5826
- tui.print(`${c.gray} shift-tab${c.reset} cycle auto-accept level (1\u20135)`);
5827
- tui.print(`${c.gray} /${c.reset} open the command palette (type to filter)`);
5828
- tui.print(`${c.gray} ctrl-v${c.reset} paste an image from the clipboard ([#Image 1])`);
5829
- tui.print(`${c.gray} \u2191 / \u2193${c.reset} cycle past messages (on the input's top line)`);
5830
- tui.print(`${c.gray} \u2190${c.reset} from the start of the input: select the [\u2699 n bg] badge (enter opens it)`);
5831
- tui.print(`${c.gray} ctrl-c${c.reset} quit`);
7444
+ tui.print(`${c3.gray}shortcuts:${c3.reset}`);
7445
+ tui.print(`${c3.gray} shift-tab${c3.reset} cycle auto-accept level (1\u20135)`);
7446
+ tui.print(`${c3.gray} /${c3.reset} open the command palette (type to filter)`);
7447
+ tui.print(`${c3.gray} ctrl-v${c3.reset} paste an image from the clipboard ([#Image 1])`);
7448
+ tui.print(`${c3.gray} \u2191 / \u2193${c3.reset} cycle past messages (on the input's top line)`);
7449
+ tui.print(`${c3.gray} \u2190${c3.reset} from the start of the input: select the [\u2699 n bg] badge (enter opens it)`);
7450
+ tui.print(`${c3.gray} ctrl-c${c3.reset} quit`);
5832
7451
  }
5833
7452
  async function runUpdateCommand(tui) {
5834
7453
  const version = readVersion();
5835
7454
  const result = await forceCheckForUpdate(version);
5836
7455
  if (!result) {
5837
- tui.print(`${c.green}\u2713${c.reset} ${c.gray}@standardagents/code${c.reset} is up to date (v${version})`);
7456
+ tui.print(`${c3.green}\u2713${c3.reset} ${c3.gray}@standardagents/code${c3.reset} is up to date (v${version})`);
5838
7457
  return;
5839
7458
  }
5840
7459
  const { latest } = result;
5841
7460
  tui.print(`
5842
- ${c.yellow}\u27F3${c.reset} Update available: ${c.gray}v${version}${c.reset} \u2192 ${c.green}v${latest}${c.reset}`);
7461
+ ${c3.yellow}\u27F3${c3.reset} Update available: ${c3.gray}v${version}${c3.reset} \u2192 ${c3.green}v${latest}${c3.reset}`);
5843
7462
  const pm = detectPackageManager();
5844
7463
  if (!pm) {
5845
- tui.print(` ${c.gray}This is a source checkout \u2014 pull the repo to update.${c.reset}`);
7464
+ tui.print(` ${c3.gray}This is a source checkout \u2014 pull the repo to update.${c3.reset}`);
5846
7465
  return;
5847
7466
  }
5848
7467
  const { display } = updateCommand(pm);
@@ -5851,28 +7470,28 @@ async function runUpdateCommand(tui) {
5851
7470
  { label: "No, skip", value: "no" }
5852
7471
  ]);
5853
7472
  if (choice === "yes") {
5854
- tui.print(` ${c.gray}Running ${display}\u2026${c.reset}`);
7473
+ tui.print(` ${c3.gray}Running ${display}\u2026${c3.reset}`);
5855
7474
  const { ok, output: pmOutput } = await runUpdate(pm);
5856
7475
  if (ok) {
5857
- tui.print(` ${c.green}\u2713${c.reset} Updated to v${latest}. Restart to use the new version.`);
7476
+ tui.print(` ${c3.green}\u2713${c3.reset} Updated to v${latest}. Restart to use the new version.`);
5858
7477
  } else {
5859
- tui.print(` ${c.red}\u2717${c.reset} Update failed:`);
7478
+ tui.print(` ${c3.red}\u2717${c3.reset} Update failed:`);
5860
7479
  for (const line of pmOutput.trim().split("\n").slice(-6)) {
5861
- tui.print(` ${c.dim}${line}${c.reset}`);
7480
+ tui.print(` ${c3.dim}${line}${c3.reset}`);
5862
7481
  }
5863
7482
  }
5864
7483
  } else {
5865
- tui.print(` ${c.gray}Skipped. Run /update later.${c.reset}`);
7484
+ tui.print(` ${c3.gray}Skipped. Run /update later.${c3.reset}`);
5866
7485
  }
5867
7486
  }
5868
7487
  async function runProcessMenu(tui, bg) {
5869
7488
  const procs = await bg.list();
5870
7489
  if (!procs.length) {
5871
- tui.print(`${c.gray}No background processes for this session.${c.reset}`);
7490
+ tui.print(`${c3.gray}No background processes for this session.${c3.reset}`);
5872
7491
  return;
5873
7492
  }
5874
7493
  const items = procs.map((p) => {
5875
- const status = p.status === "running" ? `${c.green}running${c.reset}` : `${c.gray}${p.status}${typeof p.exitCode === "number" ? ` (exit ${p.exitCode})` : ""}${c.reset}`;
7494
+ const status = p.status === "running" ? `${c3.green}running${c3.reset}` : `${c3.gray}${p.status}${typeof p.exitCode === "number" ? ` (exit ${p.exitCode})` : ""}${c3.reset}`;
5876
7495
  return {
5877
7496
  label: `${p.description || p.command}`,
5878
7497
  hint: `${p.id} \xB7 ${status}`,
@@ -5880,22 +7499,22 @@ async function runProcessMenu(tui, bg) {
5880
7499
  };
5881
7500
  });
5882
7501
  const picked = await tui.select(
5883
- `${c.bold}Background processes${c.reset} ${c.dim}(\u2191/\u2193 \xB7 enter to manage \xB7 esc to close)${c.reset}`,
7502
+ `${c3.bold}Background processes${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter to manage \xB7 esc to close)${c3.reset}`,
5884
7503
  items
5885
7504
  );
5886
7505
  if (!picked) return;
5887
7506
  const proc = procs.find((p) => p.id === picked);
5888
7507
  if (!proc || proc.status !== "running") {
5889
- tui.print(`${c.gray}${picked} is not running.${c.reset}`);
7508
+ tui.print(`${c3.gray}${picked} is not running.${c3.reset}`);
5890
7509
  return;
5891
7510
  }
5892
- const action = await tui.select(`${c.bold}${proc.description || proc.command}${c.reset}`, [
7511
+ const action = await tui.select(`${c3.bold}${proc.description || proc.command}${c3.reset}`, [
5893
7512
  { label: "Stop this process", value: "stop" },
5894
7513
  { label: "Leave it running", value: "leave" }
5895
7514
  ]);
5896
7515
  if (action === "stop") {
5897
7516
  await bg.stop(picked);
5898
- tui.print(`${c.gray}stopped ${picked}${c.reset}`);
7517
+ tui.print(`${c3.gray}stopped ${picked}${c3.reset}`);
5899
7518
  }
5900
7519
  }
5901
7520
  async function runApprovalsMenu(tui, perm, save) {
@@ -5903,7 +7522,7 @@ async function runApprovalsMenu(tui, perm, save) {
5903
7522
  const risks = Array.from(perm.allowRisk).sort((a, b) => a - b);
5904
7523
  if (!tools.length && !risks.length) {
5905
7524
  tui.print(
5906
- `${c.gray}No pre-approved commands. At a permission prompt, choose "Always allow this tool" or "Allow all level N" to add some.${c.reset}`
7525
+ `${c3.gray}No pre-approved commands. At a permission prompt, choose "Always allow this tool" or "Allow all level N" to add some.${c3.reset}`
5907
7526
  );
5908
7527
  return;
5909
7528
  }
@@ -5913,22 +7532,22 @@ async function runApprovalsMenu(tui, perm, save) {
5913
7532
  { label: "Clear all approvals", hint: "", value: "clear" }
5914
7533
  ];
5915
7534
  const picked = await tui.select(
5916
- `${c.bold}Approved commands${c.reset} ${c.dim}(enter to revoke \xB7 esc to close)${c.reset}`,
7535
+ `${c3.bold}Approved commands${c3.reset} ${c3.dim}(enter to revoke \xB7 esc to close)${c3.reset}`,
5917
7536
  items
5918
7537
  );
5919
7538
  if (!picked) return;
5920
7539
  if (picked === "clear") {
5921
7540
  perm.alwaysAllow.clear();
5922
7541
  perm.allowRisk.clear();
5923
- tui.print(`${c.gray}cleared all approvals${c.reset}`);
7542
+ tui.print(`${c3.gray}cleared all approvals${c3.reset}`);
5924
7543
  } else if (picked.startsWith("tool:")) {
5925
7544
  const t = picked.slice(5);
5926
7545
  perm.alwaysAllow.delete(t);
5927
- tui.print(`${c.gray}revoked tool ${t}${c.reset}`);
7546
+ tui.print(`${c3.gray}revoked tool ${t}${c3.reset}`);
5928
7547
  } else if (picked.startsWith("risk:")) {
5929
7548
  const r = Number(picked.slice(5));
5930
7549
  perm.allowRisk.delete(r);
5931
- tui.print(`${c.gray}revoked level ${r}${c.reset}`);
7550
+ tui.print(`${c3.gray}revoked level ${r}${c3.reset}`);
5932
7551
  }
5933
7552
  save();
5934
7553
  }
@@ -5946,7 +7565,7 @@ async function runMcpMenu(tui, mcp) {
5946
7565
  items.push({ label: "\uFF0B Install a new MCP server\u2026", hint: "find & install", value: INSTALL });
5947
7566
  items.push({ label: "Add manually (name: command)\u2026", hint: "advanced", value: ADD_MANUAL });
5948
7567
  const picked = await tui.select(
5949
- `${c.bold}MCP servers${c.reset} ${c.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c.reset}`,
7568
+ `${c3.bold}MCP servers${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c3.reset}`,
5950
7569
  items
5951
7570
  );
5952
7571
  if (!picked) return;
@@ -5960,7 +7579,7 @@ async function runMcpMenu(tui, mcp) {
5960
7579
  }
5961
7580
  const server = configured.find((s) => s.name === picked);
5962
7581
  const isConnected = connected.has(picked);
5963
- const action = await tui.select(`${c.bold}${picked}${c.reset}`, [
7582
+ const action = await tui.select(`${c3.bold}${picked}${c3.reset}`, [
5964
7583
  { label: "View tools", value: "tools" },
5965
7584
  isConnected ? { label: "Disconnect", value: "disconnect" } : { label: "Connect", value: "connect" },
5966
7585
  server.enabled ? { label: "Disable (don't auto-connect)", value: "disable" } : { label: "Enable (auto-connect on start)", value: "enable" },
@@ -5970,29 +7589,29 @@ async function runMcpMenu(tui, mcp) {
5970
7589
  if (action === "tools") {
5971
7590
  const entry = mcp.catalog().servers.find((e) => e.name === picked);
5972
7591
  if (!entry || entry.status !== "connected") {
5973
- tui.print(`${c.gray}${picked} is not connected \u2014 connect it to list tools.${c.reset}`);
7592
+ tui.print(`${c3.gray}${picked} is not connected \u2014 connect it to list tools.${c3.reset}`);
5974
7593
  return;
5975
7594
  }
5976
- if (!entry.tools.length) tui.print(`${c.gray}${picked} exposes no tools.${c.reset}`);
5977
- for (const t of entry.tools) tui.print(` ${c.cyan}${t.name}${c.reset}${t.description ? ` ${c.gray}\u2014 ${t.description}${c.reset}` : ""}`);
5978
- if (entry.resources.length) tui.print(` ${c.gray}${entry.resources.length} resource(s)${c.reset}`);
7595
+ if (!entry.tools.length) tui.print(`${c3.gray}${picked} exposes no tools.${c3.reset}`);
7596
+ for (const t of entry.tools) tui.print(` ${c3.cyan}${t.name}${c3.reset}${t.description ? ` ${c3.gray}\u2014 ${t.description}${c3.reset}` : ""}`);
7597
+ if (entry.resources.length) tui.print(` ${c3.gray}${entry.resources.length} resource(s)${c3.reset}`);
5979
7598
  } else if (action === "connect") {
5980
7599
  const res = await mcp.connect(server);
5981
- tui.print(res.ok ? `${c.cyan}\u26A1 connected (${res.tools} tools)${c.reset}` : `${c.red}\u26A0 ${res.error}${c.reset}`);
7600
+ tui.print(res.ok ? `${c3.cyan}\u26A1 connected (${res.tools} tools)${c3.reset}` : `${c3.red}\u26A0 ${res.error}${c3.reset}`);
5982
7601
  } else if (action === "disconnect") {
5983
7602
  mcp.disconnect(picked);
5984
- tui.print(`${c.gray}disconnected ${picked}${c.reset}`);
7603
+ tui.print(`${c3.gray}disconnected ${picked}${c3.reset}`);
5985
7604
  } else if (action === "enable") {
5986
7605
  mcp.setEnabled(picked, true);
5987
7606
  const res = await mcp.connect(server);
5988
- tui.print(res.ok ? `${c.cyan}\u26A1 enabled + connected (${res.tools} tools)${c.reset}` : `${c.red}\u26A0 enabled but failed: ${res.error}${c.reset}`);
7607
+ tui.print(res.ok ? `${c3.cyan}\u26A1 enabled + connected (${res.tools} tools)${c3.reset}` : `${c3.red}\u26A0 enabled but failed: ${res.error}${c3.reset}`);
5989
7608
  } else if (action === "disable") {
5990
7609
  mcp.setEnabled(picked, false);
5991
7610
  mcp.disconnect(picked);
5992
- tui.print(`${c.gray}disabled + disconnected ${picked}${c.reset}`);
7611
+ tui.print(`${c3.gray}disabled + disconnected ${picked}${c3.reset}`);
5993
7612
  } else if (action === "remove") {
5994
7613
  mcp.remove(picked);
5995
- tui.print(`${c.gray}removed ${picked}${c.reset}`);
7614
+ tui.print(`${c3.gray}removed ${picked}${c3.reset}`);
5996
7615
  }
5997
7616
  }
5998
7617
  async function addMcpServer(tui, mcp) {
@@ -6003,13 +7622,13 @@ async function addMcpServer(tui, mcp) {
6003
7622
  if (!spec) return;
6004
7623
  const cfg = parseServerSpec(spec);
6005
7624
  if (!cfg) {
6006
- tui.print(`${c.yellow}couldn't parse that. Use name: command [args]${c.reset}`);
7625
+ tui.print(`${c3.yellow}couldn't parse that. Use name: command [args]${c3.reset}`);
6007
7626
  return;
6008
7627
  }
6009
- tui.print(`${c.gray}connecting MCP "${cfg.name}" (${cfg.command} ${cfg.args.join(" ")})\u2026${c.reset}`);
7628
+ tui.print(`${c3.gray}connecting MCP "${cfg.name}" (${cfg.command} ${cfg.args.join(" ")})\u2026${c3.reset}`);
6010
7629
  const res = await mcp.add(cfg);
6011
- if (res.ok) tui.print(`${c.cyan}\u26A1 MCP "${cfg.name}" connected${c.reset} ${c.gray}(${res.tools} tool${res.tools === 1 ? "" : "s"})${c.reset}`);
6012
- else tui.print(`${c.red}\u26A0 MCP "${cfg.name}" failed:${c.reset} ${c.gray}${res.error}${c.reset} ${c.dim}(saved; retry from the MCP menu)${c.reset}`);
7630
+ if (res.ok) tui.print(`${c3.cyan}\u26A1 MCP "${cfg.name}" connected${c3.reset} ${c3.gray}(${res.tools} tool${res.tools === 1 ? "" : "s"})${c3.reset}`);
7631
+ else tui.print(`${c3.red}\u26A0 MCP "${cfg.name}" failed:${c3.reset} ${c3.gray}${res.error}${c3.reset} ${c3.dim}(saved; retry from the MCP menu)${c3.reset}`);
6013
7632
  }
6014
7633
  async function installMcpServerFlow(tui, mcp) {
6015
7634
  const query = await tui.prompt(