@standardagents/code 0.8.0 → 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();",
@@ -2373,14 +2541,14 @@ function buildInputBoxRows(opts) {
2373
2541
  const plainChars = [...plain];
2374
2542
  let di = 0;
2375
2543
  for (const ch of plainChars) {
2376
- const c2 = colorAt(perimeterIndex("top", xi++, W, bodyH));
2544
+ const c4 = colorAt(perimeterIndex("top", xi++, W, bodyH));
2377
2545
  if (ch === "\u25CF" || ch === "\u25CB") {
2378
2546
  const levelN = Math.max(1, Math.min(5, level));
2379
2547
  const filled = di < levelN;
2380
2548
  di++;
2381
- top += (filled ? levelColor || c2 : "\x1B[38;5;240m") + (filled ? "\u25CF" : "\u25CB") + reset;
2549
+ top += (filled ? levelColor || c4 : "\x1B[38;5;240m") + (filled ? "\u25CF" : "\u25CB") + reset;
2382
2550
  } else {
2383
- top += c2 + ch + reset;
2551
+ top += c4 + ch + reset;
2384
2552
  }
2385
2553
  }
2386
2554
  }
@@ -2401,10 +2569,10 @@ function buildInputBoxRows(opts) {
2401
2569
  let bottom = "";
2402
2570
  for (let x = 0; x < W; x++) {
2403
2571
  const idx = perimeterIndex("bottom", W - 1 - x, W, bodyH);
2404
- const c2 = colorAt(idx);
2405
- if (x === 0) bottom += c2 + "\u2570" + reset;
2406
- else if (x === W - 1) bottom += c2 + "\u256F" + reset;
2407
- else bottom += c2 + "\u2500" + reset;
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;
2408
2576
  }
2409
2577
  return [pad + top, ...bodyRows, pad + bottom];
2410
2578
  }
@@ -3086,7 +3254,7 @@ var Tui = class _Tui {
3086
3254
  const q = this.inputBuffer.slice(1).trim().toLowerCase();
3087
3255
  if (q === "") return this.commands;
3088
3256
  return this.commands.filter(
3089
- (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)
3090
3258
  );
3091
3259
  }
3092
3260
  runCommand(cmd) {
@@ -4171,7 +4339,7 @@ function tableCells(row) {
4171
4339
  let r = row.trim();
4172
4340
  if (r.startsWith("|")) r = r.slice(1);
4173
4341
  if (r.endsWith("|")) r = r.slice(0, -1);
4174
- return r.split("|").map((c2) => c2.trim());
4342
+ return r.split("|").map((c4) => c4.trim());
4175
4343
  }
4176
4344
  var SEPARATOR = /^[\s|:-]+$/;
4177
4345
  function isTableSeparator(line) {
@@ -4180,17 +4348,17 @@ function isTableSeparator(line) {
4180
4348
  function renderTable(rows) {
4181
4349
  const cols2 = Math.max(...rows.map((r) => r.length));
4182
4350
  const widths = [];
4183
- for (let c2 = 0; c2 < cols2; c2++) {
4184
- 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] ?? ""))));
4185
4353
  }
4186
4354
  const sep = `${GRAY} \u2502 ${R}`;
4187
4355
  const out = [];
4188
4356
  rows.forEach((r, ri) => {
4189
4357
  const cells = [];
4190
- for (let c2 = 0; c2 < cols2; c2++) {
4191
- const raw = r[c2] ?? "";
4358
+ for (let c4 = 0; c4 < cols2; c4++) {
4359
+ const raw = r[c4] ?? "";
4192
4360
  const styled = ri === 0 ? `${BOLD2}${inline(raw)}${R}` : inline(raw);
4193
- cells.push(padEndVisible(styled, widths[c2]));
4361
+ cells.push(padEndVisible(styled, widths[c4]));
4194
4362
  }
4195
4363
  out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
4196
4364
  if (ri === 0) {
@@ -4510,7 +4678,7 @@ var McpManager = class {
4510
4678
  }
4511
4679
  }
4512
4680
  closeAll() {
4513
- for (const [, c2] of this.clients) c2.close();
4681
+ for (const [, c4] of this.clients) c4.close();
4514
4682
  this.clients.clear();
4515
4683
  }
4516
4684
  get(name) {
@@ -4521,13 +4689,13 @@ var McpManager = class {
4521
4689
  }
4522
4690
  toolCount() {
4523
4691
  let n = 0;
4524
- for (const [, c2] of this.clients) n += c2.tools.length;
4692
+ for (const [, c4] of this.clients) n += c4.tools.length;
4525
4693
  return n;
4526
4694
  }
4527
4695
  /** A JSON-serializable catalog of every connected server for the KV/context. */
4528
4696
  catalog() {
4529
4697
  return {
4530
- servers: Array.from(this.clients.values()).map((c2) => c2.catalogEntry()),
4698
+ servers: Array.from(this.clients.values()).map((c4) => c4.catalogEntry()),
4531
4699
  generatedAt: Date.now()
4532
4700
  };
4533
4701
  }
@@ -4617,9 +4785,9 @@ function flattenContent(content, structured) {
4617
4785
  }
4618
4786
  function flattenResourceContents(contents) {
4619
4787
  const parts = [];
4620
- for (const c2 of contents || []) {
4621
- if (typeof c2.text === "string") parts.push(c2.text);
4622
- 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)]`);
4623
4791
  }
4624
4792
  return parts.join("\n").trim();
4625
4793
  }
@@ -4641,10 +4809,10 @@ function sortKeys(value) {
4641
4809
  }
4642
4810
  return value;
4643
4811
  }
4644
- function sha256(input2) {
4645
- return crypto.createHash("sha256").update(input2).digest("hex");
4812
+ function sha256(input3) {
4813
+ return crypto.createHash("sha256").update(input3).digest("hex");
4646
4814
  }
4647
- var DIR = path3.join(os6.homedir(), ".standardagents");
4815
+ var DIR = path3.join(os9.homedir(), ".standardagents");
4648
4816
  var FILE = path3.join(DIR, "credentials");
4649
4817
  function normalizeEndpoint(endpoint) {
4650
4818
  let e = endpoint.trim();
@@ -4702,6 +4870,276 @@ function saveDefaultEndpoint(endpoint) {
4702
4870
  } catch {
4703
4871
  }
4704
4872
  }
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.");
4903
+ }
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];
4908
+ try {
4909
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
4910
+ child.unref();
4911
+ } catch {
4912
+ }
4913
+ }
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() {
4922
+ try {
4923
+ const pkg = JSON.parse(fs4.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
4924
+ return typeof pkg.version === "string" ? pkg.version : "";
4925
+ } catch {
4926
+ return "";
4927
+ }
4928
+ }
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 = "";
4934
+ try {
4935
+ host = new URL(endpoint).hostname;
4936
+ } catch {
4937
+ return false;
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;
4948
+ }
4949
+ var DIR2 = path3.join(os9.homedir(), ".standardagents");
4950
+ var FILE2 = path3.join(DIR2, "machine.json");
4951
+ function loadMachineIdentity() {
4952
+ try {
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
+ }
4705
5143
  var PKG_NAME = "@standardagents/code";
4706
5144
  var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}`;
4707
5145
  var CACHE_REL_DIR = ".config/standardagents-cli";
@@ -4879,78 +5317,795 @@ function runUpdate(pm) {
4879
5317
  });
4880
5318
  }
4881
5319
 
4882
- // src/index.ts
4883
- var AGENT_ID = "standard_code_agent";
4884
- var AGENT_ID_VARIANTS = [AGENT_ID, "standard_code_high_agent"];
4885
- var PRODUCTION_ENDPOINT = "https://api.standardcode.ai";
4886
- var c = {
4887
- reset: "\x1B[0m",
4888
- dim: "\x1B[2m",
4889
- bold: "\x1B[1m",
4890
- white: "\x1B[97m",
4891
- cyan: "\x1B[36m",
4892
- green: "\x1B[32m",
4893
- gray: "\x1B[90m",
4894
- magenta: "\x1B[35m",
4895
- yellow: "\x1B[33m",
4896
- red: "\x1B[31m",
4897
- teal: "\x1B[38;5;37m"
4898
- // brand teal (matches the marketing site's teal accent)
4899
- };
4900
- var LOGO_MARK = [
4901
- " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
4902
- " \u2588\u2588\u2588 \u2588\u2588",
4903
- " \u2588\u2588 \u2588",
4904
- "\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588",
4905
- "\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588",
4906
- "\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588",
4907
- "\u2588 \u2588\u2588",
4908
- "\u2588\u2588 \u2588\u2588\u2588",
4909
- "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"
4910
- ];
4911
- function printUsage() {
4912
- stdout.write(
4913
- [
4914
- "",
4915
- `${c.bold}Usage${c.reset}`,
4916
- " standardcode [options] [dir]",
4917
- "",
4918
- `${c.bold}Options${c.reset}`,
4919
- " -e, --endpoint [url] Use a different Standard Agents instance for this run",
4920
- " (default: https://api.standardcode.ai).",
4921
- " If url is omitted, prompt for it.",
4922
- " Credentials are remembered for that endpoint, but",
4923
- " the saved default endpoint is not changed.",
4924
- " -h, --help Show this help.",
4925
- ""
4926
- ].join("\n")
4927
- );
4928
- }
4929
- function parseArgs2(args) {
4930
- const parsed = { help: false, promptEndpoint: false };
4931
- for (let i = 0; i < args.length; i++) {
4932
- const arg = args[i];
4933
- if (arg === "--help" || arg === "-h") {
4934
- parsed.help = true;
4935
- continue;
4936
- }
4937
- if (arg === "--endpoint" || arg === "-e") {
4938
- const value = args[i + 1];
4939
- if (value && !value.startsWith("-")) {
4940
- parsed.endpoint = value;
4941
- i++;
4942
- } else {
4943
- parsed.promptEndpoint = true;
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`);
4944
5334
  }
4945
- continue;
5335
+ } catch {
4946
5336
  }
4947
- if (arg.startsWith("--endpoint=")) {
4948
- const value = arg.slice("--endpoint=".length);
4949
- if (value) {
4950
- parsed.endpoint = value;
4951
- } else {
4952
- parsed.promptEndpoint = true;
4953
- }
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
+
6040
+ // src/index.ts
6041
+ var c3 = {
6042
+ reset: "\x1B[0m",
6043
+ dim: "\x1B[2m",
6044
+ bold: "\x1B[1m",
6045
+ white: "\x1B[97m",
6046
+ cyan: "\x1B[36m",
6047
+ green: "\x1B[32m",
6048
+ gray: "\x1B[90m",
6049
+ magenta: "\x1B[35m",
6050
+ yellow: "\x1B[33m",
6051
+ red: "\x1B[31m",
6052
+ teal: "\x1B[38;5;37m"
6053
+ // brand teal (matches the marketing site's teal accent)
6054
+ };
6055
+ var LOGO_MARK = [
6056
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
6057
+ " \u2588\u2588\u2588 \u2588\u2588",
6058
+ " \u2588\u2588 \u2588",
6059
+ "\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588",
6060
+ "\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588",
6061
+ "\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588",
6062
+ "\u2588 \u2588\u2588",
6063
+ "\u2588\u2588 \u2588\u2588\u2588",
6064
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"
6065
+ ];
6066
+ function printUsage() {
6067
+ stdout.write(
6068
+ [
6069
+ "",
6070
+ `${c3.bold}Usage${c3.reset}`,
6071
+ " standardcode [options] [dir]",
6072
+ "",
6073
+ `${c3.bold}Options${c3.reset}`,
6074
+ " -e, --endpoint [url] Use a different Standard Agents instance for this run",
6075
+ " (default: https://api.standardcode.ai).",
6076
+ " If url is omitted, prompt for it.",
6077
+ " Credentials are remembered for that endpoint, but",
6078
+ " the saved default endpoint is not changed.",
6079
+ " -h, --help Show this help.",
6080
+ ""
6081
+ ].join("\n")
6082
+ );
6083
+ }
6084
+ function parseArgs2(args) {
6085
+ const parsed = { help: false, promptEndpoint: false };
6086
+ for (let i = 0; i < args.length; i++) {
6087
+ const arg = args[i];
6088
+ if (arg === "--help" || arg === "-h") {
6089
+ parsed.help = true;
6090
+ continue;
6091
+ }
6092
+ if (arg === "--endpoint" || arg === "-e") {
6093
+ const value = args[i + 1];
6094
+ if (value && !value.startsWith("-")) {
6095
+ parsed.endpoint = value;
6096
+ i++;
6097
+ } else {
6098
+ parsed.promptEndpoint = true;
6099
+ }
6100
+ continue;
6101
+ }
6102
+ if (arg.startsWith("--endpoint=")) {
6103
+ const value = arg.slice("--endpoint=".length);
6104
+ if (value) {
6105
+ parsed.endpoint = value;
6106
+ } else {
6107
+ parsed.promptEndpoint = true;
6108
+ }
4954
6109
  continue;
4955
6110
  }
4956
6111
  if (arg === "--") {
@@ -4973,7 +6128,7 @@ function printAssistant(tui, text) {
4973
6128
  let dotted = false;
4974
6129
  for (const line of renderMarkdown(text, cols2)) {
4975
6130
  if (!dotted && line.trim()) {
4976
- tui.print(`${c.gray}\u2022${c.reset} ${line}`);
6131
+ tui.print(`${c3.gray}\u2022${c3.reset} ${line}`);
4977
6132
  dotted = true;
4978
6133
  } else {
4979
6134
  tui.print(` ${line}`);
@@ -4988,7 +6143,7 @@ function startLoader(label) {
4988
6143
  const draw = () => {
4989
6144
  const now = Date.now();
4990
6145
  const f = frames[Math.floor(now / 70) % frames.length];
4991
- 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}`);
4992
6147
  };
4993
6148
  draw();
4994
6149
  const timer = setInterval(draw, 70);
@@ -5004,53 +6159,25 @@ function farewell(stoppedProcs = 0) {
5004
6159
  if (stoppedProcs > 0) {
5005
6160
  stdout.write(
5006
6161
  `
5007
- ${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"}.
5008
6163
  `
5009
6164
  );
5010
6165
  }
5011
6166
  stdout.write(`
5012
- ${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}
5013
6168
  `);
5014
6169
  }
5015
- function isLocalHost(host) {
5016
- 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);
5017
- }
5018
- function relaxTlsForLocalEndpoint(endpoint) {
5019
- let host = "";
5020
- try {
5021
- host = new URL(endpoint).hostname;
5022
- } catch {
5023
- return false;
5024
- }
5025
- if (!endpoint.startsWith("https:") || !isLocalHost(host)) return false;
5026
- const origEmit = process.emitWarning.bind(process);
5027
- process.emitWarning = ((warning, ...args) => {
5028
- const msg = typeof warning === "string" ? warning : warning?.message ?? "";
5029
- if (msg.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
5030
- return origEmit(warning, ...args);
5031
- });
5032
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
5033
- return true;
5034
- }
5035
- function readVersion() {
5036
- try {
5037
- const pkg = JSON.parse(fs4.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
5038
- return typeof pkg.version === "string" ? pkg.version : "";
5039
- } catch {
5040
- return "";
5041
- }
5042
- }
5043
6170
  function printWelcome(endpoint, projectDir) {
5044
- const home = os6.homedir();
6171
+ const home = os9.homedir();
5045
6172
  const dir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
5046
6173
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
5047
6174
  const version = readVersion();
5048
6175
  const pad = " ";
5049
6176
  const meta = [
5050
- `${c.bold}${gradientText("Standard Code")}${c.reset}${version ? ` ${c.dim}v${version}${c.reset}` : ""}`,
5051
- `${c.dim}terminal coding agent${c.reset}`,
5052
- ...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c.teal}${host}${c.reset}`],
5053
- `${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}`
5054
6181
  ];
5055
6182
  const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
5056
6183
  const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
@@ -5065,11 +6192,11 @@ function printWelcome(endpoint, projectDir) {
5065
6192
  }
5066
6193
  function colorActivity(line) {
5067
6194
  const m = line.match(/^(\s*)([✓✗⛔])\s?([\s\S]*)$/);
5068
- if (!m) return `${c.dim}${line}${c.reset}`;
6195
+ if (!m) return `${c3.dim}${line}${c3.reset}`;
5069
6196
  const [, indent, glyph, rest] = m;
5070
6197
  if (glyph === "\u2713") {
5071
- const body = rest.replace(/\s(\([^()]*\))\s*$/, ` ${c.dim}$1${c.reset}`);
5072
- 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}`;
5073
6200
  }
5074
6201
  if (glyph === "\u2717") {
5075
6202
  const ERR_MAX_LINES = 7;
@@ -5077,22 +6204,26 @@ function colorActivity(line) {
5077
6204
  const shown = lines.slice(0, ERR_MAX_LINES);
5078
6205
  const hidden = lines.length - shown.length;
5079
6206
  const body = shown.map(
5080
- (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}`
5081
6208
  ).join("\n");
5082
6209
  if (hidden > 0) {
5083
6210
  return `${body}
5084
- ${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}`;
5085
6212
  }
5086
6213
  return body;
5087
6214
  }
5088
- return `${indent}${c.yellow}\u26D4 ${rest}${c.reset}`;
6215
+ return `${indent}${c3.yellow}\u26D4 ${rest}${c3.reset}`;
5089
6216
  }
5090
6217
  async function main() {
6218
+ if (process.argv[2] === "daemon") {
6219
+ await runDaemonCommand(process.argv.slice(3));
6220
+ return;
6221
+ }
5091
6222
  let cliArgs;
5092
6223
  try {
5093
6224
  cliArgs = parseArgs2(process.argv.slice(2));
5094
6225
  } catch (error) {
5095
- 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)}
5096
6227
  `);
5097
6228
  printUsage();
5098
6229
  process.exit(1);
@@ -5105,7 +6236,7 @@ async function main() {
5105
6236
  const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
5106
6237
  const dirArg = cliArgs.dir;
5107
6238
  const projectDir = path3.resolve(dirArg || process.cwd());
5108
- const machine = os6.hostname();
6239
+ const machine = os9.hostname();
5109
6240
  const reader = { rl: null };
5110
6241
  let handoffClosing = false;
5111
6242
  let preflightArmed = false;
@@ -5119,7 +6250,7 @@ async function main() {
5119
6250
  }
5120
6251
  preflightArmed = true;
5121
6252
  stdout.write(`
5122
- ${c.dim}Press Control-C again to exit${c.reset}
6253
+ ${c3.dim}Press Control-C again to exit${c3.reset}
5123
6254
  `);
5124
6255
  preflightTimer = setTimeout(() => {
5125
6256
  preflightArmed = false;
@@ -5141,10 +6272,10 @@ ${c.dim}Press Control-C again to exit${c.reset}
5141
6272
  const askEndpoint = async () => {
5142
6273
  for (; ; ) {
5143
6274
  const answer = (await ask(
5144
- `${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): `
5145
6276
  )).trim();
5146
6277
  if (answer) return answer;
5147
- stdout.write(`${c.dim}An endpoint URL is required.${c.reset}
6278
+ stdout.write(`${c3.dim}An endpoint URL is required.${c3.reset}
5148
6279
  `);
5149
6280
  }
5150
6281
  };
@@ -5159,7 +6290,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
5159
6290
  const tlsRelaxed = relaxTlsForLocalEndpoint(endpoint);
5160
6291
  printWelcome(endpoint, projectDir);
5161
6292
  if (tlsRelaxed) {
5162
- 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}
5163
6294
 
5164
6295
  `);
5165
6296
  }
@@ -5171,7 +6302,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
5171
6302
  loading.stop();
5172
6303
  const applied = consumeAppliedUpdate(version);
5173
6304
  if (applied) {
5174
- 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}
5175
6306
 
5176
6307
  `);
5177
6308
  }
@@ -5180,21 +6311,21 @@ ${c.dim}Press Control-C again to exit${c.reset}
5180
6311
  const decision = decideAutoUpdate(updateAvailable, { state: readAutoUpdateState(), pm });
5181
6312
  if (decision === "start" && pm && startBackgroundUpdate(updateAvailable.latest, pm)) {
5182
6313
  stdout.write(
5183
- ` ${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}
5184
6315
 
5185
6316
  `
5186
6317
  );
5187
6318
  } else if (decision === "in_flight") {
5188
6319
  stdout.write(
5189
- ` ${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}
5190
6321
 
5191
6322
  `
5192
6323
  );
5193
6324
  } else {
5194
6325
  const display = updateCommand(pm ?? "npm").display;
5195
6326
  stdout.write(
5196
- ` ${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}
5197
- ${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}
5198
6329
 
5199
6330
  `
5200
6331
  );
@@ -5212,39 +6343,39 @@ ${c.dim}Press Control-C again to exit${c.reset}
5212
6343
  if (!api || !storedCheck?.ok) {
5213
6344
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
5214
6345
  if (storedCheck && !storedCheck.ok) {
5215
- 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}
5216
6347
  `);
5217
- if (storedCheck.hint) stdout.write(` ${c.dim}${storedCheck.hint}${c.reset}
6348
+ if (storedCheck.hint) stdout.write(` ${c3.dim}${storedCheck.hint}${c3.reset}
5218
6349
  `);
5219
6350
  stdout.write("\n");
5220
6351
  }
5221
6352
  const explainFailure = (result, prefix) => {
5222
- stdout.write(`${c.red}\u2717${c.reset} ${prefix}${result.reason}
6353
+ stdout.write(`${c3.red}\u2717${c3.reset} ${prefix}${result.reason}
5223
6354
  `);
5224
- if (result.hint) stdout.write(` ${c.dim}${result.hint}${c.reset}
6355
+ if (result.hint) stdout.write(` ${c3.dim}${result.hint}${c3.reset}
5225
6356
  `);
5226
6357
  };
5227
- 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}
5228
6359
  `);
5229
6360
  if (`https://${host}` !== PRODUCTION_ENDPOINT) {
5230
- 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}
5231
6362
  `);
5232
6363
  }
5233
6364
  stdout.write(
5234
- `${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}
5235
6366
 
5236
6367
  `
5237
6368
  );
5238
6369
  stdout.write(
5239
- `${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}
5240
6371
 
5241
6372
  `
5242
6373
  );
5243
6374
  for (; ; ) {
5244
- const token = (await ask(`${c.teal}\u276F${c.reset} `)).trim();
6375
+ const token = (await ask(`${c3.teal}\u276F${c3.reset} `)).trim();
5245
6376
  if (!token) {
5246
6377
  const got = await deviceLogin(endpoint).catch((e) => {
5247
- 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}
5248
6379
  `);
5249
6380
  return null;
5250
6381
  });
@@ -5258,7 +6389,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
5258
6389
  { endpoint, access_token: got, token_type: "Bearer", saved_at: Date.now() },
5259
6390
  { updateDefault: !endpointOverride }
5260
6391
  );
5261
- 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}
5262
6393
  `);
5263
6394
  break;
5264
6395
  }
@@ -5274,7 +6405,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
5274
6405
  { endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
5275
6406
  { updateDefault: !endpointOverride }
5276
6407
  );
5277
- 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}
5278
6409
  `);
5279
6410
  break;
5280
6411
  }
@@ -5286,17 +6417,72 @@ ${c.dim}Press Control-C again to exit${c.reset}
5286
6417
  if (!api) process.exit(1);
5287
6418
  handoffClosing = true;
5288
6419
  reader.rl?.close();
5289
- 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
+ }
5290
6469
  const loadingSessions = startLoader("Loading sessions");
5291
6470
  let existing = [];
5292
6471
  try {
5293
- existing = await api.listThreads(AGENT_ID_VARIANTS, tags);
6472
+ existing = await api.listThreads(AGENT_ID_VARIANTS, resumeTags);
5294
6473
  } catch {
5295
6474
  existing = [];
5296
6475
  }
5297
6476
  const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
5298
6477
  loadingSessions.stop();
5299
- 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
+ };
5300
6486
  let threadId;
5301
6487
  let resumed = false;
5302
6488
  let historySeed;
@@ -5307,31 +6493,61 @@ ${c.dim}Press Control-C again to exit${c.reset}
5307
6493
  value: s.id
5308
6494
  }));
5309
6495
  items.push({ label: "\uFF0B Start a new session", value: null });
5310
- const home = os6.homedir();
5311
- const tilde = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
5312
- 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;
5313
6497
  const picked = await tui.select(
5314
- `${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}`,
5315
6499
  items
5316
6500
  );
5317
6501
  if (typeof picked === "string") {
5318
6502
  threadId = picked;
5319
6503
  resumed = true;
5320
6504
  } else {
5321
- threadId = await api.createThread(AGENT_ID, tags);
6505
+ threadId = await createSessionThread();
5322
6506
  historySeed = existing[0]?.id;
5323
6507
  }
5324
6508
  } else {
5325
- threadId = await api.createThread(AGENT_ID, tags);
6509
+ threadId = await createSessionThread();
5326
6510
  }
5327
6511
  for (; ; ) {
5328
- await runInteractive(tui, api, threadId, projectDir, machine, resumed, historySeed);
6512
+ await runInteractive(tui, api, threadId, projectDir, machine, resumed, session, historySeed);
5329
6513
  historySeed = threadId;
5330
- threadId = await api.createThread(AGENT_ID, tags);
6514
+ threadId = await createSessionThread();
5331
6515
  await api.kvSet(threadId, "lease_supersedes", historySeed);
5332
6516
  resumed = false;
5333
6517
  }
5334
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
+ }
5335
6551
  function isSilentMessage(m) {
5336
6552
  return m?.silent === true || m?.metadata?.silent === true;
5337
6553
  }
@@ -5354,46 +6570,10 @@ async function summarizeThreads(api, threads) {
5354
6570
  }
5355
6571
  function subagentLabel(s, titles) {
5356
6572
  const agentName = (s.agent_name || "").trim();
5357
- 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");
5358
6574
  const tagged = (s.threadName || "").trim();
5359
6575
  return tagged ? `${title} \xB7 ${tagged}` : title;
5360
6576
  }
5361
- async function deviceLogin(endpoint) {
5362
- const start = await fetch(`${endpoint}/api/auth/device/start`, { method: "POST" });
5363
- if (!start.ok) throw new Error(`This instance does not support browser sign-in (HTTP ${start.status}). Paste an API token instead.`);
5364
- const info = await start.json();
5365
- stdout.write(`${c.dim}Opening your browser to sign in. If it doesn't open, visit:${c.reset}
5366
- `);
5367
- stdout.write(`
5368
- ${c.teal}${info.verify_url}${c.reset}
5369
-
5370
- `);
5371
- stdout.write(`${c.dim}Waiting for sign-in to complete\u2026 (Ctrl-C to cancel)${c.reset}
5372
- `);
5373
- openUrl(info.verify_url);
5374
- const deadline = Date.now() + (info.expires_in ?? 600) * 1e3;
5375
- const interval = Math.max(2, info.interval ?? 2) * 1e3;
5376
- while (Date.now() < deadline) {
5377
- await new Promise((r) => setTimeout(r, interval));
5378
- const res = await fetch(info.poll_url).catch(() => null);
5379
- if (!res) continue;
5380
- if (res.status === 404) throw new Error("The sign-in link expired. Try again.");
5381
- const body = await res.json().catch(() => ({}));
5382
- if (body.status === "approved" && body.token) return body.token;
5383
- if (body.status === "denied") throw new Error("Sign-in was denied in the browser.");
5384
- }
5385
- throw new Error("Timed out waiting for browser approval. Try again.");
5386
- }
5387
- function openUrl(url) {
5388
- const platform = process.platform;
5389
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
5390
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
5391
- try {
5392
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
5393
- child.unref();
5394
- } catch {
5395
- }
5396
- }
5397
6577
  function relativeTime(unixSeconds) {
5398
6578
  const diff = Date.now() / 1e3 - unixSeconds;
5399
6579
  if (diff < 60) return "just now";
@@ -5436,8 +6616,8 @@ async function printHistory(api, threadId, tui) {
5436
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));
5437
6617
  if (!convo.length) return;
5438
6618
  const shown = convo.slice(-24);
5439
- tui.print(`${c.dim}\u2500\u2500 resuming session \xB7 ${convo.length} message${convo.length === 1 ? "" : "s"} \u2500\u2500${c.reset}`);
5440
- 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}`);
5441
6621
  for (const m of shown) {
5442
6622
  const text = messageText(m.content).trim();
5443
6623
  if (!text) continue;
@@ -5445,12 +6625,16 @@ async function printHistory(api, threadId, tui) {
5445
6625
  else printAssistant(tui, text);
5446
6626
  }
5447
6627
  }
5448
- 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";
5449
6631
  const registry = new ProcessRegistry(api, threadId, machine);
5450
- const mcp = new McpManager(projectDir);
5451
- const publishMcpCatalog = () => void api.kvSet(threadId, "mcp_catalog", mcp.catalog()).catch(() => {
5452
- });
5453
- 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);
5454
6638
  const refreshBgCount = () => {
5455
6639
  void registry.runningCount().then((n) => tui.setBackgroundCount(n)).catch(() => {
5456
6640
  });
@@ -5468,8 +6652,6 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
5468
6652
  saveApprovals(api, threadId, perm);
5469
6653
  });
5470
6654
  saveApprovals(api, threadId, perm);
5471
- void api.kvSet(threadId, "session_info", { cwd: projectDir, machine }).catch(() => {
5472
- });
5473
6655
  const attaching = startLoader("Attaching to thread");
5474
6656
  let busy = false;
5475
6657
  let interrupting = false;
@@ -5490,35 +6672,76 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
5490
6672
  for (const v of activeSteps.values()) label = v;
5491
6673
  tui.setStep(label, liveOut);
5492
6674
  };
5493
- const bridge = new Bridge(api, threadId, host, perm, {
5494
- onActivity: (line, detail) => {
5495
- tui.print(colorActivity(line));
5496
- if (detail) for (const d of detail) tui.print(d);
5497
- refreshBgCount();
5498
- },
5499
- onStatus: (id, summary) => {
5500
- if (summary) {
5501
- if (!activeSteps.has(id)) activeSteps.set(id, summary);
5502
- } else {
5503
- activeSteps.delete(id);
5504
- }
5505
- refreshStatus();
5506
- },
5507
- onConnection: (state, attempt) => {
5508
- if (state === "reconnecting") {
5509
- if (attempt >= 4) tui.setConnected(false);
5510
- } else {
5511
- tui.setConnected(true);
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
6735
+ );
5512
6736
  }
5513
6737
  },
5514
- requestApproval: async (req, summary, risk) => {
5515
- return tui.approval(
5516
- `${summary}${req.requestPermission ? `
5517
- why: ${req.requestPermission}` : ""}`,
5518
- risk
5519
- );
6738
+ {
6739
+ clientId: interactiveClientId(session.identity),
6740
+ clientName: `${machineDisplayName(session.identity)} (terminal)`,
6741
+ clientKind: "interactive",
6742
+ claim
5520
6743
  }
5521
- });
6744
+ );
5522
6745
  const stream = new MessageStream(api, threadId, {
5523
6746
  // Live streaming preview: answer text and (opt-in) internal reasoning feed
5524
6747
  // the TUI's ephemeral preview; the committed message still renders from
@@ -5615,14 +6838,14 @@ why: ${req.requestPermission}` : ""}`,
5615
6838
  const sessionEnded = new Promise((r) => endSession = r);
5616
6839
  const quit = async () => {
5617
6840
  tui.end();
5618
- const stopped2 = api.stop(threadId).catch(() => {
5619
- });
6841
+ const stopped2 = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
6842
+ }) : Promise.resolve();
5620
6843
  const procsStopped2 = host.stopAllLocalProcesses().catch(() => 0);
5621
- bridge.close();
6844
+ bridge?.close();
5622
6845
  stream.close();
5623
6846
  events.close();
5624
6847
  subActivity.closeAll();
5625
- mcp.closeAll();
6848
+ mcp?.closeAll();
5626
6849
  const [, killed2] = await Promise.race([
5627
6850
  Promise.all([stopped2, procsStopped2]),
5628
6851
  new Promise((r) => setTimeout(() => r([void 0, 0]), 1500))
@@ -5634,21 +6857,28 @@ why: ${req.requestPermission}` : ""}`,
5634
6857
  const logout = async () => {
5635
6858
  deleteCredential(api.origin);
5636
6859
  const instanceHost = api.origin.replace(/^https?:\/\//, "");
5637
- 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}`);
5638
6861
  await quit();
5639
6862
  };
5640
6863
  const bgMgr = {
5641
6864
  list: () => registry.list(),
5642
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
+ }
5643
6872
  await host.execute("background_process", { action: "stop", id });
5644
6873
  refreshBgCount();
5645
6874
  }
5646
6875
  };
5647
6876
  const mcpCtl = {
5648
6877
  configured: () => listMcpServers(),
5649
- connectedNames: () => mcp.connectedNames(),
5650
- catalog: () => mcp.catalog(),
6878
+ connectedNames: () => mcp?.connectedNames() ?? [],
6879
+ catalog: () => mcp?.catalog() ?? { servers: [] },
5651
6880
  connect: async (cfg) => {
6881
+ if (!mcp) return { ok: false, error: "MCP servers run on the session's machine." };
5652
6882
  try {
5653
6883
  const client = await mcp.connect(cfg);
5654
6884
  publishMcpCatalog();
@@ -5659,7 +6889,7 @@ why: ${req.requestPermission}` : ""}`,
5659
6889
  }
5660
6890
  },
5661
6891
  disconnect: (name) => {
5662
- mcp.disconnect(name);
6892
+ mcp?.disconnect(name);
5663
6893
  publishMcpCatalog();
5664
6894
  },
5665
6895
  add: async (cfg) => {
@@ -5675,7 +6905,7 @@ why: ${req.requestPermission}` : ""}`,
5675
6905
  );
5676
6906
  },
5677
6907
  remove: (name) => {
5678
- mcp.disconnect(name);
6908
+ mcp?.disconnect(name);
5679
6909
  removeMcpServer(name);
5680
6910
  publishMcpCatalog();
5681
6911
  },
@@ -5694,7 +6924,7 @@ why: ${req.requestPermission}` : ""}`,
5694
6924
  const n = (pendingSent.get(key) ?? 1) - 1;
5695
6925
  if (n > 0) pendingSent.set(key, n);
5696
6926
  else pendingSent.delete(key);
5697
- 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}`);
5698
6928
  return;
5699
6929
  }
5700
6930
  interrupting = false;
@@ -5711,16 +6941,16 @@ why: ${req.requestPermission}` : ""}`,
5711
6941
  try {
5712
6942
  await api.compact(threadId);
5713
6943
  } catch (err) {
5714
- 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}`);
5715
6945
  }
5716
6946
  };
5717
6947
  const runAccountCommand = async () => {
5718
- tui.print(`${c.gray}Opening your account\u2026${c.reset}`);
6948
+ tui.print(`${c3.gray}Opening your account\u2026${c3.reset}`);
5719
6949
  const link = await api.accountLink(threadId).catch(() => null);
5720
6950
  const target = link?.url ?? "https://standardcode.ai/account";
5721
6951
  openUrl(target);
5722
6952
  tui.print(
5723
- 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}`
5724
6954
  );
5725
6955
  };
5726
6956
  const ordinal = (n) => {
@@ -5731,24 +6961,24 @@ why: ${req.requestPermission}` : ""}`,
5731
6961
  const renderUpgradePanel = (q) => {
5732
6962
  const dots = [];
5733
6963
  for (let i = 0; i < q.max; i++) {
5734
- if (i < q.current) dots.push(`${c.teal}\u25CF${c.reset}`);
5735
- else if (i === q.current) dots.push(`${c.bold}${gradientText("\uFF0B")}${c.reset}`);
5736
- 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}`);
5737
6967
  }
5738
6968
  const cost = fmtCost(q);
5739
6969
  const lines = [
5740
6970
  "",
5741
- `${c.bold}${gradientText("\u2726 Add a parallel session")}${c.reset}`,
6971
+ `${c3.bold}${gradientText("\u2726 Add a parallel session")}${c3.reset}`,
5742
6972
  "",
5743
- `${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}`
5744
6974
  ];
5745
6975
  if (q.ends_trial) {
5746
6976
  lines.push(
5747
- `${c.yellow}Your $5 trial covers 1 session. Adding a ${ordinal(q.sessions)} ends the trial${c.reset}`,
5748
- `${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}`
5749
6979
  );
5750
6980
  } else if (cost) {
5751
- 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}.`);
5752
6982
  } else {
5753
6983
  lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 prorated on your next invoice.`);
5754
6984
  }
@@ -5761,7 +6991,7 @@ why: ${req.requestPermission}` : ""}`,
5761
6991
  try {
5762
6992
  if (opts.auto) {
5763
6993
  tui.print(
5764
- `${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}`
5765
6995
  );
5766
6996
  }
5767
6997
  const quote = await api.sessionsQuote(threadId);
@@ -5769,16 +6999,16 @@ why: ${req.requestPermission}` : ""}`,
5769
6999
  const link = await api.accountLink(threadId).catch(() => null);
5770
7000
  const target = link?.url ?? "https://standardcode.ai/account";
5771
7001
  tui.print(
5772
- `${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}`
5773
7003
  );
5774
7004
  openUrl(target);
5775
- 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}`);
5776
7006
  return;
5777
7007
  }
5778
7008
  if (quote.current >= quote.max) {
5779
7009
  tui.print(
5780
- `${c.yellow}You're at the maximum of ${quote.max} parallel session${quote.max === 1 ? "" : "s"}.${c.reset}
5781
- ${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}`
5782
7012
  );
5783
7013
  return;
5784
7014
  }
@@ -5790,25 +7020,25 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5790
7020
  { label: "Not now", value: "no" }
5791
7021
  ]);
5792
7022
  if (choice !== "go") {
5793
- 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}`);
5794
7024
  return;
5795
7025
  }
5796
- tui.print(`${c.gray}Applying\u2026${c.reset}`);
7026
+ tui.print(`${c3.gray}Applying\u2026${c3.reset}`);
5797
7027
  let applied;
5798
7028
  try {
5799
7029
  applied = await api.sessionsUpgrade(threadId, quote.sessions);
5800
7030
  } catch (e) {
5801
- 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)}`);
5802
7032
  return;
5803
7033
  }
5804
7034
  if (!applied?.ok) {
5805
- 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"}`);
5806
7036
  return;
5807
7037
  }
5808
7038
  const n = applied.sessions ?? quote.sessions;
5809
- 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}`);
5810
7040
  if (opts.auto && lastSent) {
5811
- tui.print(`${c.gray}Continuing\u2026${c.reset}`);
7041
+ tui.print(`${c3.gray}Continuing\u2026${c3.reset}`);
5812
7042
  await sendNow(lastSent.text, lastSent.images);
5813
7043
  }
5814
7044
  } finally {
@@ -5852,14 +7082,24 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5852
7082
  },
5853
7083
  run: () => runApprovalsMenu(tui, perm, () => saveApprovals(api, threadId, perm))
5854
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
+ ],
5855
7098
  {
5856
- name: "mcp",
5857
- label: "MCP servers",
5858
- hint: () => {
5859
- const n = mcpCtl.connectedNames().length;
5860
- return n ? `${n} connected` : "none";
5861
- },
5862
- 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)
5863
7103
  },
5864
7104
  {
5865
7105
  name: "skills",
@@ -5893,20 +7133,20 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5893
7133
  editingQueued = false;
5894
7134
  queued.push({ text, images });
5895
7135
  tui.setQueuedCount(queued.length);
5896
- tui.print(`${c.gray}\u23F3 queued:${c.reset} ${text}`);
7136
+ tui.print(`${c3.gray}\u23F3 queued:${c3.reset} ${text}`);
5897
7137
  return;
5898
7138
  }
5899
7139
  if (busy) {
5900
7140
  queued.push({ text, images });
5901
7141
  tui.setQueuedCount(queued.length);
5902
- 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}`);
5903
7143
  } else {
5904
7144
  void sendNow(text, images);
5905
7145
  }
5906
7146
  };
5907
7147
  tui.onInterrupt = () => {
5908
7148
  if (queued.length > 0) {
5909
- 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}`);
5910
7150
  void api.stop(threadId).catch(() => {
5911
7151
  }).then(() => flushQueued());
5912
7152
  } else if (busy) {
@@ -5916,7 +7156,7 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5916
7156
  liveOut = 0;
5917
7157
  tui.setWorking(false);
5918
7158
  refreshStatus();
5919
- tui.print(`${c.yellow}[interrupted by user]${c.reset}`);
7159
+ tui.print(`${c3.yellow}[interrupted by user]${c3.reset}`);
5920
7160
  void api.stop(threadId).catch(() => {
5921
7161
  });
5922
7162
  }
@@ -5934,15 +7174,27 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5934
7174
  return true;
5935
7175
  };
5936
7176
  events.connect();
5937
- 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;
5938
7179
  void api.getGoal(threadId).then((g) => tui.setGoal(g)).catch(() => {
5939
7180
  });
5940
7181
  attaching.stop();
5941
- tui.banner([
5942
- `${c.bold}${c.magenta}Standard Code${c.reset} ${c.dim}\u2014 coding agent${c.reset}`,
5943
- `${c.gray}project:${c.reset} ${projectDir}`,
5944
- `${c.gray}machine:${c.reset} ${machine} ${c.gray}thread:${c.reset} ${threadId.slice(0, 8)}`
5945
- ]);
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
+ }
5946
7198
  if (resumed) await printHistory(api, threadId, tui);
5947
7199
  try {
5948
7200
  (await api.getMessages(threadId, 200)).forEach((m) => shownIds.add(m.id));
@@ -5951,22 +7203,50 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5951
7203
  const runningProcs = (await registry.list()).filter((p) => p.status === "running");
5952
7204
  if (runningProcs.length) {
5953
7205
  tui.print(
5954
- `${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}`
5955
7207
  );
5956
- 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}`);
5957
7209
  }
5958
7210
  refreshBgCount();
5959
- const enabledServers = listMcpServers().filter((s) => s.enabled);
5960
- for (const s of enabledServers) {
5961
- const res = await mcpCtl.connect(s);
5962
- if (res.ok) {
5963
- tui.print(`${c.cyan}\u26A1 MCP "${s.name}" connected${c.reset} ${c.gray}(${res.tools} tool${res.tools === 1 ? "" : "s"})${c.reset}`);
5964
- } else {
5965
- 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
+ }
5966
7220
  }
7221
+ publishMcpCatalog();
5967
7222
  }
5968
- publishMcpCatalog();
5969
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
+ };
5970
7250
  const poll = async () => {
5971
7251
  let msgs;
5972
7252
  try {
@@ -5985,7 +7265,7 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
5985
7265
  continue;
5986
7266
  }
5987
7267
  if (m.role === "assistant" && text) printAssistant(tui, text);
5988
- 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}`);
5989
7269
  else if (m.role === "user" && text) {
5990
7270
  const pending = pendingSent.get(text) ?? 0;
5991
7271
  if (pending > 0) {
@@ -6011,6 +7291,8 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
6011
7291
  if (queued.length > 0 && !editingQueued) await flushQueued();
6012
7292
  }
6013
7293
  refreshBgCount();
7294
+ void relayApprovals().catch(() => {
7295
+ });
6014
7296
  try {
6015
7297
  const logs = await api.getLogs(threadId, 100);
6016
7298
  let landed = 0;
@@ -6047,14 +7329,14 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
6047
7329
  await sessionEnded;
6048
7330
  clearInterval(pollTimer);
6049
7331
  clearInterval(heartbeatPoll);
6050
- const stopped = api.stop(threadId).catch(() => {
6051
- });
7332
+ const stopped = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
7333
+ }) : Promise.resolve();
6052
7334
  const procsStopped = host.stopAllLocalProcesses().catch(() => 0);
6053
- bridge.close();
7335
+ bridge?.close();
6054
7336
  stream.close();
6055
7337
  events.close();
6056
7338
  subActivity.closeAll();
6057
- mcp.closeAll();
7339
+ mcp?.closeAll();
6058
7340
  const [, killed] = await Promise.race([
6059
7341
  Promise.all([stopped, procsStopped]),
6060
7342
  new Promise((r) => setTimeout(() => r([void 0, 0]), 1500))
@@ -6067,16 +7349,16 @@ ${c.gray}Close another session (its slot frees within ~90s), then resend your me
6067
7349
  tui.setStep(null, 0);
6068
7350
  tui.setBackgroundCount(0);
6069
7351
  if (killed > 0) {
6070
- 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"}.`);
6071
7353
  }
6072
- 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}`);
6073
7355
  }
6074
7356
  async function runSkillsMenu(tui, skills) {
6075
7357
  let list;
6076
7358
  try {
6077
7359
  list = await skills.list();
6078
7360
  } catch (e) {
6079
- 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}`);
6080
7362
  return;
6081
7363
  }
6082
7364
  const INSTALL = "__install__";
@@ -6087,7 +7369,7 @@ async function runSkillsMenu(tui, skills) {
6087
7369
  }));
6088
7370
  items.push({ label: "\uFF0B Install a skill\u2026", hint: "find & install", value: INSTALL });
6089
7371
  const picked = await tui.select(
6090
- `${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}`,
6091
7373
  items
6092
7374
  );
6093
7375
  if (!picked) return;
@@ -6100,8 +7382,8 @@ async function runSkillsMenu(tui, skills) {
6100
7382
  return;
6101
7383
  }
6102
7384
  const skill = list.find((s) => s.name === picked);
6103
- tui.print(`${c.cyan}${skill.name}${c.reset}${skill.version ? ` ${c.dim}v${skill.version}${c.reset}` : ""} ${c.gray}\u2014 ${skill.description}${c.reset}`);
6104
- 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}`, [
6105
7387
  skill.enabled ? { label: "Disable (hide from the agent)", value: "disable" } : { label: "Enable", value: "enable" },
6106
7388
  { label: "View files", value: "files" },
6107
7389
  { label: "Remove this skill", value: "remove" },
@@ -6110,20 +7392,20 @@ async function runSkillsMenu(tui, skills) {
6110
7392
  try {
6111
7393
  if (action === "enable" || action === "disable") {
6112
7394
  await skills.setEnabled(picked, action === "enable");
6113
- tui.print(`${c.gray}${action}d ${picked}${c.reset}`);
7395
+ tui.print(`${c3.gray}${action}d ${picked}${c3.reset}`);
6114
7396
  } else if (action === "files") {
6115
- 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}`);
6116
7398
  } else if (action === "remove") {
6117
7399
  await skills.remove(picked);
6118
- tui.print(`${c.gray}removed ${picked}${c.reset}`);
7400
+ tui.print(`${c3.gray}removed ${picked}${c3.reset}`);
6119
7401
  }
6120
7402
  } catch (e) {
6121
- 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}`);
6122
7404
  }
6123
7405
  }
6124
7406
  async function runLevelMenu(tui, perm) {
6125
7407
  const picked = await tui.select(
6126
- `${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}`,
6127
7409
  LEVELS.map((l) => ({
6128
7410
  label: levelLabel(l),
6129
7411
  hint: l === tui.level ? "current" : "",
@@ -6135,28 +7417,51 @@ async function runLevelMenu(tui, perm) {
6135
7417
  perm.level = picked;
6136
7418
  }
6137
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
+ }
6138
7443
  function showKeybindings(tui) {
6139
- tui.print(`${c.gray}shortcuts:${c.reset}`);
6140
- tui.print(`${c.gray} shift-tab${c.reset} cycle auto-accept level (1\u20135)`);
6141
- tui.print(`${c.gray} /${c.reset} open the command palette (type to filter)`);
6142
- tui.print(`${c.gray} ctrl-v${c.reset} paste an image from the clipboard ([#Image 1])`);
6143
- tui.print(`${c.gray} \u2191 / \u2193${c.reset} cycle past messages (on the input's top line)`);
6144
- tui.print(`${c.gray} \u2190${c.reset} from the start of the input: select the [\u2699 n bg] badge (enter opens it)`);
6145
- 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`);
6146
7451
  }
6147
7452
  async function runUpdateCommand(tui) {
6148
7453
  const version = readVersion();
6149
7454
  const result = await forceCheckForUpdate(version);
6150
7455
  if (!result) {
6151
- 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})`);
6152
7457
  return;
6153
7458
  }
6154
7459
  const { latest } = result;
6155
7460
  tui.print(`
6156
- ${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}`);
6157
7462
  const pm = detectPackageManager();
6158
7463
  if (!pm) {
6159
- 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}`);
6160
7465
  return;
6161
7466
  }
6162
7467
  const { display } = updateCommand(pm);
@@ -6165,28 +7470,28 @@ async function runUpdateCommand(tui) {
6165
7470
  { label: "No, skip", value: "no" }
6166
7471
  ]);
6167
7472
  if (choice === "yes") {
6168
- tui.print(` ${c.gray}Running ${display}\u2026${c.reset}`);
7473
+ tui.print(` ${c3.gray}Running ${display}\u2026${c3.reset}`);
6169
7474
  const { ok, output: pmOutput } = await runUpdate(pm);
6170
7475
  if (ok) {
6171
- 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.`);
6172
7477
  } else {
6173
- tui.print(` ${c.red}\u2717${c.reset} Update failed:`);
7478
+ tui.print(` ${c3.red}\u2717${c3.reset} Update failed:`);
6174
7479
  for (const line of pmOutput.trim().split("\n").slice(-6)) {
6175
- tui.print(` ${c.dim}${line}${c.reset}`);
7480
+ tui.print(` ${c3.dim}${line}${c3.reset}`);
6176
7481
  }
6177
7482
  }
6178
7483
  } else {
6179
- tui.print(` ${c.gray}Skipped. Run /update later.${c.reset}`);
7484
+ tui.print(` ${c3.gray}Skipped. Run /update later.${c3.reset}`);
6180
7485
  }
6181
7486
  }
6182
7487
  async function runProcessMenu(tui, bg) {
6183
7488
  const procs = await bg.list();
6184
7489
  if (!procs.length) {
6185
- 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}`);
6186
7491
  return;
6187
7492
  }
6188
7493
  const items = procs.map((p) => {
6189
- 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}`;
6190
7495
  return {
6191
7496
  label: `${p.description || p.command}`,
6192
7497
  hint: `${p.id} \xB7 ${status}`,
@@ -6194,22 +7499,22 @@ async function runProcessMenu(tui, bg) {
6194
7499
  };
6195
7500
  });
6196
7501
  const picked = await tui.select(
6197
- `${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}`,
6198
7503
  items
6199
7504
  );
6200
7505
  if (!picked) return;
6201
7506
  const proc = procs.find((p) => p.id === picked);
6202
7507
  if (!proc || proc.status !== "running") {
6203
- tui.print(`${c.gray}${picked} is not running.${c.reset}`);
7508
+ tui.print(`${c3.gray}${picked} is not running.${c3.reset}`);
6204
7509
  return;
6205
7510
  }
6206
- 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}`, [
6207
7512
  { label: "Stop this process", value: "stop" },
6208
7513
  { label: "Leave it running", value: "leave" }
6209
7514
  ]);
6210
7515
  if (action === "stop") {
6211
7516
  await bg.stop(picked);
6212
- tui.print(`${c.gray}stopped ${picked}${c.reset}`);
7517
+ tui.print(`${c3.gray}stopped ${picked}${c3.reset}`);
6213
7518
  }
6214
7519
  }
6215
7520
  async function runApprovalsMenu(tui, perm, save) {
@@ -6217,7 +7522,7 @@ async function runApprovalsMenu(tui, perm, save) {
6217
7522
  const risks = Array.from(perm.allowRisk).sort((a, b) => a - b);
6218
7523
  if (!tools.length && !risks.length) {
6219
7524
  tui.print(
6220
- `${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}`
6221
7526
  );
6222
7527
  return;
6223
7528
  }
@@ -6227,22 +7532,22 @@ async function runApprovalsMenu(tui, perm, save) {
6227
7532
  { label: "Clear all approvals", hint: "", value: "clear" }
6228
7533
  ];
6229
7534
  const picked = await tui.select(
6230
- `${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}`,
6231
7536
  items
6232
7537
  );
6233
7538
  if (!picked) return;
6234
7539
  if (picked === "clear") {
6235
7540
  perm.alwaysAllow.clear();
6236
7541
  perm.allowRisk.clear();
6237
- tui.print(`${c.gray}cleared all approvals${c.reset}`);
7542
+ tui.print(`${c3.gray}cleared all approvals${c3.reset}`);
6238
7543
  } else if (picked.startsWith("tool:")) {
6239
7544
  const t = picked.slice(5);
6240
7545
  perm.alwaysAllow.delete(t);
6241
- tui.print(`${c.gray}revoked tool ${t}${c.reset}`);
7546
+ tui.print(`${c3.gray}revoked tool ${t}${c3.reset}`);
6242
7547
  } else if (picked.startsWith("risk:")) {
6243
7548
  const r = Number(picked.slice(5));
6244
7549
  perm.allowRisk.delete(r);
6245
- tui.print(`${c.gray}revoked level ${r}${c.reset}`);
7550
+ tui.print(`${c3.gray}revoked level ${r}${c3.reset}`);
6246
7551
  }
6247
7552
  save();
6248
7553
  }
@@ -6260,7 +7565,7 @@ async function runMcpMenu(tui, mcp) {
6260
7565
  items.push({ label: "\uFF0B Install a new MCP server\u2026", hint: "find & install", value: INSTALL });
6261
7566
  items.push({ label: "Add manually (name: command)\u2026", hint: "advanced", value: ADD_MANUAL });
6262
7567
  const picked = await tui.select(
6263
- `${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}`,
6264
7569
  items
6265
7570
  );
6266
7571
  if (!picked) return;
@@ -6274,7 +7579,7 @@ async function runMcpMenu(tui, mcp) {
6274
7579
  }
6275
7580
  const server = configured.find((s) => s.name === picked);
6276
7581
  const isConnected = connected.has(picked);
6277
- const action = await tui.select(`${c.bold}${picked}${c.reset}`, [
7582
+ const action = await tui.select(`${c3.bold}${picked}${c3.reset}`, [
6278
7583
  { label: "View tools", value: "tools" },
6279
7584
  isConnected ? { label: "Disconnect", value: "disconnect" } : { label: "Connect", value: "connect" },
6280
7585
  server.enabled ? { label: "Disable (don't auto-connect)", value: "disable" } : { label: "Enable (auto-connect on start)", value: "enable" },
@@ -6284,29 +7589,29 @@ async function runMcpMenu(tui, mcp) {
6284
7589
  if (action === "tools") {
6285
7590
  const entry = mcp.catalog().servers.find((e) => e.name === picked);
6286
7591
  if (!entry || entry.status !== "connected") {
6287
- 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}`);
6288
7593
  return;
6289
7594
  }
6290
- if (!entry.tools.length) tui.print(`${c.gray}${picked} exposes no tools.${c.reset}`);
6291
- for (const t of entry.tools) tui.print(` ${c.cyan}${t.name}${c.reset}${t.description ? ` ${c.gray}\u2014 ${t.description}${c.reset}` : ""}`);
6292
- 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}`);
6293
7598
  } else if (action === "connect") {
6294
7599
  const res = await mcp.connect(server);
6295
- 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}`);
6296
7601
  } else if (action === "disconnect") {
6297
7602
  mcp.disconnect(picked);
6298
- tui.print(`${c.gray}disconnected ${picked}${c.reset}`);
7603
+ tui.print(`${c3.gray}disconnected ${picked}${c3.reset}`);
6299
7604
  } else if (action === "enable") {
6300
7605
  mcp.setEnabled(picked, true);
6301
7606
  const res = await mcp.connect(server);
6302
- 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}`);
6303
7608
  } else if (action === "disable") {
6304
7609
  mcp.setEnabled(picked, false);
6305
7610
  mcp.disconnect(picked);
6306
- tui.print(`${c.gray}disabled + disconnected ${picked}${c.reset}`);
7611
+ tui.print(`${c3.gray}disabled + disconnected ${picked}${c3.reset}`);
6307
7612
  } else if (action === "remove") {
6308
7613
  mcp.remove(picked);
6309
- tui.print(`${c.gray}removed ${picked}${c.reset}`);
7614
+ tui.print(`${c3.gray}removed ${picked}${c3.reset}`);
6310
7615
  }
6311
7616
  }
6312
7617
  async function addMcpServer(tui, mcp) {
@@ -6317,13 +7622,13 @@ async function addMcpServer(tui, mcp) {
6317
7622
  if (!spec) return;
6318
7623
  const cfg = parseServerSpec(spec);
6319
7624
  if (!cfg) {
6320
- 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}`);
6321
7626
  return;
6322
7627
  }
6323
- 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}`);
6324
7629
  const res = await mcp.add(cfg);
6325
- 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}`);
6326
- 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}`);
6327
7632
  }
6328
7633
  async function installMcpServerFlow(tui, mcp) {
6329
7634
  const query = await tui.prompt(