@standardagents/code 0.13.0 → 0.13.1

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
@@ -632,6 +632,17 @@ var ApiClient = class {
632
632
  }
633
633
  /** Write one value to the signed-in user's account-wide KV (null deletes). */
634
634
  async userKvSet(key, value) {
635
+ if (key.startsWith("standardcode.machine.")) {
636
+ try {
637
+ await this.json(`/api/users/me/standardcode/machine_value`, {
638
+ method: "POST",
639
+ body: JSON.stringify({ key, value })
640
+ });
641
+ return;
642
+ } catch (error) {
643
+ if (!(error instanceof ApiHttpError) || error.status !== 404) throw error;
644
+ }
645
+ }
635
646
  await this.json(`/api/users/me/kv`, {
636
647
  method: "POST",
637
648
  body: JSON.stringify({ key, value })
@@ -3095,6 +3106,147 @@ var MessageStream = class {
3095
3106
  }
3096
3107
  }
3097
3108
  };
3109
+ var AccountUserStream = class {
3110
+ constructor(api, clientId, options) {
3111
+ this.api = api;
3112
+ this.clientId = clientId;
3113
+ this.options = options;
3114
+ }
3115
+ api;
3116
+ clientId;
3117
+ options;
3118
+ ws = null;
3119
+ closed = false;
3120
+ started = false;
3121
+ heartbeat = null;
3122
+ reconnectAttempt = 0;
3123
+ reconnectTimer = null;
3124
+ eventListeners = /* @__PURE__ */ new Set();
3125
+ connectionListeners = /* @__PURE__ */ new Set();
3126
+ connected = false;
3127
+ start() {
3128
+ if (this.started || this.closed) return;
3129
+ this.started = true;
3130
+ this.openSocket();
3131
+ }
3132
+ onEvent(listener) {
3133
+ this.eventListeners.add(listener);
3134
+ return () => this.eventListeners.delete(listener);
3135
+ }
3136
+ onConnection(listener) {
3137
+ this.connectionListeners.add(listener);
3138
+ return () => this.connectionListeners.delete(listener);
3139
+ }
3140
+ /** Wait for a live socket. The timeout is local and performs no network work. */
3141
+ waitUntilConnected(timeoutMs = 15e3) {
3142
+ if (this.connected) return Promise.resolve();
3143
+ this.start();
3144
+ return new Promise((resolve, reject) => {
3145
+ let timer;
3146
+ const remove = this.onConnection((state) => {
3147
+ if (state !== "connected") return;
3148
+ clearTimeout(timer);
3149
+ remove();
3150
+ resolve();
3151
+ });
3152
+ timer = setTimeout(() => {
3153
+ remove();
3154
+ reject(new Error("The account stream did not connect in time."));
3155
+ }, timeoutMs);
3156
+ });
3157
+ }
3158
+ openSocket() {
3159
+ if (this.closed) return;
3160
+ const clientId = this.options.uniqueConnectionId ? `${this.clientId}:${crypto.randomBytes(8).toString("hex")}` : this.clientId;
3161
+ let url = `${this.api.wsEndpoint}/api/users/me/stream?token=${encodeURIComponent(this.api.bearer)}&client_id=${encodeURIComponent(clientId)}&client_kind=${encodeURIComponent(this.options.clientKind)}`;
3162
+ if (this.options.clientName) url += `&client_name=${encodeURIComponent(this.options.clientName)}`;
3163
+ let ws;
3164
+ try {
3165
+ ws = new WebSocket(url);
3166
+ } catch {
3167
+ this.scheduleReconnect();
3168
+ return;
3169
+ }
3170
+ this.ws = ws;
3171
+ ws.addEventListener("open", () => {
3172
+ if (this.ws !== ws) return;
3173
+ this.connected = true;
3174
+ this.reconnectAttempt = 0;
3175
+ this.startHeartbeat(ws);
3176
+ this.emitConnection("connected", 0);
3177
+ });
3178
+ ws.addEventListener("message", (event) => {
3179
+ if (this.ws === ws) this.heartbeat?.markAlive();
3180
+ this.onMessage(String(event.data));
3181
+ });
3182
+ ws.addEventListener("error", () => this.handleDrop(ws));
3183
+ ws.addEventListener("close", () => this.handleDrop(ws));
3184
+ }
3185
+ onMessage(raw) {
3186
+ let message;
3187
+ try {
3188
+ message = JSON.parse(raw);
3189
+ } catch {
3190
+ return;
3191
+ }
3192
+ if (!message || typeof message !== "object") return;
3193
+ const frame = message;
3194
+ if (typeof frame.event === "string") {
3195
+ this.emitEvent(frame.event, frame.data);
3196
+ return;
3197
+ }
3198
+ if (frame.type === "wake" && typeof frame.threadId === "string") {
3199
+ this.emitEvent("standardagents.wake", { thread_id: frame.threadId });
3200
+ }
3201
+ }
3202
+ emitEvent(event, data) {
3203
+ for (const listener of this.eventListeners) listener(event, data);
3204
+ }
3205
+ emitConnection(state, attempt) {
3206
+ for (const listener of this.connectionListeners) listener(state, attempt);
3207
+ }
3208
+ handleDrop(ws) {
3209
+ if (this.ws !== ws) return;
3210
+ this.ws = null;
3211
+ this.connected = false;
3212
+ this.stopHeartbeat();
3213
+ this.scheduleReconnect();
3214
+ }
3215
+ scheduleReconnect() {
3216
+ if (this.closed || this.reconnectTimer) return;
3217
+ this.reconnectAttempt++;
3218
+ this.emitConnection("reconnecting", this.reconnectAttempt);
3219
+ const base = Math.min(500 * 2 ** (this.reconnectAttempt - 1), 15e3);
3220
+ const delay = base + Math.floor(Math.random() * 400);
3221
+ this.reconnectTimer = setTimeout(() => {
3222
+ this.reconnectTimer = null;
3223
+ this.openSocket();
3224
+ }, delay);
3225
+ }
3226
+ startHeartbeat(ws) {
3227
+ this.stopHeartbeat();
3228
+ this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
3229
+ this.heartbeat.start();
3230
+ }
3231
+ stopHeartbeat() {
3232
+ this.heartbeat?.stop();
3233
+ this.heartbeat = null;
3234
+ }
3235
+ close() {
3236
+ this.closed = true;
3237
+ this.connected = false;
3238
+ this.stopHeartbeat();
3239
+ if (this.reconnectTimer) {
3240
+ clearTimeout(this.reconnectTimer);
3241
+ this.reconnectTimer = null;
3242
+ }
3243
+ const ws = this.ws;
3244
+ this.ws = null;
3245
+ ws?.close();
3246
+ this.eventListeners.clear();
3247
+ this.connectionListeners.clear();
3248
+ }
3249
+ };
3098
3250
 
3099
3251
  // src/events-stream.ts
3100
3252
  var SystemEvents = class {
@@ -6492,7 +6644,7 @@ function readVersion() {
6492
6644
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
6493
6645
  } catch {
6494
6646
  }
6495
- return "0.13.0" ;
6647
+ return "0.13.1" ;
6496
6648
  }
6497
6649
  function isLocalHost(host) {
6498
6650
  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);
@@ -6607,6 +6759,68 @@ async function readFsResponse(api, machineId) {
6607
6759
  async function readFsRequest(api, machineId) {
6608
6760
  return parseFsRequest(await api.userKvGet(fsRequestKey(machineId)));
6609
6761
  }
6762
+ async function requestFsBrowse(api, accountStream, machineId, req, timeoutMs = 15e3) {
6763
+ await accountStream.waitUntilConnected(timeoutMs);
6764
+ const responseKey = fsResponseKey(machineId);
6765
+ return new Promise((resolve, reject) => {
6766
+ let settled = false;
6767
+ let reading = false;
6768
+ let readAgain = false;
6769
+ let timer;
6770
+ let removeEvent = () => {
6771
+ };
6772
+ let removeConnection = () => {
6773
+ };
6774
+ const cleanup = () => {
6775
+ clearTimeout(timer);
6776
+ removeEvent();
6777
+ removeConnection();
6778
+ };
6779
+ const fail = (error) => {
6780
+ if (settled) return;
6781
+ settled = true;
6782
+ cleanup();
6783
+ reject(error);
6784
+ };
6785
+ const read = async () => {
6786
+ if (settled) return;
6787
+ if (reading) {
6788
+ readAgain = true;
6789
+ return;
6790
+ }
6791
+ reading = true;
6792
+ try {
6793
+ do {
6794
+ readAgain = false;
6795
+ const response = await readFsResponse(api, machineId);
6796
+ if (response?.nonce === req.nonce) {
6797
+ settled = true;
6798
+ cleanup();
6799
+ resolve(response);
6800
+ return;
6801
+ }
6802
+ } while (readAgain && !settled);
6803
+ } catch (error) {
6804
+ fail(error);
6805
+ } finally {
6806
+ reading = false;
6807
+ }
6808
+ };
6809
+ removeEvent = accountStream.onEvent((event, data) => {
6810
+ if (event !== "standardcode.machine_changed") return;
6811
+ const key = data && typeof data === "object" ? data.key : null;
6812
+ if (key === responseKey) void read();
6813
+ });
6814
+ removeConnection = accountStream.onConnection((state) => {
6815
+ if (state === "connected") void read();
6816
+ });
6817
+ timer = setTimeout(
6818
+ () => fail(new Error("The remote machine did not answer in time.")),
6819
+ timeoutMs
6820
+ );
6821
+ void writeFsRequest(api, machineId, req).catch(fail);
6822
+ });
6823
+ }
6610
6824
  async function writeFsResponse(api, machineId, res) {
6611
6825
  await api.userKvSet(fsResponseKey(machineId), { ...res, responded_at: Date.now() });
6612
6826
  }
@@ -6756,8 +6970,11 @@ async function loadMachine(api, machineId) {
6756
6970
  overlayProjectNames(rec, projNames);
6757
6971
  return rec;
6758
6972
  }
6973
+ var CONNECTED_LAST_SEEN_AT = Number.MAX_SAFE_INTEGER;
6759
6974
  function daemonOnline(record2, now = Date.now()) {
6760
- return !!record2.daemon && now - record2.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
6975
+ if (!record2.daemon) return false;
6976
+ if (typeof record2.daemon.connected === "boolean") return record2.daemon.connected;
6977
+ return now - record2.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
6761
6978
  }
6762
6979
  function newRecord(identity) {
6763
6980
  const now = Date.now();
@@ -6786,6 +7003,18 @@ async function updateOwnMachineRecord(api, identity, mutate) {
6786
7003
  await api.userKvSet(machineKey(identity.machine_id), record2);
6787
7004
  return record2;
6788
7005
  }
7006
+ async function setDaemonPresence(api, identity, version, connected, pid = process.pid) {
7007
+ await updateOwnMachineRecord(api, identity, (record2) => {
7008
+ const now = Date.now();
7009
+ record2.daemon = {
7010
+ version,
7011
+ installed_at: record2.daemon?.installed_at ?? now,
7012
+ last_seen_at: connected ? CONNECTED_LAST_SEEN_AT : 0,
7013
+ connected,
7014
+ pid
7015
+ };
7016
+ });
7017
+ }
6789
7018
  function projectRepository(projectDir) {
6790
7019
  try {
6791
7020
  const url = execFileSync("git", ["-C", projectDir, "remote", "get-url", "origin"], {
@@ -7028,22 +7257,20 @@ function parseBrowseResult(value) {
7028
7257
  error: typeof r.error === "string" ? r.error : void 0
7029
7258
  };
7030
7259
  }
7031
- function remoteBrowseBackend(api, machineId, label) {
7260
+ function remoteBrowseBackend(api, accountStream, machineId, label) {
7032
7261
  const rpc = async (req) => {
7033
7262
  const nonce = crypto.randomBytes(8).toString("hex");
7034
- await writeFsRequest(api, machineId, { nonce, ...req });
7035
- const deadline = Date.now() + 15e3;
7036
- while (Date.now() < deadline) {
7037
- await new Promise((r) => setTimeout(r, 700));
7038
- const res = await readFsResponse(api, machineId).catch(() => null);
7039
- if (res && res.nonce === nonce) {
7040
- if (!res.ok) throw new Error(res.error || "Browse failed on the remote machine.");
7041
- const parsed = parseBrowseResult(res.result);
7042
- if (!parsed) throw new Error("The remote machine sent an unreadable listing.");
7043
- return parsed;
7263
+ const res = await requestFsBrowse(api, accountStream, machineId, { nonce, ...req }).catch(
7264
+ (error) => {
7265
+ throw new Error(
7266
+ error instanceof Error && error.message === "The remote machine did not answer in time." ? `${label} didn't answer \u2014 is its daemon online?` : error instanceof Error ? error.message : String(error)
7267
+ );
7044
7268
  }
7045
- }
7046
- throw new Error(`${label} didn't answer \u2014 is its daemon online?`);
7269
+ );
7270
+ if (!res.ok) throw new Error(res.error || "Browse failed on the remote machine.");
7271
+ const parsed = parseBrowseResult(res.result);
7272
+ if (!parsed) throw new Error("The remote machine sent an unreadable listing.");
7273
+ return parsed;
7047
7274
  };
7048
7275
  return {
7049
7276
  label,
@@ -7225,106 +7452,31 @@ async function awaitApprovalViaRelay(api, threadId, request, options = {}) {
7225
7452
  });
7226
7453
  }
7227
7454
  }
7455
+
7456
+ // src/daemon-user-stream.ts
7228
7457
  var DaemonUserStream = class {
7458
+ stream;
7229
7459
  constructor(api, clientId, hooks, clientName) {
7230
- this.api = api;
7231
- this.clientId = clientId;
7232
- this.hooks = hooks;
7233
- this.clientName = clientName;
7234
- }
7235
- api;
7236
- clientId;
7237
- hooks;
7238
- clientName;
7239
- ws = null;
7240
- closed = false;
7241
- heartbeat = null;
7242
- reconnectAttempt = 0;
7243
- reconnectTimer = null;
7244
- start() {
7245
- this.openSocket();
7246
- }
7247
- openSocket() {
7248
- if (this.closed) return;
7249
- const connectionClientId = `${this.clientId}:${crypto.randomBytes(8).toString("hex")}`;
7250
- let url = `${this.api.wsEndpoint}/api/users/me/stream?token=${encodeURIComponent(this.api.bearer)}&client_id=${encodeURIComponent(connectionClientId)}&client_kind=daemon`;
7251
- if (this.clientName) url += `&client_name=${encodeURIComponent(this.clientName)}`;
7252
- let ws;
7253
- try {
7254
- ws = new WebSocket(url);
7255
- } catch {
7256
- this.scheduleReconnect();
7257
- return;
7258
- }
7259
- this.ws = ws;
7260
- ws.addEventListener("open", () => {
7261
- this.reconnectAttempt = 0;
7262
- this.startHeartbeat(ws);
7263
- this.hooks.onConnection?.("connected", 0);
7460
+ this.stream = new AccountUserStream(api, clientId, {
7461
+ clientKind: "daemon",
7462
+ clientName,
7463
+ uniqueConnectionId: true
7264
7464
  });
7265
- ws.addEventListener("message", (event) => {
7266
- if (this.ws === ws) this.heartbeat?.markAlive();
7267
- this.onMessage(String(event.data));
7268
- });
7269
- ws.addEventListener("error", () => this.handleDrop(ws));
7270
- ws.addEventListener("close", () => this.handleDrop(ws));
7271
- }
7272
- onMessage(raw) {
7273
- let message;
7274
- try {
7275
- message = JSON.parse(raw);
7276
- } catch {
7277
- return;
7278
- }
7279
- if (!message || typeof message !== "object") return;
7280
- const frame = message;
7281
- if (typeof frame.event === "string") {
7282
- if (frame.event === "standardagents.wake") {
7283
- const threadId = frame.data?.thread_id;
7284
- if (typeof threadId === "string") this.hooks.onWake(threadId);
7285
- } else {
7286
- this.hooks.onEvent(frame.event, frame.data);
7465
+ this.stream.onEvent((event, data) => {
7466
+ if (event === "standardagents.wake") {
7467
+ const threadId = data?.thread_id;
7468
+ if (typeof threadId === "string") hooks.onWake(threadId);
7469
+ return;
7287
7470
  }
7288
- return;
7289
- }
7290
- if (frame.type === "wake" && typeof frame.threadId === "string") {
7291
- this.hooks.onWake(frame.threadId);
7292
- }
7293
- }
7294
- handleDrop(ws) {
7295
- if (this.ws !== ws) return;
7296
- this.ws = null;
7297
- this.stopHeartbeat();
7298
- this.scheduleReconnect();
7299
- }
7300
- scheduleReconnect() {
7301
- if (this.closed || this.reconnectTimer) return;
7302
- this.reconnectAttempt++;
7303
- this.hooks.onConnection?.("reconnecting", this.reconnectAttempt);
7304
- const base = Math.min(500 * 2 ** (this.reconnectAttempt - 1), 15e3);
7305
- const delay = base + Math.floor(Math.random() * 400);
7306
- this.reconnectTimer = setTimeout(() => {
7307
- this.reconnectTimer = null;
7308
- this.openSocket();
7309
- }, delay);
7310
- }
7311
- startHeartbeat(ws) {
7312
- this.stopHeartbeat();
7313
- this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
7314
- this.heartbeat.start();
7471
+ hooks.onEvent(event, data);
7472
+ });
7473
+ if (hooks.onConnection) this.stream.onConnection(hooks.onConnection);
7315
7474
  }
7316
- stopHeartbeat() {
7317
- this.heartbeat?.stop();
7318
- this.heartbeat = null;
7475
+ start() {
7476
+ this.stream.start();
7319
7477
  }
7320
7478
  close() {
7321
- this.closed = true;
7322
- this.stopHeartbeat();
7323
- if (this.reconnectTimer) {
7324
- clearTimeout(this.reconnectTimer);
7325
- this.reconnectTimer = null;
7326
- }
7327
- this.ws?.close();
7479
+ this.stream.close();
7328
7480
  }
7329
7481
  };
7330
7482
  var PKG_NAME = "@standardagents/code";
@@ -7697,15 +7849,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7697
7849
  process.on("unhandledRejection", (err) => {
7698
7850
  daemonLog(`unhandledRejection: ${err instanceof Error ? err.stack : String(err)}`);
7699
7851
  });
7700
- await updateOwnMachineRecord(api, identity, (record2) => {
7701
- const now = Date.now();
7702
- record2.daemon = {
7703
- version,
7704
- installed_at: record2.daemon?.installed_at ?? now,
7705
- last_seen_at: 0,
7706
- pid: process.pid
7707
- };
7708
- }).catch(
7852
+ await setDaemonPresence(api, identity, version, false).catch(
7709
7853
  (e) => daemonLog(`machine registration failed: ${e instanceof Error ? e.message : String(e)}`)
7710
7854
  );
7711
7855
  const workers = /* @__PURE__ */ new Map();
@@ -7717,13 +7861,26 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7717
7861
  let forcedUpdatePending = false;
7718
7862
  let restartInProgress = false;
7719
7863
  let shuttingDown = false;
7720
- function shutdown(code) {
7864
+ let presenceWrite = Promise.resolve();
7865
+ function publishPresence(connected) {
7866
+ presenceWrite = presenceWrite.then(() => setDaemonPresence(api, identity, version, connected)).catch((error) => {
7867
+ daemonLog(
7868
+ `presence ${connected ? "publish" : "clear"} failed: ${error instanceof Error ? error.message : String(error)}`
7869
+ );
7870
+ });
7871
+ return presenceWrite;
7872
+ }
7873
+ async function shutdown(code) {
7721
7874
  if (shuttingDown) return;
7722
7875
  shuttingDown = true;
7723
7876
  if (updateTimer) clearInterval(updateTimer);
7724
7877
  for (const worker of workers.values()) worker.stop();
7725
7878
  workers.clear();
7726
7879
  hub?.close();
7880
+ await Promise.race([
7881
+ publishPresence(false),
7882
+ new Promise((resolve) => setTimeout(resolve, 2e3))
7883
+ ]);
7727
7884
  daemonLog(`daemon exiting (code ${code})`);
7728
7885
  process.exit(code);
7729
7886
  }
@@ -7742,14 +7899,14 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7742
7899
  daemonLog(`forced update ${ok ? "succeeded" : "failed"}: ${output4.trim().split("\n").slice(-2).join(" | ")}`);
7743
7900
  if (ok) {
7744
7901
  daemonLog("restarting to apply the forced update");
7745
- shutdown(0);
7902
+ await shutdown(0);
7746
7903
  return;
7747
7904
  }
7748
7905
  restartInProgress = false;
7749
7906
  }
7750
7907
  if (updateReady) {
7751
7908
  daemonLog("restarting to apply the installed update");
7752
- shutdown(0);
7909
+ await shutdown(0);
7753
7910
  }
7754
7911
  }
7755
7912
  const threadIsActive = (threadId) => lineActiveThreads.has(threadId) || samaActiveThreads.has(threadId);
@@ -7886,9 +8043,16 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7886
8043
  }
7887
8044
  fsBusy = true;
7888
8045
  try {
7889
- const req = await readFsRequest(api, identity.machine_id).catch(() => null);
8046
+ let req;
8047
+ try {
8048
+ req = await readFsRequest(api, identity.machine_id);
8049
+ } catch (error2) {
8050
+ daemonLog(`fs-browse request read failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
8051
+ return;
8052
+ }
7890
8053
  if (!req || req.nonce === lastFsNonce) return;
7891
8054
  lastFsNonce = req.nonce;
8055
+ daemonLog(`fs-browse ${req.op} received (${req.nonce})`);
7892
8056
  let ok = true;
7893
8057
  let result;
7894
8058
  let error;
@@ -7898,8 +8062,12 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
7898
8062
  ok = false;
7899
8063
  error = err instanceof Error ? err.message : "browse failed";
7900
8064
  }
7901
- await writeFsResponse(api, identity.machine_id, { nonce: req.nonce, ok, result, error }).catch(() => {
7902
- });
8065
+ try {
8066
+ await writeFsResponse(api, identity.machine_id, { nonce: req.nonce, ok, result, error });
8067
+ daemonLog(`fs-browse ${req.op} ${ok ? "answered" : "failed"} (${req.nonce})${error ? `: ${error}` : ""}`);
8068
+ } catch (writeError) {
8069
+ daemonLog(`fs-browse response write failed (${req.nonce}): ${writeError instanceof Error ? writeError.message : String(writeError)}`);
8070
+ }
7903
8071
  } finally {
7904
8072
  fsBusy = false;
7905
8073
  if (fsAgain) {
@@ -8001,9 +8169,11 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
8001
8169
  }
8002
8170
  if (event !== "standardcode.machine_changed") return;
8003
8171
  const key = data && typeof data === "object" ? data.key : null;
8172
+ daemonLog(`machine event: ${typeof key === "string" ? key : "missing key"}`);
8004
8173
  if (key === `${machinePrefix}.cmd`) void drainCommands();
8005
- else if (key === `${machinePrefix}.fsreq`) void drainFsRequests();
8006
- else if (key === `${machinePrefix}.name`) {
8174
+ else if (key === `${machinePrefix}.fsreq`) {
8175
+ void drainFsRequests();
8176
+ } else if (key === `${machinePrefix}.name`) {
8007
8177
  void getMachineName(api, identity.machine_id).then((name) => {
8008
8178
  displayName = name || os8.hostname();
8009
8179
  }).catch(() => {
@@ -8012,11 +8182,14 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
8012
8182
  },
8013
8183
  onConnection: (state, attempt) => {
8014
8184
  if (state === "reconnecting") {
8015
- if (attempt === 1) daemonLog("user stream: reconnecting");
8185
+ if (attempt === 1) {
8186
+ daemonLog("user stream: reconnecting");
8187
+ void publishPresence(false);
8188
+ }
8016
8189
  return;
8017
8190
  }
8018
8191
  daemonLog("user stream: connected");
8019
- void recoverOnConnect();
8192
+ void publishPresence(true).then(() => recoverOnConnect());
8020
8193
  }
8021
8194
  },
8022
8195
  displayName
@@ -8024,8 +8197,8 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
8024
8197
  hub.start();
8025
8198
  void checkUpdates();
8026
8199
  updateTimer = setInterval(() => void checkUpdates(), UPDATE_CHECK_MS);
8027
- process.on("SIGTERM", () => shutdown(0));
8028
- process.on("SIGINT", () => shutdown(0));
8200
+ process.on("SIGTERM", () => void shutdown(0));
8201
+ process.on("SIGINT", () => void shutdown(0));
8029
8202
  daemonLog(`daemon ready \u2014 waiting for work tagged ${runnerTag}`);
8030
8203
  await new Promise(() => {
8031
8204
  });
@@ -8372,8 +8545,8 @@ async function statusCommand() {
8372
8545
  return;
8373
8546
  }
8374
8547
  const online = daemonOnline(record2);
8375
- const streamConnected = !!record2.daemon && record2.daemon.last_seen_at > Date.now();
8376
- const seen = streamConnected && record2.daemon ? `user stream connected (v${record2.daemon.version})` : record2.daemon ? `last legacy heartbeat ${Math.round((Date.now() - record2.daemon.last_seen_at) / 1e3)}s ago (v${record2.daemon.version})` : "never";
8548
+ const streamConnected = !!record2.daemon && (record2.daemon.connected ?? record2.daemon.last_seen_at > Date.now());
8549
+ const seen = streamConnected && record2.daemon ? `user stream connected (v${record2.daemon.version})` : record2.daemon?.connected === false ? `user stream disconnected (v${record2.daemon.version})` : record2.daemon ? `last legacy heartbeat ${Math.round((Date.now() - record2.daemon.last_seen_at) / 1e3)}s ago (v${record2.daemon.version})` : "never";
8377
8550
  stdout.write(`${c3.bold}Registry:${c3.reset} ${online ? `${c3.green}online${c3.reset}` : `${c3.yellow}offline${c3.reset}`} \xB7 ${seen}
8378
8551
  `);
8379
8552
  const projects = Object.keys(record2.projects);
@@ -9028,6 +9201,12 @@ ${c5.dim}Press Control-C again to exit${c5.reset}
9028
9201
  handoffClosing = true;
9029
9202
  reader.rl?.close();
9030
9203
  const identity = loadMachineIdentity();
9204
+ const accountStream = new AccountUserStream(api, interactiveClientId(identity), {
9205
+ clientKind: "cli",
9206
+ clientName: `${machine} (terminal)`
9207
+ });
9208
+ accountStream.start();
9209
+ process.once("exit", () => accountStream.close());
9031
9210
  void registerProject(api, identity, projectDir).catch(() => {
9032
9211
  });
9033
9212
  const tui = new Tui(1);
@@ -9155,7 +9334,7 @@ ${c5.bold}This machine has no always-on daemon${c5.reset} ${c5.dim}\u2014 sessio
9155
9334
  session.remotePath = void 0;
9156
9335
  projectDir = launchDir;
9157
9336
  if (where) {
9158
- const remotePath = await pickRemoteProject(tui, api, where);
9337
+ const remotePath = await pickRemoteProject(tui, api, accountStream, where);
9159
9338
  if (!remotePath) {
9160
9339
  step = "machine";
9161
9340
  continue;
@@ -9275,7 +9454,18 @@ ${c5.bold}This machine has no always-on daemon${c5.reset} ${c5.dim}\u2014 sessio
9275
9454
  }
9276
9455
  for (; ; ) {
9277
9456
  const agentTitle = AGENT_CHOICES.find((a) => a.id === selectedAgent)?.title ?? selectedAgent;
9278
- await runInteractive(tui, api, threadId, projectDir, machine, resumed, session, agentTitle, historySeed);
9457
+ await runInteractive(
9458
+ tui,
9459
+ api,
9460
+ accountStream,
9461
+ threadId,
9462
+ projectDir,
9463
+ machine,
9464
+ resumed,
9465
+ session,
9466
+ agentTitle,
9467
+ historySeed
9468
+ );
9279
9469
  historySeed = threadId;
9280
9470
  threadId = await createSessionThread();
9281
9471
  await api.kvSet(threadId, "lease_supersedes", historySeed);
@@ -9379,7 +9569,7 @@ async function pickLocalProject(tui, api, self, cwd, machineName) {
9379
9569
  if (picked !== NEW) return picked;
9380
9570
  return await pickDirectory(tui, localBrowseBackend(label), { startPath: cwd, loader: startLoader });
9381
9571
  }
9382
- async function pickRemoteProject(tui, api, runner) {
9572
+ async function pickRemoteProject(tui, api, accountStream, runner) {
9383
9573
  const ENTER_PATH = "__enter_path__";
9384
9574
  const projects = Object.entries(runner.projects).sort(
9385
9575
  (a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
@@ -9401,7 +9591,7 @@ async function pickRemoteProject(tui, api, runner) {
9401
9591
  );
9402
9592
  if (!picked) return null;
9403
9593
  if (picked !== ENTER_PATH) return picked;
9404
- return await pickDirectory(tui, remoteBrowseBackend(api, runner.id, runner.name), {
9594
+ return await pickDirectory(tui, remoteBrowseBackend(api, accountStream, runner.id, runner.name), {
9405
9595
  loader: startLoader
9406
9596
  });
9407
9597
  }
@@ -9469,7 +9659,7 @@ async function printHistory(api, threadId, tui) {
9469
9659
  else printAssistant(tui, text);
9470
9660
  }
9471
9661
  }
9472
- async function runInteractive(tui, api, threadId, projectDir, machine, resumed, session, agentTitle, historySeedThreadId) {
9662
+ async function runInteractive(tui, api, accountStream, threadId, projectDir, machine, resumed, session, agentTitle, historySeedThreadId) {
9473
9663
  const remote = session.mode === "remote";
9474
9664
  const runnerName = session.runner?.name ?? "the remote machine";
9475
9665
  let currentOwner = null;
@@ -10129,7 +10319,7 @@ ${c5.gray}Close another session (its slot frees within ~90s), then resend your m
10129
10319
  name: "machines",
10130
10320
  label: "Your machines",
10131
10321
  hint: "list, rename, update, manage projects",
10132
- run: () => runMachinesMenu(tui, api, session.identity)
10322
+ run: () => runMachinesMenu(tui, api, accountStream, session.identity)
10133
10323
  },
10134
10324
  {
10135
10325
  name: "daemon",
@@ -10606,7 +10796,7 @@ async function runLevelMenu(tui, perm) {
10606
10796
  perm.level = picked;
10607
10797
  }
10608
10798
  }
10609
- async function runMachinesMenu(tui, api, self) {
10799
+ async function runMachinesMenu(tui, api, accountStream, self) {
10610
10800
  let machines;
10611
10801
  try {
10612
10802
  machines = await loadMachines(api);
@@ -10635,9 +10825,9 @@ async function runMachinesMenu(tui, api, self) {
10635
10825
  );
10636
10826
  if (!picked) return;
10637
10827
  const machine = machines.find((m) => m.id === picked);
10638
- await manageMachine(tui, api, self, machine);
10828
+ await manageMachine(tui, api, accountStream, self, machine);
10639
10829
  }
10640
- async function manageMachine(tui, api, self, machine) {
10830
+ async function manageMachine(tui, api, accountStream, self, machine) {
10641
10831
  const isSelf = machine.id === self.machine_id;
10642
10832
  const online = daemonOnline(machine);
10643
10833
  const canRunCommands = isSelf || !!machine.daemon;
@@ -10675,7 +10865,7 @@ async function manageMachine(tui, api, self, machine) {
10675
10865
  machine.icon = trimmed || void 0;
10676
10866
  tui.print(`${c5.green}\u2713${c5.reset} icon ${trimmed ? `set to ${trimmed}` : "reset"} for ${machine.name}`);
10677
10867
  }
10678
- return manageMachine(tui, api, self, machine);
10868
+ return manageMachine(tui, api, accountStream, self, machine);
10679
10869
  }
10680
10870
  const dispatch = async (kind, args) => {
10681
10871
  if (isSelf) {
@@ -10703,10 +10893,10 @@ async function manageMachine(tui, api, self, machine) {
10703
10893
  tui.print(`${c5.green}\u2713${c5.reset} Update ${c5.gray}${applyNote} (its daemon updates and restarts on the new version).${c5.reset}`);
10704
10894
  }
10705
10895
  } else if (action === "projects") {
10706
- await manageMachineProjects(tui, api, self, machine, dispatch, applyNote);
10896
+ await manageMachineProjects(tui, api, accountStream, self, machine, dispatch, applyNote);
10707
10897
  }
10708
10898
  }
10709
- async function manageMachineProjects(tui, api, self, machine, dispatch, applyNote) {
10899
+ async function manageMachineProjects(tui, api, accountStream, self, machine, dispatch, applyNote) {
10710
10900
  const ADD = "__add__";
10711
10901
  const paths = Object.keys(machine.projects).sort();
10712
10902
  const projLabels = projectDisplayLabels(machine.projects);
@@ -10725,7 +10915,7 @@ async function manageMachineProjects(tui, api, self, machine, dispatch, applyNot
10725
10915
  if (!picked) return;
10726
10916
  if (picked === ADD) {
10727
10917
  const isSelf = machine.id === self.machine_id;
10728
- const backend = isSelf ? localBrowseBackend(machine.name) : remoteBrowseBackend(api, machine.id, machine.name);
10918
+ const backend = isSelf ? localBrowseBackend(machine.name) : remoteBrowseBackend(api, accountStream, machine.id, machine.name);
10729
10919
  const chosen = await pickDirectory(tui, backend, {
10730
10920
  startPath: isSelf ? process.cwd() : null,
10731
10921
  loader: startLoader