@standardagents/code 0.13.4 → 0.13.6

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/README.md CHANGED
@@ -76,8 +76,9 @@ we're letting new folks in every week.
76
76
  thread and keep running across reconnects and resumes.
77
77
  - **Idle attachments stay dormant.** Queue/draft snapshots refresh from events and lifecycle
78
78
  transitions, stream and idle execution-bridge heartbeats use Durable Object hibernation
79
- auto-responses, and transcript safety requests run only while the agent is active. The bridge
80
- switches to an observable liveness heartbeat only during a forwarded tool or approval, so
79
+ auto-responses on a 30-second quiet cadence (real traffic suppresses the keepalive), and
80
+ transcript safety requests run only while the agent is active. The bridge switches to a
81
+ 5-second observable liveness heartbeat only during a forwarded tool or approval, so
81
82
  leaving a terminal attached does not continuously wake an idle thread. Each running client owns
82
83
  one account user stream; a machine daemon opens a per-thread execution bridge only while
83
84
  forwarded work is active. Stream transitions update machine presence once. Remote filesystem
@@ -134,8 +135,10 @@ Sama One is BYOK: the first time you pick it, the CLI opens
134
135
  `https://standardcode.ai/app` where you connect your ChatGPT account once.
135
136
  That connection installs the authorization in your account's secret
136
137
  environment on the instance — shared by the CLI, web console, and macOS app —
137
- and the CLI itself never sees or stores the OpenAI key. Sama One sessions are
138
- not limited by the simultaneous-thread lease.
138
+ and the CLI itself never sees or stores the OpenAI key. Connection changes are
139
+ pushed over the existing account WebSocket to every open client; the clients
140
+ do not poll account metadata. Sama One sessions are not limited by the
141
+ simultaneous-thread lease.
139
142
 
140
143
  You only do this once per machine. Session start then asks **where the session should run** —
141
144
  this machine or any machine whose daemon is online — and offers to **resume** a session for that
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import * as session_state_star from '@standardagents/code-network/session-state'
11
11
  import * as transcript_delivery_star from '@standardagents/code-network/transcript-delivery';
12
12
  import * as approvals_star from '@standardagents/code-network/approvals';
13
13
  import net from 'net';
14
+ import { setImmediate } from 'timers';
14
15
  import fs11 from 'fs';
15
16
  import { permissionKey, Bridge as Bridge$1 } from '@standardagents/code-network/bridge';
16
17
  import readline from 'readline';
@@ -237,6 +238,45 @@ __reExport(approvals_exports, approvals_star);
237
238
  var ALLOWED_HOST = "chatgpt.com";
238
239
  var ALLOWED_PORT = 443;
239
240
  var CHUNK_BYTES = 256 * 1024;
241
+ var TunnelFrameCoalescer = class {
242
+ constructor(emit) {
243
+ this.emit = emit;
244
+ }
245
+ emit;
246
+ chunks = [];
247
+ bytes = 0;
248
+ scheduled = false;
249
+ push(chunk) {
250
+ if (chunk.length === 0) return;
251
+ let offset = 0;
252
+ while (offset < chunk.length) {
253
+ const remaining = CHUNK_BYTES - this.bytes;
254
+ const take = Math.min(remaining, chunk.length - offset);
255
+ this.chunks.push(chunk.subarray(offset, offset + take));
256
+ this.bytes += take;
257
+ offset += take;
258
+ if (this.bytes === CHUNK_BYTES) this.flush();
259
+ }
260
+ if (this.bytes > 0 && !this.scheduled) {
261
+ this.scheduled = true;
262
+ setImmediate(() => {
263
+ this.scheduled = false;
264
+ this.flush();
265
+ });
266
+ }
267
+ }
268
+ flush() {
269
+ if (this.bytes === 0) return;
270
+ const chunk = this.chunks.length === 1 ? this.chunks[0] : Buffer.concat(this.chunks, this.bytes);
271
+ this.chunks = [];
272
+ this.bytes = 0;
273
+ this.emit(chunk);
274
+ }
275
+ discard() {
276
+ this.chunks = [];
277
+ this.bytes = 0;
278
+ }
279
+ };
240
280
  var TunnelManager = class {
241
281
  constructor(send) {
242
282
  this.send = send;
@@ -302,18 +342,19 @@ var TunnelManager = class {
302
342
  const socket = net.connect({ host, port });
303
343
  socket.setNoDelay(true);
304
344
  socket.setKeepAlive(true, 15e3);
305
- this.tunnels.set(id, { socket });
345
+ const outbound = new TunnelFrameCoalescer((chunk) => {
346
+ this.send({ type: "stream_data", id, data: chunk.toString("base64") });
347
+ });
348
+ this.tunnels.set(id, { socket, outbound });
306
349
  socket.on("connect", () => {
307
350
  opened = true;
308
351
  this.send({ type: "stream_opened", id });
309
352
  });
310
353
  socket.on("data", (data) => {
311
- for (let i = 0; i < data.length; i += CHUNK_BYTES) {
312
- const slice = data.subarray(i, i + CHUNK_BYTES);
313
- this.send({ type: "stream_data", id, data: slice.toString("base64") });
314
- }
354
+ outbound.push(data);
315
355
  });
316
356
  socket.on("end", () => {
357
+ outbound.flush();
317
358
  this.send({ type: "stream_end", id });
318
359
  this.tunnels.delete(id);
319
360
  });
@@ -321,12 +362,14 @@ var TunnelManager = class {
321
362
  if (!opened) {
322
363
  this.send({ type: "stream_error", id, error: `Tunnel connect failed: ${error.message}` });
323
364
  } else {
365
+ outbound.flush();
324
366
  this.send({ type: "stream_abort", id, error: error.message });
325
367
  }
326
368
  this.tunnels.delete(id);
327
369
  });
328
370
  socket.on("close", () => {
329
371
  if (this.tunnels.has(id)) {
372
+ outbound.flush();
330
373
  this.send({ type: "stream_end", id });
331
374
  this.tunnels.delete(id);
332
375
  }
@@ -340,6 +383,7 @@ var TunnelManager = class {
340
383
  const tunnel = this.tunnels.get(id);
341
384
  if (!tunnel) return;
342
385
  this.tunnels.delete(id);
386
+ tunnel.outbound.discard();
343
387
  try {
344
388
  tunnel.socket.destroy();
345
389
  } catch {
@@ -2520,7 +2564,7 @@ var Tui = class _Tui {
2520
2564
  /**
2521
2565
  * Set the context-window fill percentage (0–100), or null to hide it.
2522
2566
  * Driven by the runtime's `context_usage` KV, scaled to the compaction
2523
- * trigger (see index.ts poll). Always painted on the status line far right.
2567
+ * trigger. Always painted on the status line far right.
2524
2568
  */
2525
2569
  setContextPct(pct) {
2526
2570
  const next = pct == null ? null : Math.max(0, Math.min(100, Math.round(pct)));
@@ -2855,8 +2899,8 @@ var Tui = class _Tui {
2855
2899
  // (which renders full markdown), so there's no double-render.
2856
2900
  static STREAM_TAIL = 20;
2857
2901
  // How long a reasoning-only preview may sit untouched before it's wiped. A
2858
- // visible answer must NEVER expire: it is the only copy until polling
2859
- // promotes the durable message into terminal scrollback.
2902
+ // visible answer must NEVER expire: it is the only copy until the durable
2903
+ // boundary event promotes the message into terminal scrollback.
2860
2904
  static STREAM_IDLE_MS = 1e4;
2861
2905
  /** Append a fragment of streamed answer text (rendered as progressive Markdown). */
2862
2906
  streamResponseDelta(delta, messageId) {
@@ -3904,7 +3948,7 @@ function readVersion() {
3904
3948
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
3905
3949
  } catch {
3906
3950
  }
3907
- return "0.13.4" ;
3951
+ return "0.13.6" ;
3908
3952
  }
3909
3953
  function isLocalHost(host) {
3910
3954
  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);
@@ -6955,8 +6999,11 @@ function usage() {
6955
6999
  `${c3.bold}standardcode daemon${c3.reset} \u2014 headless execution client for this machine`,
6956
7000
  "",
6957
7001
  `${c3.bold}Commands${c3.reset}`,
6958
- " install [--endpoint url] Sign in (if needed), name this machine, and install",
7002
+ " install [--endpoint url] [--name label | --yes]",
7003
+ " Sign in (if needed), name this machine, and install",
6959
7004
  " the always-on service (launchd / systemd).",
7005
+ " --name/--yes skip the name prompt (non-interactive,",
7006
+ " e.g. when another app drives the install).",
6960
7007
  " uninstall Stop and remove the service.",
6961
7008
  " status Service + registry status for this machine.",
6962
7009
  " run [--endpoint url] Run the daemon in the foreground (what the service runs).",
@@ -6981,6 +7028,32 @@ function parseEndpointFlag(args) {
6981
7028
  }
6982
7029
  return { endpoint, rest };
6983
7030
  }
7031
+ function parseInstallFlags(args) {
7032
+ const rest = [];
7033
+ let name;
7034
+ let yes = false;
7035
+ for (let i = 0; i < args.length; i++) {
7036
+ const arg = args[i];
7037
+ if (arg === "--yes" || arg === "-y") {
7038
+ yes = true;
7039
+ } else if (arg === "--name") {
7040
+ const next = args[i + 1];
7041
+ if (next !== void 0 && !next.startsWith("-")) {
7042
+ name = next;
7043
+ i++;
7044
+ } else {
7045
+ yes = true;
7046
+ }
7047
+ } else if (arg.startsWith("--name=")) {
7048
+ const value = arg.slice("--name=".length);
7049
+ if (value) name = value;
7050
+ else yes = true;
7051
+ } else {
7052
+ rest.push(arg);
7053
+ }
7054
+ }
7055
+ return { name: name?.trim() || void 0, yes, rest };
7056
+ }
6984
7057
  function resolveEndpoint(flag) {
6985
7058
  return (0, credentials_exports.normalizeEndpoint)(
6986
7059
  flag || process.env.STANDARD_CODE_DAEMON_ENDPOINT || (0, credentials_exports.defaultEndpoint)() || PRODUCTION_ENDPOINT
@@ -7014,17 +7087,22 @@ async function ensureSignedIn(endpoint) {
7014
7087
  `);
7015
7088
  return api;
7016
7089
  }
7017
- async function installCommand(endpointFlag) {
7090
+ async function installCommand(endpointFlag, opts = {}) {
7018
7091
  const endpoint = resolveEndpoint(endpointFlag);
7019
7092
  const api = await ensureSignedIn(endpoint);
7020
7093
  const identity = loadMachineIdentity();
7021
7094
  const existing = await loadMachine(api, identity.machine_id).catch(() => null);
7022
7095
  const suggested = machineDisplayName(existing ?? { hostname: os6.hostname(), id: identity.machine_id });
7023
- const rl = readline3.createInterface({ input: stdin, output: stdout });
7024
- const answer = (await rl.question(
7025
- `${c3.bold}Machine name${c3.reset} ${c3.dim}(shown in the session picker)${c3.reset} [${suggested}]: `
7026
- )).trim();
7027
- rl.close();
7096
+ let answer = "";
7097
+ if (opts.name) {
7098
+ answer = opts.name;
7099
+ } else if (!opts.yes) {
7100
+ const rl = readline3.createInterface({ input: stdin, output: stdout });
7101
+ answer = (await rl.question(
7102
+ `${c3.bold}Machine name${c3.reset} ${c3.dim}(shown in the session picker)${c3.reset} [${suggested}]: `
7103
+ )).trim();
7104
+ rl.close();
7105
+ }
7028
7106
  if (answer) await setMachineName(api, identity.machine_id, answer);
7029
7107
  await updateOwnMachineRecord(api, identity);
7030
7108
  const displayName = answer || suggested;
@@ -7141,16 +7219,13 @@ async function runDaemonCommand(argv) {
7141
7219
  const { endpoint, rest } = parseEndpointFlag(restArgs);
7142
7220
  switch (command) {
7143
7221
  case "run":
7144
- if (rest.includes("--ephemeral") || process.env.STANDARD_CODE_EPHEMERAL === "1") {
7145
- process.stdin.resume();
7146
- process.stdin.on("end", () => process.exit(0));
7147
- process.stdin.on("close", () => process.exit(0));
7148
- }
7149
7222
  await runDaemon({ endpoint: endpoint || process.env.STANDARD_CODE_DAEMON_ENDPOINT });
7150
7223
  return;
7151
- case "install":
7152
- await installCommand(endpoint);
7224
+ case "install": {
7225
+ const flags = parseInstallFlags(rest);
7226
+ await installCommand(endpoint, flags);
7153
7227
  return;
7228
+ }
7154
7229
  case "uninstall": {
7155
7230
  const result = uninstallService();
7156
7231
  stdout.write(`${result.ok ? c3.green + "\u2713" : c3.red + "\u2717"}${c3.reset} ${result.detail}
@@ -8028,7 +8103,7 @@ ${c5.dim}Install this machine's daemon (\`standardcode daemon install\`) or run
8028
8103
  const threadAgent = existing.find((t) => t.id === picked)?.agent_id;
8029
8104
  selectedAgent = threadAgent === OPENSAMA_AGENT_ID ? OPENSAMA_AGENT_ID : AGENT_ID;
8030
8105
  if (selectedAgent === OPENSAMA_AGENT_ID) {
8031
- const ok = await ensureSamaAuth(tui, api);
8106
+ const ok = await ensureSamaAuth(tui, api, accountStream);
8032
8107
  if (!ok) continue;
8033
8108
  }
8034
8109
  break flow;
@@ -8060,7 +8135,7 @@ ${c5.dim}Install this machine's daemon (\`standardcode daemon install\`) or run
8060
8135
  selectedAgent = picked;
8061
8136
  }
8062
8137
  if (selectedAgent === OPENSAMA_AGENT_ID) {
8063
- const ok = await ensureSamaAuth(tui, api);
8138
+ const ok = await ensureSamaAuth(tui, api, accountStream);
8064
8139
  if (!ok) {
8065
8140
  if (agentOverride) process.exit(0);
8066
8141
  continue;
@@ -8095,7 +8170,7 @@ ${c5.dim}Install this machine's daemon (\`standardcode daemon install\`) or run
8095
8170
  function shortenPath(p, max = 38) {
8096
8171
  return p.length > max ? "\u2026" + p.slice(-(max - 1)) : p;
8097
8172
  }
8098
- async function ensureSamaAuth(tui, api) {
8173
+ async function ensureSamaAuth(tui, api, accountStream) {
8099
8174
  const checking = startLoader("Checking your ChatGPT connection");
8100
8175
  const already = await api.openSamaStatus().catch(() => false);
8101
8176
  checking.stop();
@@ -8117,24 +8192,55 @@ async function ensureSamaAuth(tui, api) {
8117
8192
  `${c5.dim}Finish connecting ChatGPT in the browser \u2014 waiting here for the authorization to land on your account\u2026${c5.reset}`
8118
8193
  );
8119
8194
  const waiting = startLoader("Waiting for your ChatGPT authorization");
8120
- const deadline = Date.now() + 5 * 60 * 1e3;
8121
- while (Date.now() < deadline) {
8122
- await new Promise((r) => setTimeout(r, 3e3));
8123
- if (await api.openSamaStatus().catch(() => false)) {
8124
- waiting.stop();
8125
- tui.print(
8126
- `${c5.green}\u2713${c5.reset} ChatGPT connected \u2014 Sama One is now unlocked on your account (terminal, web, and macOS app).`
8127
- );
8128
- return true;
8129
- }
8130
- }
8195
+ const connected = await waitForSamaAuthorization(api, accountStream, 5 * 60 * 1e3);
8131
8196
  waiting.stop();
8197
+ if (connected) {
8198
+ tui.print(
8199
+ `${c5.green}\u2713${c5.reset} ChatGPT connected \u2014 Sama One is now unlocked on your account (terminal, web, and macOS app).`
8200
+ );
8201
+ return true;
8202
+ }
8132
8203
  tui.print(
8133
8204
  `${c5.yellow}Still not connected.${c5.reset} Finish the flow at ${c5.teal}standardcode.ai/app${c5.reset} and pick Sama One again.`
8134
8205
  );
8135
8206
  return false;
8136
8207
  }
8137
- async function runAgentSwitchMenu(tui, api, threadId) {
8208
+ function waitForSamaAuthorization(api, accountStream, timeoutMs) {
8209
+ return new Promise((resolve) => {
8210
+ let settled = false;
8211
+ let checking = false;
8212
+ let stopEvent = () => {
8213
+ };
8214
+ let stopConnection = () => {
8215
+ };
8216
+ let timer;
8217
+ const finish = (connected) => {
8218
+ if (settled) return;
8219
+ settled = true;
8220
+ clearTimeout(timer);
8221
+ stopEvent();
8222
+ stopConnection();
8223
+ resolve(connected);
8224
+ };
8225
+ timer = setTimeout(() => finish(false), timeoutMs);
8226
+ const reconcile = async () => {
8227
+ if (settled || checking) return;
8228
+ checking = true;
8229
+ const connected = await api.openSamaStatus().catch(() => false);
8230
+ checking = false;
8231
+ if (connected) finish(true);
8232
+ };
8233
+ stopEvent = accountStream.onEvent((event, data) => {
8234
+ if (event !== "standardcode.opensama_changed") return;
8235
+ if (data?.connected === true) finish(true);
8236
+ });
8237
+ stopConnection = accountStream.onConnection((state) => {
8238
+ if (state === "connected") void reconcile();
8239
+ });
8240
+ void reconcile();
8241
+ });
8242
+ }
8243
+ async function runAgentSwitchMenu(tui, api, accountStream, threadId) {
8138
8244
  const picked = await tui.select(
8139
8245
  `${c5.bold}Switch agent${c5.reset} ${c5.dim}takes effect on the next message${c5.reset}`,
8140
8246
  AGENT_CHOICES.map((choice) => ({
@@ -8146,7 +8252,7 @@ async function runAgentSwitchMenu(tui, api, threadId) {
8146
8252
  );
8147
8253
  if (!picked) return;
8148
8254
  if (picked === OPENSAMA_AGENT_ID) {
8149
- const ok = await ensureSamaAuth(tui, api);
8255
+ const ok = await ensureSamaAuth(tui, api, accountStream);
8150
8256
  if (!ok) return;
8151
8257
  }
8152
8258
  const title = AGENT_CHOICES.find((choice) => choice.id === picked)?.title ?? picked;
@@ -8367,7 +8473,8 @@ async function runInteractive(tui, api, accountStream, threadId, projectDir, mac
8367
8473
  },
8368
8474
  // Live streaming preview: answer text and (opt-in) internal reasoning feed
8369
8475
  // the TUI's ephemeral preview; the committed message still renders from
8370
- // polling, which calls tui.clearStream() first so there's no double-render.
8476
+ // the durable boundary event, which clears the preview before printing so
8477
+ // there's no double-render.
8371
8478
  onChunk: (text, mid) => tui.streamResponseDelta(text, mid),
8372
8479
  onReasoningChunk: (text, mid) => tui.streamThinkingDelta(text, mid),
8373
8480
  onAssistantText: () => {
@@ -8848,7 +8955,7 @@ ${c5.gray}Close another session (its slot frees within ~90s), then resend your m
8848
8955
  name: "agent",
8849
8956
  label: "Switch agent",
8850
8957
  hint: "hand this session to a different agent",
8851
- run: () => runAgentSwitchMenu(tui, api, threadId)
8958
+ run: () => runAgentSwitchMenu(tui, api, accountStream, threadId)
8852
8959
  },
8853
8960
  {
8854
8961
  name: "machines",