@standardagents/code 0.13.8 → 0.13.10

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
@@ -67,10 +67,10 @@ we're letting new folks in every week.
67
67
  Work continues on that machine after you close the terminal, and a machine without a daemon
68
68
  still runs the full CLI as a watcher of sessions executing elsewhere.
69
69
  - **Permission-aware by design.** A single auto-accept level (1–5) controls what runs unattended;
70
- anything riskier prompts you first, and operations explicitly marked as requiring permission
71
- prompt regardless of their numeric risk unless you previously granted that exact tool. A
72
- catastrophic-command guard blocks dangerous shell calls at *every* level, so you can't
73
- accidentally `rm -rf` your way into a bad day.
70
+ higher-risk calls prompt you first. Operations explicitly marked as requiring permission prompt
71
+ regardless of their numeric risk unless you previously granted that exact tool. Accepting a risk
72
+ tier raises the shared thread level so every client displays and enforces the same policy. A
73
+ catastrophic-command guard blocks dangerous shell calls at *every* level.
74
74
  - **Real project containment.** File destinations and background-process working directories are
75
75
  checked through filesystem symlinks; any path that resolves outside the project always requires
76
76
  a fresh human approval.
@@ -115,6 +115,169 @@ pnpm code <dir> # run against a project directory (defaults to cwd)
115
115
  pnpm code --endpoint http://localhost:5178 <dir>
116
116
  ```
117
117
 
118
+ The parallel Rust TUI preview signs in, loads the shared machine → project →
119
+ session projection, and attaches through the same version-matched network
120
+ core. It owns the terminal's alternate screen and does not open native HTTP or
121
+ WebSocket connections. Terminals at least 100 columns wide keep the session
122
+ tree available as a full-height left column; Tab or its clickable divider
123
+ collapses it to a one-column rail with a vertically stacked `Tab` hint. A
124
+ one-column gutter separates that rail from the conversation while the
125
+ transcript, composer, and chat controls expand into the reclaimed space.
126
+ Narrower terminals show the session tree full-screen when focused and omit the
127
+ sidebar collapse control. An authenticated launch with no attached session uses
128
+ that same shell. A normal launch registers or refreshes the current directory
129
+ as a project and highlights its row without creating a thread. `--browse`
130
+ opens the account-wide picker without a project target. The sidebar remains
131
+ available while the right column presents a session-selection empty state.
132
+ Machine and project headings sit on the same left edge, sessions use one
133
+ shallow indentation step, and a blank row separates machines. A one-cell `⣿`
134
+ texture marks machine headings and continues through their divider. Machine
135
+ rows have no disclosure glyph; a collapsed machine instead shows its known
136
+ thread count, such as `(13)`; offline machines show that retained count too.
137
+ Collapsed projects likewise append their known thread count.
138
+ Each machine's green/red presence dot precedes its name. The leading texture
139
+ cell takes the branch accent only while that machine is active, hovered, or
140
+ keyboard-highlighted; inactive machine texture remains structural gray.
141
+ Machines receive stable colors from DMUX's eight-accent palette, so color
142
+ belongs to a branch rather than to global selection. Machine color is limited
143
+ to its texture/divider while the active machine name stays white. Project
144
+ headings and dividers remain structural gray at every state. Unrelated
145
+ hierarchy recedes to dark gray.
146
+ The attached project and thread use typography without persistent row fills;
147
+ the thread title inherits its machine accent. Hover or keyboard navigation uses
148
+ the same brighter row background for machines, projects, and threads;
149
+ navigated threads also add a `›` marker. Every thread has a status cell: hollow gray is idle, solid
150
+ blue is unread, and a green animated texture is working. Unread state uses the
151
+ shared core's account-scoped, message-recency watermark; opening a thread
152
+ advances that shared state, and background activity timestamps never create a
153
+ false unread dot.
154
+ Click a heading or select its name and press Enter to collapse it. Right moves
155
+ keyboard focus to the heading's complete `[+]` button; Enter activates it,
156
+ Left returns to the name, and Right again returns to an attached conversation.
157
+ The button is hidden until its row is hovered or keyboard-highlighted; while
158
+ hidden, the heading divider fills those final three cells instead of reserving
159
+ an empty control gap.
160
+ The project `[+]` opens a prospective session composer. The machine `[+]`
161
+ opens its structure-only directory picker and registers the chosen project
162
+ before opening that composer. The canonical thread is created when its first
163
+ message is submitted, so closing an untouched composer leaves no empty thread.
164
+ The composer takes keyboard focus and explains that the first message creates
165
+ the session.
166
+ An offline machine has no disclosure/collapse action and hides its retained
167
+ project/session branch. Its machine row keeps the last known thread count and
168
+ can still receive keyboard or mouse highlight, but its foreground remains gray
169
+ and activation does nothing. Creation controls are absent while that machine is
170
+ offline, and presence is checked again before a core request is sent.
171
+ Those actions use the shared core's `project.open`, `session.start`, and
172
+ `machines.fsBrowse` operations. The Rust face receives filesystem data and
173
+ canonical session metadata from the core.
174
+ Press Tab to reveal and focus sessions, use ↑/↓ or j/k and Enter to activate,
175
+ and press Esc or Tab to return to the composer. Clicking anywhere in the
176
+ expanded sidebar also transfers keyboard focus there, beginning on the clicked
177
+ row when there is one. Left from an empty composer
178
+ also reveals and focuses the sidebar; Left retains normal cursor movement when
179
+ the composer contains text. Activating a prospective or existing chat returns
180
+ keyboard focus to the conversation. Switching saves the current shared draft
181
+ before detaching, and never stops the session's remote work. While the selected
182
+ conversation loads, the full-height session tree stays mounted and only the
183
+ right chat column enters its loading state. Session rows also switch on one
184
+ click. Passive mouse motion gives clickable session rows a high-contrast
185
+ surface, highlights the sidebar toggle rail, Markdown
186
+ links, and the jump-to-latest pill, and asks supporting terminals such as
187
+ Ghostty for a pointer cursor. Redraws are coalesced until the semantic hover
188
+ target really changes. The transcript owns pane-local click-drag selection, so it copies
189
+ semantic Markdown text without Shift and never sweeps through the sidebar.
190
+ White is reserved for user text and high-value accents. Assistant prose and
191
+ tool, system, reasoning, and structural rows use sampled cool-gray tiers;
192
+ semantic states and Markdown keep muted accent colors. Live thought and answer blocks grow from a stable prefix,
193
+ so streamed chunks extend visible text without deleting and reflowing its
194
+ beginning. Running subagents retain a bounded list of their reads, searches,
195
+ edits, and other tool actions beneath the current activity phrase.
196
+ Settled reads and searches roll up under `Explored` while preserving every
197
+ action. Successful commands use one compact highlighted row. Failures retain
198
+ their complete output as a quiet gray block beneath the highlighted command,
199
+ and prose-like tool results share the transcript's word wrapper. Known
200
+ arguments and results receive muted semantic syntax colors. Consecutive edits
201
+ roll up under `Edited N files`; their complete red and green hunks fill the
202
+ available conversation width.
203
+ Thinking appears in an expandable block with an animated Braille texture while
204
+ the trace is active. Active traces start collapsed, and completed traces retain
205
+ a one-line disclosure. Click any thinking row to toggle its trace, or press Ctrl+T
206
+ to toggle the newest trace.
207
+ Releasing the button clears the highlight and shows `Copied text` in the status
208
+ row. Clicking anywhere in the active conversation column returns keyboard
209
+ focus to its composer without disrupting drag selection. Prose wraps at word
210
+ boundaries; long unbroken tokens and code still
211
+ hard-wrap to the pane. The complete loaded transcript remains scrollable with
212
+ the mouse wheel or Page Up/Page Down. Ctrl+Home jumps to the beginning and
213
+ Ctrl+End resumes bottom-follow; output that arrives while scrolled up leaves
214
+ the viewport anchored and shows a one-click “Jump to latest” pill at the bottom
215
+ of the conversation. The pill calls out new output when it arrives. Wheel bursts
216
+ accumulate between frames, preserving responsive trackpad motion instead of dropping input.
217
+ While a turn runs, an animated amber/coral/violet Braille status row stays
218
+ directly above the composer, after the optional `Session todos` card.
219
+ The composer and submitted user messages share one quiet filled rectangular
220
+ surface with a chevron gutter. One empty surface row sits above the editable
221
+ rows and one below, so the empty composer is three terminal rows; it grows
222
+ from one to eight content rows, wraps prose on word boundaries,
223
+ preserves hard newlines from Shift+Enter and multiline paste, and then scrolls
224
+ internally without displacing more of the transcript. Short conversations use
225
+ natural document flow: the composer and telemetry follow the final transcript
226
+ row, leaving unused space below. Once the transcript consumes the available
227
+ viewport, the composer stays pinned at the bottom and the transcript scrolls
228
+ above it. The mouse
229
+ wheel follows the pane under the pointer, so a capped composer scrolls without
230
+ moving the conversation; the next edit resumes caret-follow. Pastes over 1,000
231
+ characters render as an atomic cyan `[Pasted Content N chars]` element while
232
+ their exact text remains in the shared draft and submitted message. Backspace
233
+ or Delete at that element removes the complete paste. Enter sends or queues,
234
+ while Ctrl+Enter steers. Up/Down moves through visual editor
235
+ rows before recalling the core-backed per-thread sent history at the top or
236
+ bottom edge.
237
+ Structured session todos appear in a collapsible card above the composer. One
238
+ blank row separates the neutral teal-gray card from the transcript. Todo
239
+ descriptions and steps wrap to show their complete content. Escape promotes the
240
+ oldest queued message through the shared core. During active work with an empty
241
+ queue, Escape stops execution and releases the composer for a new message.
242
+ An offline runner does not make its cloud conversation disappear: the session
243
+ still opens for review, but the composer pauses instead of letting new work
244
+ silently wait for a missing executor. There is no separate offline banner; the
245
+ composer becomes a hollow, unfilled surface carrying the state inline. If work
246
+ is already waiting, it says `Waiting for <machine> — Esc stops`. Pushed machine-presence events unlock
247
+ the composer and restore the retained sidebar branch as soon as the daemon
248
+ reconnects; there is no presence poll or session relist.
249
+ The line beneath the composer is session telemetry rather than a persistent
250
+ keyboard cheat sheet: it shows the attached thread's agent, runner machine,
251
+ current working directory, running background-process count, and
252
+ compaction-scaled context usage. The muted path gets priority over the literal
253
+ `context` label. Full agent names stay visible at comfortable widths; only a
254
+ narrow footer abbreviates them (`Unlimited One` → `U1`, `Sama One` → `S1`)
255
+ before squeezing the deepest complete path breadcrumbs further.
256
+ The row ends with the old terminal face's five colored permission dots, so the
257
+ current effective auto-accept ceiling remains visible without adding another
258
+ row. Accepted risk grants raise the dots immediately and reconcile from the
259
+ persisted approval state. Approval requests show the tool summary, stated
260
+ reason, execution machine, five-dot risk level, and fully labeled decisions in
261
+ a bordered card. These
262
+ values come from the shared roster, session tree,
263
+ approvals, process registry, and context projections in the network core.
264
+ Headings, emphasis, links, lists, tables, quotes, and fenced code use
265
+ the same progressive parser contract as the TypeScript UI. PNG attachments use
266
+ Kitty Unicode placeholders in verified Kitty/Ghostty/WezTerm environments,
267
+ including configured tmux; other terminals keep the same layout and show a
268
+ text fallback:
269
+
270
+ ```bash
271
+ pnpm tui:rust
272
+ # Open the account-wide project picker:
273
+ pnpm tui:rust -- --browse
274
+ # Optional direct-attach fast path:
275
+ pnpm tui:rust -- --thread <thread-id>
276
+ ```
277
+
278
+ This preview is additive; the TypeScript CLI remains the normal entry point
279
+ while the Rust face is brought to feature parity.
280
+
118
281
  Installed from npm:
119
282
 
120
283
  ```bash
@@ -154,6 +317,19 @@ The first time you choose this machine, the CLI offers to install its daemon in
154
317
  here. Declining leaves local execution unavailable; remote machines and watching stay available,
155
318
  and the command sets it up any time.
156
319
 
320
+ Manage the local daemon from any shell:
321
+
322
+ ```bash
323
+ standardcode daemon status
324
+ standardcode daemon restart
325
+ standardcode daemon update
326
+ standardcode daemon add-project /absolute/path/to/project
327
+ standardcode daemon uninstall
328
+ ```
329
+
330
+ `restart` immediately reloads the installed launchd or systemd service. `update` lets active tool
331
+ calls finish, installs the latest Standard Code package, and restarts the daemon.
332
+
157
333
  Use `--endpoint [url]` or `-e [url]` to point a single CLI run at a different Standard Agents
158
334
  instance (local dev, self-hosted). If you omit the URL, the CLI prompts for it. Tokens are
159
335
  remembered per endpoint, but this override does not change the saved default endpoint.
@@ -178,9 +354,10 @@ development never disables TLS verification in unrelated commands.
178
354
 
179
355
  - **Forwarded tools** executed by the session's machine daemon: read/write/edit files, list,
180
356
  grep, glob, bash, delete.
181
- - **Permissions**: a single auto-accept level 1–5 (shift-tab cycles it); risky calls prompt;
182
- server-marked permission requests cannot be downgraded by a model-supplied risk; a catastrophic-
183
- command guard blocks dangerous shell commands at every level.
357
+ - **Permissions**: a shared auto-accept level 1–5 (shift-tab cycles it); accepting a risk tier
358
+ raises the stored thread level across clients; server-marked permission requests cannot be
359
+ downgraded by a model-supplied risk. A catastrophic-command guard blocks dangerous shell commands
360
+ at every level.
184
361
  - **Durable, resumable** long-running/permission-gated tool calls (park & resume over HTTP).
185
362
  - **Background processes** tracked in the thread (survive resume from any machine).
186
363
  - **Subagent delegation** and non-blocking web research.
@@ -194,7 +371,8 @@ development never disables TLS verification in unrelated commands.
194
371
  ## Tests
195
372
 
196
373
  ```bash
197
- pnpm test # version lockstep + typecheck + every declared no-backend test
374
+ pnpm test # TypeScript checks/tests plus Rust unit and PTY tests
375
+ pnpm check:rust # rustfmt and clippy with warnings denied
198
376
  ```
199
377
 
200
378
  `scripts/run-tests.mjs` owns the explicit no-backend test manifest, prints each
@@ -216,10 +394,16 @@ test layout, and contributor conventions.
216
394
  - Two version-locked packages: `@standardagents/code` and its shared
217
395
  `@standardagents/code-network` client. Both use Node built-ins and
218
396
  web-platform APIs without third-party runtime packages.
219
- - `src/progressive-markdown.ts` is the canonical structured streaming parser shared with the web
220
- and native heads. Run `pnpm markdown:sync` after changing it and `pnpm markdown:check` in CI.
221
- - The hosted instance's generated `src/session-state.ts` owns live-draft accumulation as well as
222
- busy/tool/message reconciliation; a changed chunk ID is an alias, never permission to erase text.
397
+ - `src/progressive-markdown.ts` is the canonical structured streaming parser used by every face.
398
+ The Node face-core sends its versioned documents to Rust, which handles
399
+ terminal layout and paint. Web receives an exact source copy, and macOS runs
400
+ the generated JavaScriptCore bundle. Shared vectors cover parser output and
401
+ Rust transport-schema compatibility.
402
+ Run `pnpm markdown:sync` after changing the authored TypeScript parser and
403
+ `pnpm markdown:check` in CI.
404
+ - The hosted instance authors the session-state reducer published through
405
+ `@standardagents/code-network`. It owns live-draft accumulation and busy/tool/message
406
+ reconciliation; a changed chunk ID is an alias, never permission to erase text.
223
407
  - Commit messages use Conventional Commits, scope `cli` (e.g. `feat(cli): …`).
224
408
 
225
409
  ---
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import * as approvals_star from '@standardagents/code-network/approvals';
14
14
  import net from 'net';
15
15
  import { setImmediate } from 'timers';
16
16
  import fs12 from 'fs';
17
+ import { isTranscriptMetaTool } from '@standardagents/code-network/tool-activity';
17
18
  import { permissionKey, Bridge as Bridge$1 } from '@standardagents/code-network/bridge';
18
19
  import readline from 'readline';
19
20
  import * as types_star from '@standardagents/code-network/types';
@@ -687,7 +688,10 @@ var Bridge = class extends Bridge$1 {
687
688
  isCatastrophic: (command) => isCatastrophic(command),
688
689
  decide: (permKey, risk, requestPermission) => decide(perm, permKey, risk, requestPermission),
689
690
  grantAlways: (permKey) => void perm.alwaysAllow.add(permKey),
690
- grantRisk: (risk) => void perm.allowRisk.add(risk),
691
+ grantRisk: (risk) => {
692
+ perm.level = Math.max(perm.level, risk);
693
+ perm.allowRisk.clear();
694
+ },
691
695
  persistApprovals: () => void (0, approvals_exports.saveApprovals)(api, threadId, perm)
692
696
  },
693
697
  {
@@ -798,7 +802,6 @@ function prettyAgentName(id) {
798
802
  if (!key) return "subagent";
799
803
  return AGENT_NAMES[key] || key.replace(/[_-]+/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase());
800
804
  }
801
- var META_TOOLS = /* @__PURE__ */ new Set(["set_goal", "update_goal_step", "reasoning"]);
802
805
  var APPROVAL_PENDING_MARKER = "Awaiting your approval";
803
806
  var ToolActivityFeed = class {
804
807
  calls = /* @__PURE__ */ new Map();
@@ -833,7 +836,7 @@ var ToolActivityFeed = class {
833
836
  const text = (0, session_state_exports.messageText)(message.content);
834
837
  if (text.includes(session_state_exports.WAITING_FOR_CLIENT_MARKER) || text.includes(APPROVAL_PENDING_MARKER)) return null;
835
838
  this.settled.add(callId);
836
- if (META_TOOLS.has(req.tool)) return null;
839
+ if (isTranscriptMetaTool(req.tool)) return null;
837
840
  const summary = describe(req);
838
841
  if (toolResultFailed(message, text)) {
839
842
  return { line: `\u2717 ${summary} \u2014 ${text.trim() || "failed"}` };
@@ -4014,7 +4017,7 @@ function readVersion() {
4014
4017
  if (typeof pkg.version === "string" && pkg.version) return pkg.version;
4015
4018
  } catch {
4016
4019
  }
4017
- return "0.13.8" ;
4020
+ return "0.13.10" ;
4018
4021
  }
4019
4022
  function isLocalHost(host) {
4020
4023
  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);
@@ -6464,7 +6467,10 @@ var ThreadWorker = class {
6464
6467
  return { choice: "deny", reason: "The approval request timed out with no one to approve it." };
6465
6468
  }
6466
6469
  if (response.choice === "always") this.perm.alwaysAllow.add(permKey);
6467
- if (response.choice === "always_risk") this.perm.allowRisk.add(effectiveRisk);
6470
+ if (response.choice === "always_risk") {
6471
+ this.perm.level = Math.max(this.perm.level, effectiveRisk);
6472
+ this.perm.allowRisk.clear();
6473
+ }
6468
6474
  if (response.choice === "always" || response.choice === "always_risk") {
6469
6475
  (0, approvals_exports.saveApprovals)(api, threadId, this.perm);
6470
6476
  }
@@ -6976,6 +6982,15 @@ function run2(cmd, args) {
6976
6982
  var plistPath = () => path9.join(os6.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
6977
6983
  var unitPath = () => path9.join(os6.homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
6978
6984
  var xmlEscape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6985
+ function restartServiceInvocation(platform, uid = 501) {
6986
+ if (platform === "darwin") {
6987
+ return { cmd: "launchctl", args: ["kickstart", "-k", `gui/${uid}/${SERVICE_LABEL}`] };
6988
+ }
6989
+ if (platform === "linux") {
6990
+ return { cmd: "systemctl", args: ["--user", "restart", SYSTEMD_UNIT] };
6991
+ }
6992
+ return null;
6993
+ }
6979
6994
  function installService(command, endpoint) {
6980
6995
  if (process.platform === "darwin") return installLaunchd(command, endpoint);
6981
6996
  if (process.platform === "linux") return installSystemd(command, endpoint);
@@ -7086,6 +7101,39 @@ function uninstallService() {
7086
7101
  }
7087
7102
  return { ok: false, detail: `Unsupported platform: ${process.platform}` };
7088
7103
  }
7104
+ function restartService() {
7105
+ if (process.platform === "darwin") {
7106
+ if (!fs12.existsSync(plistPath())) {
7107
+ return {
7108
+ ok: false,
7109
+ detail: "The daemon service is not installed. Run `standardcode daemon install` first."
7110
+ };
7111
+ }
7112
+ const uid = typeof process.getuid === "function" ? process.getuid() : 501;
7113
+ const invocation = restartServiceInvocation(process.platform, uid);
7114
+ const restart = run2(invocation.cmd, invocation.args);
7115
+ if (restart.ok) return { ok: true, detail: "LaunchAgent restarted" };
7116
+ const boot = run2("launchctl", ["bootstrap", `gui/${uid}`, plistPath()]);
7117
+ if (boot.ok) return { ok: true, detail: "LaunchAgent loaded and started" };
7118
+ return {
7119
+ ok: false,
7120
+ detail: `launchctl failed: ${restart.output || boot.output}`,
7121
+ manualHint: `Load it manually: launchctl bootstrap gui/${uid} ${plistPath()}`
7122
+ };
7123
+ }
7124
+ if (process.platform === "linux") {
7125
+ if (!fs12.existsSync(unitPath())) {
7126
+ return {
7127
+ ok: false,
7128
+ detail: "The daemon service is not installed. Run `standardcode daemon install` first."
7129
+ };
7130
+ }
7131
+ const invocation = restartServiceInvocation(process.platform);
7132
+ const restart = run2(invocation.cmd, invocation.args);
7133
+ return restart.ok ? { ok: true, detail: "systemd user service restarted" } : { ok: false, detail: `systemctl restart failed: ${restart.output}` };
7134
+ }
7135
+ return { ok: false, detail: `Unsupported platform: ${process.platform}` };
7136
+ }
7089
7137
  function serviceStatus() {
7090
7138
  if (process.platform === "darwin") {
7091
7139
  const installed = fs12.existsSync(plistPath());
@@ -7133,6 +7181,8 @@ function usage() {
7133
7181
  " e.g. when another app drives the install).",
7134
7182
  " uninstall Stop and remove the service.",
7135
7183
  " status Service + registry status for this machine.",
7184
+ " restart Restart the installed service immediately.",
7185
+ " update [--endpoint url] Install the latest CLI, then restart the service.",
7136
7186
  " run [--endpoint url] Run the daemon in the foreground (what the service runs).",
7137
7187
  " add-project <path> Register a project directory for remote sessions.",
7138
7188
  " remove-project <path> Remove a registered project directory.",
@@ -7262,9 +7312,10 @@ async function installCommand(endpointFlag, opts = {}) {
7262
7312
  stdout.write(
7263
7313
  `${c3.green}\u2713${c3.reset} ${c3.bold}${displayName}${c3.reset} is online.
7264
7314
 
7265
- Sessions started elsewhere can now run on this machine.
7266
- ${c3.dim}Projects register automatically when you run standardcode in a directory here,
7267
- or add one now: standardcode daemon add-project <path>${c3.reset}
7315
+ Standard Code sessions can now edit files and run tools on this machine while you drive them from any device signed in to your account.
7316
+ ${c3.dim}Projects are added automatically when you start Standard Code from their directory.
7317
+ To add a project manually, run:${c3.reset}
7318
+ ${c3.bold}standardcode daemon add-project /absolute/path/to/project${c3.reset}
7268
7319
  `
7269
7320
  );
7270
7321
  } else {
@@ -7314,6 +7365,55 @@ async function statusCommand() {
7314
7365
  for (const p of projects.sort()) stdout.write(` ${c3.dim}${p}${c3.reset}
7315
7366
  `);
7316
7367
  }
7368
+ async function updateDaemonCommand(endpointFlag) {
7369
+ const status = serviceStatus();
7370
+ if (!status.installed) {
7371
+ stdout.write(
7372
+ `${c3.red}\u2717${c3.reset} The daemon service is not installed. Run \`standardcode daemon install\` first.
7373
+ `
7374
+ );
7375
+ process.exit(1);
7376
+ }
7377
+ const endpoint = resolveEndpoint(endpointFlag);
7378
+ const api = await ensureSignedIn(endpoint);
7379
+ const identity = loadMachineIdentity();
7380
+ const record = await loadMachine(api, identity.machine_id).catch(() => null);
7381
+ if (status.running && record && daemonOnline(record)) {
7382
+ await enqueueMachineCommand(api, identity.machine_id, "update");
7383
+ stdout.write(
7384
+ `${c3.green}\u2713${c3.reset} Update requested. The daemon will finish active tool calls, install the latest Standard Code, and restart.
7385
+ `
7386
+ );
7387
+ return;
7388
+ }
7389
+ const pm = detectPackageManager();
7390
+ if (!pm) {
7391
+ stdout.write(
7392
+ `${c3.red}\u2717${c3.reset} This source checkout cannot update the installed CLI. Run \`standardcode daemon update\` through your global Standard Code command.
7393
+ `
7394
+ );
7395
+ process.exit(1);
7396
+ }
7397
+ const { display } = updateCommand(pm);
7398
+ stdout.write(`${c3.dim}Running ${display}\u2026${c3.reset}
7399
+ `);
7400
+ const result = await runUpdate(pm);
7401
+ if (!result.ok) {
7402
+ stdout.write(`${c3.red}\u2717${c3.reset} Update failed.
7403
+ `);
7404
+ for (const line of result.output.trim().split("\n").slice(-6)) {
7405
+ if (line) stdout.write(`${c3.dim}${line}${c3.reset}
7406
+ `);
7407
+ }
7408
+ process.exit(1);
7409
+ }
7410
+ const restarted = restartService();
7411
+ stdout.write(
7412
+ `${restarted.ok ? c3.green + "\u2713" : c3.red + "\u2717"}${c3.reset} ${restarted.ok ? "Standard Code updated. " : "Standard Code updated, but the daemon restart failed. "}${restarted.detail}
7413
+ `
7414
+ );
7415
+ if (!restarted.ok) process.exit(1);
7416
+ }
7317
7417
  async function projectCommand(action, target) {
7318
7418
  if (!target) {
7319
7419
  stdout.write(`${c3.red}\u2717${c3.reset} Expected a project path.
@@ -7369,6 +7469,18 @@ async function runDaemonCommand(argv) {
7369
7469
  case "status":
7370
7470
  await statusCommand();
7371
7471
  return;
7472
+ case "restart": {
7473
+ const result = restartService();
7474
+ stdout.write(`${result.ok ? c3.green + "\u2713" : c3.red + "\u2717"}${c3.reset} ${result.detail}
7475
+ `);
7476
+ if (result.manualHint) stdout.write(`${c3.dim}${result.manualHint}${c3.reset}
7477
+ `);
7478
+ if (!result.ok) process.exit(1);
7479
+ return;
7480
+ }
7481
+ case "update":
7482
+ await updateDaemonCommand(endpoint);
7483
+ return;
7372
7484
  case "add-project":
7373
7485
  await projectCommand("add", rest[0]);
7374
7486
  return;
@@ -8586,13 +8698,9 @@ async function runInteractive(tui, api, accountStream, threadId, projectDir, mac
8586
8698
  const pendingSent = /* @__PURE__ */ new Map();
8587
8699
  let lastSent = null;
8588
8700
  let upgradeInFlight = false;
8589
- let tokensIn = 0;
8590
- let tokensOut = 0;
8591
8701
  let liveOut = 0;
8592
- const countedLogs = /* @__PURE__ */ new Set();
8593
8702
  const activeSteps = /* @__PURE__ */ new Map();
8594
8703
  const refreshStatus = () => {
8595
- tui.setTokens(tokensIn, tokensOut + liveOut);
8596
8704
  let label = null;
8597
8705
  for (const v of activeSteps.values()) label = v;
8598
8706
  tui.setStep(label, liveOut);
@@ -9432,24 +9540,6 @@ ${c5.dim}runs on ${request.machine || runnerName}${c5.reset}`,
9432
9540
  refreshBgCount();
9433
9541
  void relayApprovals().catch(() => {
9434
9542
  });
9435
- try {
9436
- const logs = await api.getLogs(threadId, 100);
9437
- let landed = 0;
9438
- for (const l of logs) {
9439
- if (!l.is_complete) continue;
9440
- const id = l.id ?? `${l.created_at}:${l.total_tokens}`;
9441
- if (countedLogs.has(id)) continue;
9442
- countedLogs.add(id);
9443
- const inT = Number(l.input_tokens) || 0;
9444
- const outT = Number(l.output_tokens) || 0;
9445
- tokensIn += inT;
9446
- tokensOut += outT;
9447
- landed += outT;
9448
- }
9449
- if (landed > 0) liveOut = 0;
9450
- refreshStatus();
9451
- } catch {
9452
- }
9453
9543
  const COMPACTION_TRIGGER_FRACTION = 0.5;
9454
9544
  const COMPACTION_TRIGGER_CAP = 5e4;
9455
9545
  try {
@@ -9733,7 +9823,8 @@ function showDaemonInfo(tui, session) {
9733
9823
  `${c5.dim}The daemon is the executor on every machine: it runs the agent's tools, keeps working after you close the terminal, self-restarts, self-updates, and picks up sessions you start from anywhere.${c5.reset}`
9734
9824
  );
9735
9825
  } else {
9736
- tui.print(`${c5.dim}Manage it with standardcode daemon status | uninstall | add-project <path>.${c5.reset}`);
9826
+ tui.print(`${c5.dim}Daemon commands: standardcode daemon status | restart | update | uninstall.${c5.reset}`);
9827
+ tui.print(`${c5.dim}Add a project: standardcode daemon add-project /absolute/path/to/project${c5.reset}`);
9737
9828
  }
9738
9829
  }
9739
9830
  function showKeybindings(tui) {