@cabane/companion 0.6.177 → 0.6.179

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/cli.js CHANGED
@@ -110,9 +110,6 @@ function write(line) {
110
110
  process.stdout.write(`${line}
111
111
  `);
112
112
  }
113
- function blank() {
114
- process.stdout.write("\n");
115
- }
116
113
  function tildePath(path, home = homedir3()) {
117
114
  if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
118
115
  return path;
@@ -1046,7 +1043,7 @@ var init_bundled_binaries = __esm({
1046
1043
  });
1047
1044
 
1048
1045
  // packages/agent-runtime/src/failure.ts
1049
- import { z as z3 } from "zod";
1046
+ import { z as z4 } from "zod";
1050
1047
  function encodeFailureReason(failure) {
1051
1048
  switch (failure.kind) {
1052
1049
  case "auth_expired":
@@ -1135,11 +1132,11 @@ var turnFailureSchema, USAGE_CAPPED, RATE_LIMITED, SERVER_ERROR, AUTH_EXPIRED, A
1135
1132
  var init_failure = __esm({
1136
1133
  "packages/agent-runtime/src/failure.ts"() {
1137
1134
  "use strict";
1138
- turnFailureSchema = z3.discriminatedUnion("kind", [
1139
- z3.object({ kind: z3.literal("usage_capped"), resetsAt: z3.string().optional() }),
1140
- z3.object({ kind: z3.literal("rate_limited") }),
1141
- z3.object({ kind: z3.literal("server_error") }),
1142
- z3.object({ kind: z3.literal("auth_expired") })
1135
+ turnFailureSchema = z4.discriminatedUnion("kind", [
1136
+ z4.object({ kind: z4.literal("usage_capped"), resetsAt: z4.string().optional() }),
1137
+ z4.object({ kind: z4.literal("rate_limited") }),
1138
+ z4.object({ kind: z4.literal("server_error") }),
1139
+ z4.object({ kind: z4.literal("auth_expired") })
1143
1140
  ]);
1144
1141
  USAGE_CAPPED = "usage_capped";
1145
1142
  RATE_LIMITED = "rate_limited";
@@ -1752,44 +1749,44 @@ var init_version = __esm({
1752
1749
  });
1753
1750
 
1754
1751
  // packages/agent-runtime/src/host-policy.ts
1755
- import { z as z5 } from "zod";
1752
+ import { z as z6 } from "zod";
1756
1753
  var hostPolicySchema;
1757
1754
  var init_host_policy = __esm({
1758
1755
  "packages/agent-runtime/src/host-policy.ts"() {
1759
1756
  "use strict";
1760
- hostPolicySchema = z5.object({
1757
+ hostPolicySchema = z6.object({
1761
1758
  // Host filesystem and shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the notebook pair,
1762
1759
  // git-worktree tools and `REPL`. Off on the locked assistant surface; on in the coding posture.
1763
- hostFs: z5.boolean(),
1760
+ hostFs: z6.boolean(),
1764
1761
  // Public web (`WebSearch` / `WebFetch`). Not host reach; a grant like the rest.
1765
- web: z5.boolean(),
1762
+ web: z6.boolean(),
1766
1763
  // Browser automation (the Playwright MCP surface). Varies by host.
1767
- browser: z5.boolean(),
1764
+ browser: z6.boolean(),
1768
1765
  // User-configured MCP servers permitted.
1769
- userMcp: z5.boolean(),
1766
+ userMcp: z6.boolean(),
1770
1767
  // Subagent dispatch (`Task` / `Agent`) — a grant: `false` on the locked surface, `true` in the
1771
1768
  // coding posture, where the coding allow-hook reaches sub-agent tool calls. A subagent completes
1772
1769
  // within the turn, so it's not an invariant like `scheduling`.
1773
- subagents: z5.boolean(),
1770
+ subagents: z6.boolean(),
1774
1771
  // ── Hard platform invariants — always denied, never granted ────────────────
1775
1772
  // Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*` families,
1776
1773
  // `Monitor`, `Workflow`, …). A turn is one query that resolves on `result`; a scheduled callback
1777
1774
  // fires after the reply window has closed and strands the agent.
1778
- scheduling: z5.literal("never"),
1775
+ scheduling: z6.literal("never"),
1779
1776
  // Human-facing UI prompts (`AskUserQuestion`). Nothing in a Cabane conversation can answer a
1780
1777
  // structured prompt, so the call hangs the turn. The agent asks in its reply instead.
1781
- uiPrompts: z5.literal("never")
1778
+ uiPrompts: z6.literal("never")
1782
1779
  });
1783
1780
  }
1784
1781
  });
1785
1782
 
1786
1783
  // packages/agent-runtime/src/turn-event.ts
1787
- import { z as z6 } from "zod";
1784
+ import { z as z7 } from "zod";
1788
1785
  var turnEventSchema;
1789
1786
  var init_turn_event = __esm({
1790
1787
  "packages/agent-runtime/src/turn-event.ts"() {
1791
1788
  "use strict";
1792
- turnEventSchema = z6.discriminatedUnion("type", [
1789
+ turnEventSchema = z7.discriminatedUnion("type", [
1793
1790
  // The runtime's opaque session state, emitted when the adapter learns it (e.g. the SDK
1794
1791
  // `system/init` frame). The platform stores `state` verbatim per (conversation, agent) and
1795
1792
  // hands it back next turn; only the adapter knows what it means.
@@ -1801,21 +1798,21 @@ var init_turn_event = __esm({
1801
1798
  // server rewinds the mark, so the next turn rebuilds a full catch-up (this turn is unavoidably
1802
1799
  // lossy — the degrade is known only after the manifest was committed). A plain boolean; the
1803
1800
  // runtime-specific reason stays in the adapter's `onWarn` log. Optional.
1804
- z6.object({
1805
- type: z6.literal("session"),
1806
- state: z6.string(),
1807
- degraded: z6.boolean().optional()
1801
+ z7.object({
1802
+ type: z7.literal("session"),
1803
+ state: z7.string(),
1804
+ degraded: z7.boolean().optional()
1808
1805
  }),
1809
1806
  // One readable thinking summary. Transient — surfaced live, never persisted as content.
1810
- z6.object({ type: z6.literal("thinking"), text: z6.string() }),
1807
+ z7.object({ type: z7.literal("thinking"), text: z7.string() }),
1811
1808
  // Assistant text. `terminal: false` is interim narration (commits as `progress`);
1812
1809
  // `terminal: true` is the turn's closing reply (commits as `final`).
1813
- z6.object({ type: z6.literal("text"), body: z6.string(), terminal: z6.boolean() }),
1810
+ z7.object({ type: z7.literal("text"), body: z7.string(), terminal: z7.boolean() }),
1814
1811
  // Runtime/provider-authored prose about the turn — a failure's explanation, or a line the
1815
1812
  // harness wrote on a turn it recovered. Not the agent's words: the pump persists it as
1816
1813
  // `runtime_notice`, drawn in the SystemNote voice. Adapters emit it only from positive runtime
1817
1814
  // evidence; the web never classifies English strings.
1818
- z6.object({ type: z6.literal("runtime_notice"), body: z6.string() }),
1815
+ z7.object({ type: z7.literal("runtime_notice"), body: z7.string() }),
1819
1816
  // A tool-activity transition. `name` is prefix-stripped (`cabane_read`, not
1820
1817
  // `mcp__cabane__cabane_read`); `summary` is the short card label; `phase` pairs `start` with a
1821
1818
  // later `done`/`error` on the same `id`.
@@ -1824,15 +1821,15 @@ var init_turn_event = __esm({
1824
1821
  // log are a consumer concern (docs/turn-protocol.md). `mcpServer` is the MCP server the call
1825
1822
  // came from (`cabane`, `playwright`, …), derived before `name` dropped the prefix; null for a
1826
1823
  // host tool. Optional, read as null when absent.
1827
- z6.object({
1828
- type: z6.literal("tool"),
1829
- id: z6.string(),
1830
- name: z6.string(),
1831
- phase: z6.enum(["start", "done", "error"]),
1832
- summary: z6.string(),
1833
- input: z6.unknown().optional(),
1834
- result: z6.unknown().optional(),
1835
- mcpServer: z6.string().nullable().optional()
1824
+ z7.object({
1825
+ type: z7.literal("tool"),
1826
+ id: z7.string(),
1827
+ name: z7.string(),
1828
+ phase: z7.enum(["start", "done", "error"]),
1829
+ summary: z7.string(),
1830
+ input: z7.unknown().optional(),
1831
+ result: z7.unknown().optional(),
1832
+ mcpServer: z7.string().nullable().optional()
1836
1833
  }),
1837
1834
  // The turn's terminal outcome. `ok:false` carries a machine reason (`no_session`, an error
1838
1835
  // code), surfaced by the host's clear-PATCH as `errorReason`.
@@ -1853,48 +1850,48 @@ var init_turn_event = __esm({
1853
1850
  // started with); `resolvedConfig` the knobs the harness reported (`{ reasoningEffort }` for
1854
1851
  // codex). Known only after the run streams, so they ride this event home and the host relays
1855
1852
  // them on settle. A cancel has no result event at all.
1856
- z6.object({
1857
- type: z6.literal("result"),
1858
- ok: z6.boolean(),
1859
- reason: z6.string().optional(),
1860
- usage: z6.object({
1861
- inputTokens: z6.number(),
1862
- outputTokens: z6.number(),
1863
- cacheReadTokens: z6.number().optional(),
1864
- cacheCreationTokens: z6.number().optional(),
1865
- contextTokens: z6.number().optional(),
1866
- contextWindow: z6.number().optional(),
1853
+ z7.object({
1854
+ type: z7.literal("result"),
1855
+ ok: z7.boolean(),
1856
+ reason: z7.string().optional(),
1857
+ usage: z7.object({
1858
+ inputTokens: z7.number(),
1859
+ outputTokens: z7.number(),
1860
+ cacheReadTokens: z7.number().optional(),
1861
+ cacheCreationTokens: z7.number().optional(),
1862
+ contextTokens: z7.number().optional(),
1863
+ contextWindow: z7.number().optional(),
1867
1864
  // The rest of the harness's context reading, when it gave one: where it will compact, its
1868
1865
  // `/context` breakdown, and every compaction it reported. Claude Code only.
1869
- contextCompactAt: z6.number().optional(),
1870
- contextCategories: z6.array(
1871
- z6.object({
1872
- name: z6.string(),
1873
- tokens: z6.number(),
1874
- kind: z6.enum(["used", "free", "buffer", "deferred"])
1866
+ contextCompactAt: z7.number().optional(),
1867
+ contextCategories: z7.array(
1868
+ z7.object({
1869
+ name: z7.string(),
1870
+ tokens: z7.number(),
1871
+ kind: z7.enum(["used", "free", "buffer", "deferred"])
1875
1872
  })
1876
1873
  ).optional(),
1877
- contextCompactions: z6.array(
1878
- z6.object({
1879
- trigger: z6.enum(["manual", "auto"]),
1880
- preTokens: z6.number(),
1881
- postTokens: z6.number().optional()
1874
+ contextCompactions: z7.array(
1875
+ z7.object({
1876
+ trigger: z7.enum(["manual", "auto"]),
1877
+ preTokens: z7.number(),
1878
+ postTokens: z7.number().optional()
1882
1879
  })
1883
1880
  ).optional()
1884
1881
  }).optional(),
1885
- resolvedModel: z6.string().optional(),
1886
- resolvedConfig: z6.object({
1887
- effort: z6.string().optional(),
1888
- thinking: z6.string().optional(),
1889
- reasoningEffort: z6.string().optional()
1882
+ resolvedModel: z7.string().optional(),
1883
+ resolvedConfig: z7.object({
1884
+ effort: z7.string().optional(),
1885
+ thinking: z7.string().optional(),
1886
+ reasoningEffort: z7.string().optional()
1890
1887
  }).optional(),
1891
1888
  // The harness-reported MCP inventory from this turn's init frame. Diagnostic only: names,
1892
1889
  // statuses and a count — never server definitions, credentials or session ids.
1893
1890
  // `initReceived:false` tells a missing init frame from a real empty inventory.
1894
- mcpInventory: z6.object({
1895
- initReceived: z6.boolean(),
1896
- servers: z6.array(z6.object({ name: z6.string(), status: z6.string() })),
1897
- toolCount: z6.number().int().nonnegative()
1891
+ mcpInventory: z7.object({
1892
+ initReceived: z7.boolean(),
1893
+ servers: z7.array(z7.object({ name: z7.string(), status: z7.string() })),
1894
+ toolCount: z7.number().int().nonnegative()
1898
1895
  }).optional()
1899
1896
  })
1900
1897
  ]);
@@ -1902,27 +1899,27 @@ var init_turn_event = __esm({
1902
1899
  });
1903
1900
 
1904
1901
  // packages/agent-runtime/src/turn-diagnostics.ts
1905
- import { z as z7 } from "zod";
1902
+ import { z as z8 } from "zod";
1906
1903
  var turnResultReasonSchema, turnOutcomes, turnSessionModes, turnRuntimeResultKinds, turnFinalSources, turnDiagnosticsSchema;
1907
1904
  var init_turn_diagnostics = __esm({
1908
1905
  "packages/agent-runtime/src/turn-diagnostics.ts"() {
1909
1906
  "use strict";
1910
- turnResultReasonSchema = z7.discriminatedUnion("kind", [
1911
- z7.object({ kind: z7.literal("usage_capped"), resetsAt: z7.string().optional() }),
1912
- z7.object({ kind: z7.literal("rate_limited") }),
1913
- z7.object({ kind: z7.literal("server_error") }),
1914
- z7.object({ kind: z7.literal("auth_expired") }),
1915
- z7.object({ kind: z7.literal("no_result") }),
1916
- z7.object({ kind: z7.literal("empty_result") }),
1917
- z7.object({ kind: z7.literal("empty_result_unverified") }),
1918
- z7.object({ kind: z7.literal("timeout_idle") }),
1919
- z7.object({ kind: z7.literal("timeout_total") }),
1920
- z7.object({ kind: z7.literal("cancelled") }),
1921
- z7.object({ kind: z7.literal("lease_lost") }),
1922
- z7.object({ kind: z7.literal("skipped") }),
1923
- z7.object({ kind: z7.literal("session_start_failed") }),
1924
- z7.object({ kind: z7.literal("workspace_tools_missing") }),
1925
- z7.object({ kind: z7.literal("runtime_error") })
1907
+ turnResultReasonSchema = z8.discriminatedUnion("kind", [
1908
+ z8.object({ kind: z8.literal("usage_capped"), resetsAt: z8.string().optional() }),
1909
+ z8.object({ kind: z8.literal("rate_limited") }),
1910
+ z8.object({ kind: z8.literal("server_error") }),
1911
+ z8.object({ kind: z8.literal("auth_expired") }),
1912
+ z8.object({ kind: z8.literal("no_result") }),
1913
+ z8.object({ kind: z8.literal("empty_result") }),
1914
+ z8.object({ kind: z8.literal("empty_result_unverified") }),
1915
+ z8.object({ kind: z8.literal("timeout_idle") }),
1916
+ z8.object({ kind: z8.literal("timeout_total") }),
1917
+ z8.object({ kind: z8.literal("cancelled") }),
1918
+ z8.object({ kind: z8.literal("lease_lost") }),
1919
+ z8.object({ kind: z8.literal("skipped") }),
1920
+ z8.object({ kind: z8.literal("session_start_failed") }),
1921
+ z8.object({ kind: z8.literal("workspace_tools_missing") }),
1922
+ z8.object({ kind: z8.literal("runtime_error") })
1926
1923
  ]);
1927
1924
  turnOutcomes = ["success", "failure", "cancelled", "skipped"];
1928
1925
  turnSessionModes = ["fresh", "resumed", "degraded"];
@@ -1934,25 +1931,25 @@ var init_turn_diagnostics = __esm({
1934
1931
  "marker",
1935
1932
  "none"
1936
1933
  ];
1937
- turnDiagnosticsSchema = z7.object({
1938
- outcome: z7.enum(turnOutcomes),
1934
+ turnDiagnosticsSchema = z8.object({
1935
+ outcome: z8.enum(turnOutcomes),
1939
1936
  resultReason: turnResultReasonSchema.nullable(),
1940
- sessionMode: z7.enum(turnSessionModes),
1941
- sessionFingerprint: z7.string().regex(/^[a-f0-9]{16}$/).nullable(),
1942
- eventCounts: z7.object({
1943
- session: z7.number().int().nonnegative(),
1944
- text: z7.number().int().nonnegative(),
1945
- runtime_notice: z7.number().int().nonnegative(),
1946
- thinking: z7.number().int().nonnegative(),
1947
- tool: z7.number().int().nonnegative(),
1948
- result: z7.number().int().nonnegative()
1937
+ sessionMode: z8.enum(turnSessionModes),
1938
+ sessionFingerprint: z8.string().regex(/^[a-f0-9]{16}$/).nullable(),
1939
+ eventCounts: z8.object({
1940
+ session: z8.number().int().nonnegative(),
1941
+ text: z8.number().int().nonnegative(),
1942
+ runtime_notice: z8.number().int().nonnegative(),
1943
+ thinking: z8.number().int().nonnegative(),
1944
+ tool: z8.number().int().nonnegative(),
1945
+ result: z8.number().int().nonnegative()
1949
1946
  }),
1950
- runtimeResultKind: z7.enum(turnRuntimeResultKinds).nullable(),
1951
- finalSource: z7.enum(turnFinalSources),
1952
- mcpInventory: z7.object({
1953
- initReceived: z7.boolean(),
1954
- servers: z7.array(z7.object({ name: z7.string(), status: z7.string() })),
1955
- toolCount: z7.number().int().nonnegative()
1947
+ runtimeResultKind: z8.enum(turnRuntimeResultKinds).nullable(),
1948
+ finalSource: z8.enum(turnFinalSources),
1949
+ mcpInventory: z8.object({
1950
+ initReceived: z8.boolean(),
1951
+ servers: z8.array(z8.object({ name: z8.string(), status: z8.string() })),
1952
+ toolCount: z8.number().int().nonnegative()
1956
1953
  }).optional()
1957
1954
  });
1958
1955
  }
@@ -2026,105 +2023,105 @@ var init_empty_result = __esm({
2026
2023
  });
2027
2024
 
2028
2025
  // packages/agent-runtime/src/turn-request.ts
2029
- import { z as z8 } from "zod";
2026
+ import { z as z9 } from "zod";
2030
2027
  var contentBlockSchema, effortLevelSchema, resolvedRunConfigSchema, resolvedMcpServerSchema, resolvedMcpServersSchema, hostInjectedServersSchema, turnRequestSchema;
2031
2028
  var init_turn_request = __esm({
2032
2029
  "packages/agent-runtime/src/turn-request.ts"() {
2033
2030
  "use strict";
2034
2031
  init_host_policy();
2035
- contentBlockSchema = z8.discriminatedUnion("type", [
2036
- z8.object({ type: z8.literal("text"), text: z8.string() }),
2037
- z8.object({
2038
- type: z8.literal("image"),
2039
- source: z8.object({ type: z8.literal("url"), url: z8.string() })
2032
+ contentBlockSchema = z9.discriminatedUnion("type", [
2033
+ z9.object({ type: z9.literal("text"), text: z9.string() }),
2034
+ z9.object({
2035
+ type: z9.literal("image"),
2036
+ source: z9.object({ type: z9.literal("url"), url: z9.string() })
2040
2037
  }),
2041
- z8.object({
2042
- type: z8.literal("document"),
2043
- source: z8.object({ type: z8.literal("url"), url: z8.string() })
2038
+ z9.object({
2039
+ type: z9.literal("document"),
2040
+ source: z9.object({ type: z9.literal("url"), url: z9.string() })
2044
2041
  })
2045
2042
  ]);
2046
- effortLevelSchema = z8.enum(["low", "medium", "high", "xhigh", "max"]);
2047
- resolvedRunConfigSchema = z8.object({
2048
- model: z8.string().nullable(),
2043
+ effortLevelSchema = z9.enum(["low", "medium", "high", "xhigh", "max"]);
2044
+ resolvedRunConfigSchema = z9.object({
2045
+ model: z9.string().nullable(),
2049
2046
  effort: effortLevelSchema.optional(),
2050
- runtimeOptions: z8.record(z8.string(), z8.unknown()).optional()
2047
+ runtimeOptions: z9.record(z9.string(), z9.unknown()).optional()
2051
2048
  });
2052
- resolvedMcpServerSchema = z8.union([
2053
- z8.object({
2054
- type: z8.literal("stdio").optional(),
2055
- command: z8.string(),
2056
- args: z8.array(z8.string()).optional(),
2057
- env: z8.record(z8.string(), z8.string()).optional(),
2058
- envFromEnv: z8.record(z8.string(), z8.string()).optional()
2049
+ resolvedMcpServerSchema = z9.union([
2050
+ z9.object({
2051
+ type: z9.literal("stdio").optional(),
2052
+ command: z9.string(),
2053
+ args: z9.array(z9.string()).optional(),
2054
+ env: z9.record(z9.string(), z9.string()).optional(),
2055
+ envFromEnv: z9.record(z9.string(), z9.string()).optional()
2059
2056
  }),
2060
- z8.object({
2061
- type: z8.literal("http"),
2062
- url: z8.string(),
2063
- headers: z8.record(z8.string(), z8.string()).optional(),
2064
- headersFromEnv: z8.record(z8.string(), z8.string()).optional()
2057
+ z9.object({
2058
+ type: z9.literal("http"),
2059
+ url: z9.string(),
2060
+ headers: z9.record(z9.string(), z9.string()).optional(),
2061
+ headersFromEnv: z9.record(z9.string(), z9.string()).optional()
2065
2062
  }),
2066
- z8.object({
2067
- type: z8.literal("sse"),
2068
- url: z8.string(),
2069
- headers: z8.record(z8.string(), z8.string()).optional(),
2070
- headersFromEnv: z8.record(z8.string(), z8.string()).optional()
2063
+ z9.object({
2064
+ type: z9.literal("sse"),
2065
+ url: z9.string(),
2066
+ headers: z9.record(z9.string(), z9.string()).optional(),
2067
+ headersFromEnv: z9.record(z9.string(), z9.string()).optional()
2071
2068
  })
2072
2069
  ]);
2073
- resolvedMcpServersSchema = z8.record(z8.string(), resolvedMcpServerSchema);
2074
- hostInjectedServersSchema = z8.record(z8.string(), z8.unknown());
2075
- turnRequestSchema = z8.object({
2070
+ resolvedMcpServersSchema = z9.record(z9.string(), resolvedMcpServerSchema);
2071
+ hostInjectedServersSchema = z9.record(z9.string(), z9.unknown());
2072
+ turnRequestSchema = z9.object({
2076
2073
  // Server-composed standing instructions: the core segment (body, tool naming, identity, agent
2077
2074
  // charter) and, after it, the conversation segment (workspace and room charters, skill indexes,
2078
2075
  // roster). One string to the adapter, current as of this dispatch.
2079
- systemPrompt: z8.string(),
2076
+ systemPrompt: z9.string(),
2080
2077
  // The same text as its two ordered segments, so an adapter that writes standing text into the
2081
2078
  // conversation (Codex's input preamble) can tell which half changed. `systemPrompt` is
2082
2079
  // `joinStanding(standing)`. Optional: absent from an API older than the split, where
2083
2080
  // `systemPrompt` is the core alone and the conversation material rides `prompt`.
2084
- standing: z8.object({ core: z8.string(), conversation: z8.string() }).optional(),
2081
+ standing: z9.object({ core: z9.string(), conversation: z9.string() }).optional(),
2085
2082
  // Server-composed per-turn user text (anchor reminder + the triggering message).
2086
- prompt: z8.string(),
2083
+ prompt: z9.string(),
2087
2084
  // The multi-block user-message body (text + vision).
2088
- content: z8.array(contentBlockSchema),
2085
+ content: z9.array(contentBlockSchema),
2089
2086
  // Portable-or-dialect run-config (above).
2090
2087
  config: resolvedRunConfigSchema,
2091
2088
  // Abstract capability grants; the adapter maps them to tool names.
2092
2089
  policy: hostPolicySchema,
2093
2090
  // Prior opaque session state, or null for a fresh session.
2094
- session: z8.string().nullable(),
2091
+ session: z9.string().nullable(),
2095
2092
  // The cabane control-plane coordinates for this turn's MCP + post-back.
2096
- cabane: z8.object({
2097
- mcpUrl: z8.string(),
2098
- bearer: z8.string(),
2099
- activeConversationId: z8.string(),
2093
+ cabane: z9.object({
2094
+ mcpUrl: z9.string(),
2095
+ bearer: z9.string(),
2096
+ activeConversationId: z9.string(),
2100
2097
  // Scoped turn-control MCP URL. Codex and opencode mount it as `cabane_companion` with the bearer
2101
2098
  // and active-conversation header, for ask, withdraw_ask, wake_me, cancel_wake and skip_turn.
2102
2099
  // Claude Code mounts the companion's in-process instance instead. Optional; the companion
2103
2100
  // populates it.
2104
- turnControlUrl: z8.string().optional(),
2101
+ turnControlUrl: z9.string().optional(),
2105
2102
  // Workspace coordinate retained for envelope compatibility. Current
2106
2103
  // adapters use MCP, whose workspace operations take workspaceId explicitly.
2107
2104
  // The companion supplies this field; it is not required by those adapters.
2108
- workspaceId: z8.string().optional()
2105
+ workspaceId: z9.string().optional()
2109
2106
  }),
2110
2107
  // Machine-local resolution (host-filled): the checkout cwd, extra env from a
2111
2108
  // prepare hook, and the resolved user MCP servers.
2112
- local: z8.object({
2113
- cwd: z8.string().optional(),
2114
- env: z8.record(z8.string(), z8.string()).optional(),
2109
+ local: z9.object({
2110
+ cwd: z9.string().optional(),
2111
+ env: z9.record(z9.string(), z9.string()).optional(),
2115
2112
  mcpServers: resolvedMcpServersSchema.optional(),
2116
2113
  // The values `headersFromEnv` names, per turn (`CABANE_MCP_<n>` → complete header value). The
2117
2114
  // adapter merges them into the harness process's environment and nowhere else; the host never
2118
2115
  // merges them into its own. Absent when no server references one.
2119
- secretEnv: z8.record(z8.string(), z8.string()).optional(),
2116
+ secretEnv: z9.record(z9.string(), z9.string()).optional(),
2120
2117
  // Machine-local claude-code knobs for an operator running their own companion — the auto-memory
2121
2118
  // escape hatch. `autoMemory: true` opts back into Claude Code's auto-memory (governed by the
2122
2119
  // operator's `.claude/settings.json`); absent/false keeps the adapter's force-off default
2123
2120
  // (`buildClaudeCodeOptions`).
2124
- claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional()
2121
+ claudeCode: z9.object({ autoMemory: z9.boolean().optional() }).optional()
2125
2122
  }),
2126
2123
  // Host-owned injected servers (host-filled).
2127
- extra: z8.object({
2124
+ extra: z9.object({
2128
2125
  mcpServers: hostInjectedServersSchema
2129
2126
  })
2130
2127
  });
@@ -2148,18 +2145,18 @@ var init_standing = __esm({
2148
2145
  });
2149
2146
 
2150
2147
  // packages/agent-runtime/src/conformance.ts
2151
- import { z as z9 } from "zod";
2148
+ import { z as z10 } from "zod";
2152
2149
  var conformanceFixtureSchema;
2153
2150
  var init_conformance = __esm({
2154
2151
  "packages/agent-runtime/src/conformance.ts"() {
2155
2152
  "use strict";
2156
2153
  init_turn_event();
2157
2154
  init_turn_request();
2158
- conformanceFixtureSchema = z9.object({
2159
- name: z9.string(),
2155
+ conformanceFixtureSchema = z10.object({
2156
+ name: z10.string(),
2160
2157
  request: turnRequestSchema,
2161
- nativeStream: z9.array(z9.unknown()),
2162
- expected: z9.array(turnEventSchema)
2158
+ nativeStream: z10.array(z10.unknown()),
2159
+ expected: z10.array(turnEventSchema)
2163
2160
  });
2164
2161
  }
2165
2162
  });
@@ -2656,7 +2653,7 @@ var init_mcp_secret_env = __esm({
2656
2653
  });
2657
2654
 
2658
2655
  // packages/agent-runtime/src/claude-code/policy.ts
2659
- import { z as z10 } from "zod";
2656
+ import { z as z11 } from "zod";
2660
2657
  function disallowedToolsFor(policy) {
2661
2658
  const tools = [...UNSUPPORTED_TOOLS, ...STRANDING_TOOLS];
2662
2659
  if (!policy.subagents) tools.unshift(...SUBAGENT_TOOLS);
@@ -2716,18 +2713,18 @@ var init_policy = __esm({
2716
2713
  "Workflow"
2717
2714
  ];
2718
2715
  DEFAULT_WEB_TOOLS = ["WebSearch", "WebFetch"];
2719
- claudeCodeDialectSchema = z10.object({
2720
- thinking: z10.discriminatedUnion("type", [
2721
- z10.object({
2722
- type: z10.literal("adaptive"),
2723
- display: z10.enum(["summarized", "omitted"]).optional()
2716
+ claudeCodeDialectSchema = z11.object({
2717
+ thinking: z11.discriminatedUnion("type", [
2718
+ z11.object({
2719
+ type: z11.literal("adaptive"),
2720
+ display: z11.enum(["summarized", "omitted"]).optional()
2724
2721
  }),
2725
- z10.object({
2726
- type: z10.literal("enabled"),
2727
- budgetTokens: z10.number().int().positive().optional(),
2728
- display: z10.enum(["summarized", "omitted"]).optional()
2722
+ z11.object({
2723
+ type: z11.literal("enabled"),
2724
+ budgetTokens: z11.number().int().positive().optional(),
2725
+ display: z11.enum(["summarized", "omitted"]).optional()
2729
2726
  }),
2730
- z10.object({ type: z10.literal("disabled") })
2727
+ z11.object({ type: z11.literal("disabled") })
2731
2728
  ]).optional()
2732
2729
  }).loose();
2733
2730
  }
@@ -4297,7 +4294,7 @@ var init_loop2 = __esm({
4297
4294
  });
4298
4295
 
4299
4296
  // packages/agent-runtime/src/opencode/policy.ts
4300
- import { z as z11 } from "zod";
4297
+ import { z as z12 } from "zod";
4301
4298
  function opencodeToolPolicy(policy) {
4302
4299
  const tools = {};
4303
4300
  const deny = (names) => {
@@ -4332,8 +4329,8 @@ var init_policy2 = __esm({
4332
4329
  OPENCODE_SUBAGENT_TOOLS = ["task"];
4333
4330
  OPENCODE_SCHEDULING_TOOLS = [];
4334
4331
  OPENCODE_UI_PROMPT_TOOLS = ["question"];
4335
- opencodeDialectSchema = z11.object({
4336
- agent: z11.string().min(1).optional()
4332
+ opencodeDialectSchema = z12.object({
4333
+ agent: z12.string().min(1).optional()
4337
4334
  }).loose();
4338
4335
  }
4339
4336
  });
@@ -5624,7 +5621,7 @@ var init_loop3 = __esm({
5624
5621
  });
5625
5622
 
5626
5623
  // packages/agent-runtime/src/codex/policy.ts
5627
- import { z as z12 } from "zod";
5624
+ import { z as z13 } from "zod";
5628
5625
  function codexToolPolicy(policy) {
5629
5626
  return policy.hostFs ? {
5630
5627
  sandboxMode: "danger-full-access",
@@ -5651,8 +5648,8 @@ var init_policy3 = __esm({
5651
5648
  "packages/agent-runtime/src/codex/policy.ts"() {
5652
5649
  "use strict";
5653
5650
  CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
5654
- codexDialectSchema = z12.object({
5655
- modelReasoningEffort: z12.enum(CODEX_REASONING_EFFORTS).optional()
5651
+ codexDialectSchema = z13.object({
5652
+ modelReasoningEffort: z13.enum(CODEX_REASONING_EFFORTS).optional()
5656
5653
  }).loose();
5657
5654
  }
5658
5655
  });
@@ -7123,7 +7120,7 @@ var init_src = __esm({
7123
7120
  // src/runner/sandbox-policy.ts
7124
7121
  import { NetworkConfigSchema } from "@anthropic-ai/sandbox-runtime";
7125
7122
  import { globToRegex } from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js";
7126
- import { z as z13 } from "zod";
7123
+ import { z as z14 } from "zod";
7127
7124
  function parseSandboxPolicy(raw) {
7128
7125
  if (raw === void 0) return { ok: true, policy: {} };
7129
7126
  const result = sandboxPolicySchema.safeParse(raw);
@@ -7243,29 +7240,29 @@ var hostPattern, secretRef, envCredentialSchema, gitCredentialSchema, sandboxPol
7243
7240
  var init_sandbox_policy = __esm({
7244
7241
  "src/runner/sandbox-policy.ts"() {
7245
7242
  "use strict";
7246
- hostPattern = z13.string().min(1);
7247
- secretRef = z13.string().regex(/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/, "must be a ${NAME} reference into secrets.json");
7248
- envCredentialSchema = z13.object({ secret: secretRef, hosts: z13.array(hostPattern).min(1) }).strict();
7249
- gitCredentialSchema = z13.object({
7250
- host: z13.string().regex(/^[A-Za-z0-9.-]+(:\d+)?$/, "must be a host name, like github.com"),
7251
- username: z13.string().min(1).optional(),
7243
+ hostPattern = z14.string().min(1);
7244
+ secretRef = z14.string().regex(/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/, "must be a ${NAME} reference into secrets.json");
7245
+ envCredentialSchema = z14.object({ secret: secretRef, hosts: z14.array(hostPattern).min(1) }).strict();
7246
+ gitCredentialSchema = z14.object({
7247
+ host: z14.string().regex(/^[A-Za-z0-9.-]+(:\d+)?$/, "must be a host name, like github.com"),
7248
+ username: z14.string().min(1).optional(),
7252
7249
  secret: secretRef
7253
7250
  }).strict();
7254
- sandboxPolicySchema = z13.object({
7255
- hosts: z13.object({
7256
- allow: z13.array(hostPattern).optional(),
7257
- deny: z13.array(hostPattern).optional(),
7258
- defaults: z13.boolean().optional()
7251
+ sandboxPolicySchema = z14.object({
7252
+ hosts: z14.object({
7253
+ allow: z14.array(hostPattern).optional(),
7254
+ deny: z14.array(hostPattern).optional(),
7255
+ defaults: z14.boolean().optional()
7259
7256
  }).strict().optional(),
7260
- filesystem: z13.object({
7261
- mode: z13.enum(["blocklist", "strict"]).optional(),
7262
- denyRead: z13.array(z13.string().min(1)).optional(),
7263
- allowRead: z13.array(z13.string().min(1)).optional(),
7264
- allowWrite: z13.array(z13.string().min(1)).optional()
7257
+ filesystem: z14.object({
7258
+ mode: z14.enum(["blocklist", "strict"]).optional(),
7259
+ denyRead: z14.array(z14.string().min(1)).optional(),
7260
+ allowRead: z14.array(z14.string().min(1)).optional(),
7261
+ allowWrite: z14.array(z14.string().min(1)).optional()
7265
7262
  }).strict().optional(),
7266
- credentials: z13.object({
7267
- env: z13.record(z13.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), envCredentialSchema).optional(),
7268
- git: z13.array(gitCredentialSchema).optional()
7263
+ credentials: z14.object({
7264
+ env: z14.record(z14.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), envCredentialSchema).optional(),
7265
+ git: z14.array(gitCredentialSchema).optional()
7269
7266
  }).strict().optional()
7270
7267
  }).strict();
7271
7268
  DEFAULT_ALLOWED_HOSTS = [
@@ -7647,7 +7644,7 @@ var init_turn_containment = __esm({
7647
7644
  // src/secrets.ts
7648
7645
  import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
7649
7646
  import { join as join14 } from "path";
7650
- import { z as z14 } from "zod";
7647
+ import { z as z15 } from "zod";
7651
7648
  function secretsPath() {
7652
7649
  return join14(cabaneDir(), "secrets.json");
7653
7650
  }
@@ -7770,14 +7767,14 @@ var init_secrets = __esm({
7770
7767
  init_config();
7771
7768
  init_errors();
7772
7769
  PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
7773
- secretStoreSchema = z14.record(z14.string(), z14.string());
7770
+ secretStoreSchema = z15.record(z15.string(), z15.string());
7774
7771
  SECRET_ENV_NAME = /^CABANE_MCP_(\d+)$/;
7775
7772
  ERROR_REASON_MAX = 200;
7776
7773
  }
7777
7774
  });
7778
7775
 
7779
7776
  // src/run-config.ts
7780
- import { z as z15 } from "zod";
7777
+ import { z as z16 } from "zod";
7781
7778
  function parseRunConfig(raw, onWarn) {
7782
7779
  const result = runConfigSchema.safeParse(raw ?? {});
7783
7780
  if (result.success) return result.data;
@@ -7790,48 +7787,48 @@ var mcpStdioServerSchema, mcpHttpServerSchema, mcpSseServerSchema, mcpServerDefS
7790
7787
  var init_run_config = __esm({
7791
7788
  "src/run-config.ts"() {
7792
7789
  "use strict";
7793
- mcpStdioServerSchema = z15.object({
7794
- type: z15.literal("stdio").optional(),
7795
- command: z15.string().min(1),
7796
- args: z15.array(z15.string()).optional(),
7797
- env: z15.record(z15.string(), z15.string()).optional(),
7790
+ mcpStdioServerSchema = z16.object({
7791
+ type: z16.literal("stdio").optional(),
7792
+ command: z16.string().min(1),
7793
+ args: z16.array(z16.string()).optional(),
7794
+ env: z16.record(z16.string(), z16.string()).optional(),
7798
7795
  // Variable → the name of a `secretEnv` entry holding its value. Set by the server on a
7799
7796
  // room's shared browser, whose connect URL is a credential.
7800
- envFromEnv: z15.record(z15.string(), z15.string()).optional()
7797
+ envFromEnv: z16.record(z16.string(), z16.string()).optional()
7801
7798
  });
7802
- mcpHttpServerSchema = z15.object({
7803
- type: z15.literal("http"),
7804
- url: z15.string().url(),
7805
- headers: z15.record(z15.string(), z15.string()).optional(),
7799
+ mcpHttpServerSchema = z16.object({
7800
+ type: z16.literal("http"),
7801
+ url: z16.string().url(),
7802
+ headers: z16.record(z16.string(), z16.string()).optional(),
7806
7803
  // Header → the name of a `secretEnv` entry holding its value. Set by
7807
7804
  // the server on a room connection's entry, and by `resolveMcpSecrets` on a
7808
7805
  // header whose `${VAR}` it resolved.
7809
- headersFromEnv: z15.record(z15.string(), z15.string()).optional()
7806
+ headersFromEnv: z16.record(z16.string(), z16.string()).optional()
7810
7807
  });
7811
- mcpSseServerSchema = z15.object({
7812
- type: z15.literal("sse"),
7813
- url: z15.string().url(),
7814
- headers: z15.record(z15.string(), z15.string()).optional(),
7808
+ mcpSseServerSchema = z16.object({
7809
+ type: z16.literal("sse"),
7810
+ url: z16.string().url(),
7811
+ headers: z16.record(z16.string(), z16.string()).optional(),
7815
7812
  // Header → the name of a `secretEnv` entry holding its value. Set by
7816
7813
  // the server on a room connection's entry, and by `resolveMcpSecrets` on a
7817
7814
  // header whose `${VAR}` it resolved.
7818
- headersFromEnv: z15.record(z15.string(), z15.string()).optional()
7815
+ headersFromEnv: z16.record(z16.string(), z16.string()).optional()
7819
7816
  });
7820
- mcpServerDefSchema = z15.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
7821
- thinkingConfigSchema = z15.discriminatedUnion("type", [
7822
- z15.object({ type: z15.literal("adaptive") }),
7823
- z15.object({ type: z15.literal("enabled"), budgetTokens: z15.number().int().positive().optional() }),
7824
- z15.object({ type: z15.literal("disabled") })
7817
+ mcpServerDefSchema = z16.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
7818
+ thinkingConfigSchema = z16.discriminatedUnion("type", [
7819
+ z16.object({ type: z16.literal("adaptive") }),
7820
+ z16.object({ type: z16.literal("enabled"), budgetTokens: z16.number().int().positive().optional() }),
7821
+ z16.object({ type: z16.literal("disabled") })
7825
7822
  ]);
7826
- effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
7827
- runConfigSchema = z15.object({
7823
+ effortSchema = z16.enum(["low", "medium", "high", "xhigh", "max"]);
7824
+ runConfigSchema = z16.object({
7828
7825
  // `true` grants the host filesystem/shell; absent is the locked surface. Kept
7829
7826
  // in lockstep with `@cabane/shared`'s
7830
7827
  // `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
7831
7828
  // stripped, so an older companion riding a newer server never rejects the config).
7832
- hostAccess: z15.boolean().optional(),
7833
- mcpServers: z15.record(z15.string(), mcpServerDefSchema).optional(),
7834
- model: z15.string().min(1).optional(),
7829
+ hostAccess: z16.boolean().optional(),
7830
+ mcpServers: z16.record(z16.string(), mcpServerDefSchema).optional(),
7831
+ model: z16.string().min(1).optional(),
7835
7832
  thinking: thinkingConfigSchema.optional(),
7836
7833
  effort: effortSchema.optional()
7837
7834
  });
@@ -7840,7 +7837,7 @@ var init_run_config = __esm({
7840
7837
  });
7841
7838
 
7842
7839
  // src/runner/protocol.ts
7843
- import { z as z16 } from "zod";
7840
+ import { z as z17 } from "zod";
7844
7841
  function required(env, name) {
7845
7842
  const value = env[name];
7846
7843
  if (!value) throw new Error(`runner: ${name} is not set`);
@@ -7923,76 +7920,76 @@ var init_protocol = __esm({
7923
7920
  // Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
7924
7921
  spec: "CABANE_RUNNER_SPEC"
7925
7922
  };
7926
- runtimeHealthSchema = z16.object({
7927
- quotaState: z16.enum(["ok", "limited", "auth_expired", "unknown"]),
7928
- limitedUntil: z16.string().nullable(),
7929
- lastError: z16.string().nullable()
7923
+ runtimeHealthSchema = z17.object({
7924
+ quotaState: z17.enum(["ok", "limited", "auth_expired", "unknown"]),
7925
+ limitedUntil: z17.string().nullable(),
7926
+ lastError: z17.string().nullable()
7930
7927
  });
7931
- runnerSpecSchema = z16.object({
7932
- deviceId: z16.string().optional(),
7928
+ runnerSpecSchema = z17.object({
7929
+ deviceId: z17.string().optional(),
7933
7930
  // The launcher's boot id: the one its heartbeat carries, so a runner's lease
7934
7931
  // matches the device that holds it.
7935
- bootId: z16.string(),
7936
- workspaceId: z16.string(),
7937
- workspaceSlug: z16.string(),
7938
- agentId: z16.string(),
7939
- agentUsername: z16.string(),
7940
- agentName: z16.string(),
7932
+ bootId: z17.string(),
7933
+ workspaceId: z17.string(),
7934
+ workspaceSlug: z17.string(),
7935
+ agentId: z17.string(),
7936
+ agentUsername: z17.string(),
7937
+ agentName: z17.string(),
7941
7938
  local: localAgentConfigSchema,
7942
7939
  runConfig: runConfigSchema,
7943
- opencodeServerUrl: z16.string().optional(),
7944
- claudeCodeAvailable: z16.boolean(),
7945
- codexAvailable: z16.boolean(),
7946
- idleTimeoutMs: z16.number().int().positive().optional(),
7947
- totalTimeoutMs: z16.number().int().positive().optional(),
7948
- leaseRenewalMs: z16.number().int().positive().optional(),
7949
- preparingRowDelayMs: z16.number().int().nonnegative().optional(),
7940
+ opencodeServerUrl: z17.string().optional(),
7941
+ claudeCodeAvailable: z17.boolean(),
7942
+ codexAvailable: z17.boolean(),
7943
+ idleTimeoutMs: z17.number().int().positive().optional(),
7944
+ totalTimeoutMs: z17.number().int().positive().optional(),
7945
+ leaseRenewalMs: z17.number().int().positive().optional(),
7946
+ preparingRowDelayMs: z17.number().int().nonnegative().optional(),
7950
7947
  // Whether the turn writes a transcript. The directory is the runner's own
7951
7948
  // (`transcriptsDir()`), so `CABANE_RUNNER_STATE_DIR` moves it with the outbox.
7952
- transcripts: z16.boolean(),
7949
+ transcripts: z17.boolean(),
7953
7950
  // Whether the harness starts under the OS sandbox. The launcher sends 'os'
7954
7951
  // only when this host can make one.
7955
- sandbox: z16.enum(["os", "none"]),
7952
+ sandbox: z17.enum(["os", "none"]),
7956
7953
  // The device's sandbox policy, validated by the launcher. Absent
7957
7954
  // when the sandbox is off.
7958
7955
  sandboxPolicy: sandboxPolicySchema.optional(),
7959
7956
  // Every companion store the launcher's latest walk found, denied
7960
7957
  // to the harness. Absent when the sandbox is off.
7961
- companionStores: z16.array(z16.string()).optional(),
7962
- logLevel: z16.string(),
7958
+ companionStores: z17.array(z17.string()).optional(),
7959
+ logLevel: z17.string(),
7963
7960
  // The launcher's connector-health observations, by runtime, as of the start.
7964
- connectorHealth: z16.record(z16.string(), runtimeHealthSchema),
7961
+ connectorHealth: z17.record(z17.string(), runtimeHealthSchema),
7965
7962
  // True when this (conversation, agent) pair was already told its session
7966
7963
  // could not be saved: the notice is once per pair, not once per process.
7967
- sessionWriteNoticeClaimed: z16.boolean(),
7964
+ sessionWriteNoticeClaimed: z17.boolean(),
7968
7965
  // The runner's identity, minted with its credential. The start
7969
7966
  // PATCH carries it so the lease knows which runner holds the span.
7970
- runnerId: z16.string(),
7967
+ runnerId: z17.string(),
7971
7968
  // The launcher keeps the runtime's session files beside the attempt
7972
7969
  // and restores them into this runner's home, so the runner names the attempt
7973
7970
  // each context was composed for and waits for the launcher to say it's in
7974
7971
  // place. Absent: nothing to wait for (a runner on the launcher's own disk).
7975
- attemptState: z16.boolean().optional(),
7972
+ attemptState: z17.boolean().optional(),
7976
7973
  // MCP `${NAME}` secrets come from the launcher, by name, instead of
7977
7974
  // this machine's secrets.json. Absent: the runner reads the device's file.
7978
- secretsFromLauncher: z16.boolean().optional()
7975
+ secretsFromLauncher: z17.boolean().optional()
7979
7976
  });
7980
- dispatchSchema = z16.object({
7981
- type: z16.literal("device:dispatch_requested"),
7982
- agentId: z16.string(),
7983
- deviceId: z16.string(),
7984
- conversationId: z16.string(),
7985
- messageId: z16.string(),
7986
- episode: z16.number().int().optional()
7977
+ dispatchSchema = z17.object({
7978
+ type: z17.literal("device:dispatch_requested"),
7979
+ agentId: z17.string(),
7980
+ deviceId: z17.string(),
7981
+ conversationId: z17.string(),
7982
+ messageId: z17.string(),
7983
+ episode: z17.number().int().optional()
7987
7984
  });
7988
- credentialsSchema = z16.object({
7989
- credential: z16.string().startsWith("cabrn_"),
7990
- sandboxCredentials: z16.array(
7991
- z16.object({
7992
- name: z16.string(),
7993
- value: z16.string(),
7994
- hosts: z16.array(z16.string()),
7995
- gitHost: z16.string().optional()
7985
+ credentialsSchema = z17.object({
7986
+ credential: z17.string().startsWith("cabrn_"),
7987
+ sandboxCredentials: z17.array(
7988
+ z17.object({
7989
+ name: z17.string(),
7990
+ value: z17.string(),
7991
+ hosts: z17.array(z17.string()),
7992
+ gitHost: z17.string().optional()
7996
7993
  })
7997
7994
  ).optional()
7998
7995
  });
@@ -8443,7 +8440,7 @@ var init_launch = __esm({
8443
8440
 
8444
8441
  // src/turn-control-tools.ts
8445
8442
  import { randomUUID as randomUUID2 } from "crypto";
8446
- import { z as z17 } from "zod";
8443
+ import { z as z18 } from "zod";
8447
8444
  function createSkipState() {
8448
8445
  return { skipped: false, reason: null };
8449
8446
  }
@@ -8494,7 +8491,7 @@ function createTurnControlMcpServer(acts, skipState) {
8494
8491
  SKIP_TURN_TOOL,
8495
8492
  TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
8496
8493
  {
8497
- reason: z17.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
8494
+ reason: z18.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
8498
8495
  },
8499
8496
  async (args) => {
8500
8497
  skipState.skipped = true;
@@ -8510,22 +8507,22 @@ function createTurnControlMcpServer(acts, skipState) {
8510
8507
  ASK_TOOL,
8511
8508
  TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
8512
8509
  {
8513
- targetUserId: z17.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
8514
- question: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
8515
- headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
8516
- options: z17.array(z17.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
8517
- questions: z17.array(
8518
- z17.object({
8519
- headline: z17.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
8520
- body: z17.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
8521
- options: z17.array(
8522
- z17.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)
8510
+ targetUserId: z18.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
8511
+ question: z18.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
8512
+ headline: z18.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
8513
+ options: z18.array(z18.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
8514
+ questions: z18.array(
8515
+ z18.object({
8516
+ headline: z18.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
8517
+ body: z18.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
8518
+ options: z18.array(
8519
+ z18.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)
8523
8520
  ).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
8524
8521
  })
8525
8522
  ).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
8526
- replaces: z17.object({
8527
- askId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
8528
- reason: z17.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
8523
+ replaces: z18.object({
8524
+ askId: z18.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
8525
+ reason: z18.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
8529
8526
  }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
8530
8527
  },
8531
8528
  async (args) => {
@@ -8566,9 +8563,9 @@ function createTurnControlMcpServer(acts, skipState) {
8566
8563
  WAKE_ME_TOOL,
8567
8564
  TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
8568
8565
  {
8569
- afterSeconds: z17.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
8570
- at: z17.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
8571
- note: z17.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
8566
+ afterSeconds: z18.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
8567
+ at: z18.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
8568
+ note: z18.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
8572
8569
  },
8573
8570
  async (args) => {
8574
8571
  const hasAfter = args.afterSeconds !== void 0;
@@ -8602,8 +8599,8 @@ function createTurnControlMcpServer(acts, skipState) {
8602
8599
  WITHDRAW_ASK_TOOL,
8603
8600
  TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
8604
8601
  {
8605
- askId: z17.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
8606
- reason: z17.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
8602
+ askId: z18.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
8603
+ reason: z18.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
8607
8604
  },
8608
8605
  async (args) => performAct(acts, {
8609
8606
  kind: "withdraw_ask",
@@ -10699,7 +10696,7 @@ function connectTarget(target) {
10699
10696
  }
10700
10697
  function createEgressProxy(opts) {
10701
10698
  const { allow, log: log2 } = opts;
10702
- const refuse2 = (host) => `cabane: ${host} is not on this device's egress allow-list (runners.docker.egressAllow)
10699
+ const refuse = (host) => `cabane: ${host} is not on this device's egress allow-list (runners.docker.egressAllow)
10703
10700
  `;
10704
10701
  const server = createServer2((req, res) => {
10705
10702
  req.on("error", () => res.destroy());
@@ -10713,7 +10710,7 @@ function createEgressProxy(opts) {
10713
10710
  }
10714
10711
  if (url.protocol !== "http:" || !egressAllowed(url.hostname, allow, url.port ? Number(url.port) : 80, 80)) {
10715
10712
  log2?.warn({ host: url.hostname }, "egress: refused");
10716
- res.writeHead(403, { "content-type": "text/plain" }).end(refuse2(url.hostname));
10713
+ res.writeHead(403, { "content-type": "text/plain" }).end(refuse(url.hostname));
10717
10714
  return;
10718
10715
  }
10719
10716
  const upstream = httpRequest(
@@ -10742,7 +10739,7 @@ function createEgressProxy(opts) {
10742
10739
  if (!target || !egressAllowed(target.host, allow, target.port)) {
10743
10740
  const host = target?.host ?? String(req.url);
10744
10741
  log2?.warn({ host }, "egress: refused");
10745
- const body = refuse2(host);
10742
+ const body = refuse(host);
10746
10743
  socket.end(
10747
10744
  `HTTP/1.1 403 Forbidden\r
10748
10745
  content-type: text/plain\r
@@ -12121,6 +12118,30 @@ import { homedir as homedir6 } from "os";
12121
12118
  import { join as join7 } from "path";
12122
12119
 
12123
12120
  // src/device-state.ts
12121
+ var DEVICE_STATES = ["waiting", "online", "offline", "deactivated"];
12122
+ var DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS = 9e4;
12123
+ var DEFAULT_DEVICE_RECONNECT_GRACE_MS = 9e4;
12124
+ var HARNESS_STATES = [
12125
+ "checking",
12126
+ "connected",
12127
+ "needs_sign_in",
12128
+ "limited",
12129
+ "problem",
12130
+ "off",
12131
+ "gone"
12132
+ ];
12133
+ var DEVICE_HEALTH_STATES = ["checking", "ready", "degraded"];
12134
+ var DEVICE_STATE_LABEL = {
12135
+ waiting: "Waiting for its first check-in",
12136
+ online: "Online",
12137
+ offline: "Offline",
12138
+ deactivated: "Deactivated"
12139
+ };
12140
+ var DEVICE_HEALTH_LABEL = {
12141
+ checking: "Checking harnesses\u2026",
12142
+ ready: "Ready",
12143
+ degraded: "Needs attention"
12144
+ };
12124
12145
  var HARNESS_STATE_LABEL = {
12125
12146
  checking: "checking",
12126
12147
  connected: "connected",
@@ -12250,7 +12271,7 @@ var AGENT_CHOICES = [
12250
12271
  { value: "opencode", label: "OpenCode" }
12251
12272
  ];
12252
12273
  var CHOOSE_AGENT = "Cabane needs one of them. Which will you set up?";
12253
- var SETUP_AGAIN = `Then run ${cabane("setup")} again. It picks up where you left off.`;
12274
+ var SETUP_AGAIN = `Then ${cabane("status")} shows it signed in; nothing else to run.`;
12254
12275
  function agentNote(runtime, c = {}) {
12255
12276
  const k = ctx(c);
12256
12277
  if (runtime === "claude-code") {
@@ -12312,27 +12333,10 @@ function agentNote(runtime, c = {}) {
12312
12333
  ...here.map(indent),
12313
12334
  indent("opencode serve --port 4096"),
12314
12335
  "",
12315
- `Then come back here and run ${cabane("setup")} again.`
12336
+ `Then ${cabane("status")} shows it signed in; nothing else to run.`
12316
12337
  ]
12317
12338
  };
12318
12339
  }
12319
- function nothingUsableLines(detection) {
12320
- const lines2 = [];
12321
- for (const runtime of ["claude-code", "codex"]) {
12322
- const state = detection[runtime];
12323
- const label = HARNESS_LABELS[runtime];
12324
- if (state.state === "signed_out") {
12325
- lines2.push(
12326
- `${label} isn't signed in on this device. Run ${loginCommand(runtime)} to use it.`
12327
- );
12328
- } else if (state.state === "problem") {
12329
- const reason = unusableReason(runtime, state.check);
12330
- if (reason) lines2.push(reason);
12331
- }
12332
- }
12333
- lines2.push(`Cabane can run while you sign in to a harness.`);
12334
- return lines2;
12335
- }
12336
12340
  function indent(line) {
12337
12341
  return ` ${line}`;
12338
12342
  }
@@ -12953,6 +12957,8 @@ async function claimSetupCode(baseUrl, input) {
12953
12957
  baseUrl: body.baseUrl,
12954
12958
  deviceToken: token,
12955
12959
  deviceId: body.deviceId,
12960
+ ..."workspace" in body && body.workspace && typeof body.workspace === "object" && "name" in body.workspace && typeof body.workspace.name === "string" && "slug" in body.workspace && typeof body.workspace.slug === "string" ? { workspace: { name: body.workspace.name, slug: body.workspace.slug } } : {},
12961
+ ..."devicesUrl" in body && typeof body.devicesUrl === "string" ? { devicesUrl: body.devicesUrl } : {},
12956
12962
  ..."deviceLabel" in body && typeof body.deviceLabel === "string" ? { deviceLabel: body.deviceLabel } : {}
12957
12963
  };
12958
12964
  }
@@ -12961,9 +12967,6 @@ function deviceLabelFromHostname(hostname4) {
12961
12967
  return trimmed.length ? trimmed.slice(0, 120) : void 0;
12962
12968
  }
12963
12969
 
12964
- // src/commands/setup.ts
12965
- init_errors();
12966
-
12967
12970
  // src/commands/start.ts
12968
12971
  init_cabane_command();
12969
12972
  init_config();
@@ -13482,6 +13485,27 @@ function warnAboutHarnessReadiness(cfg, deps = {}) {
13482
13485
  }
13483
13486
  }
13484
13487
 
13488
+ // src/supervisor.ts
13489
+ init_errors();
13490
+
13491
+ // src/heartbeat-status.ts
13492
+ import { z as z3 } from "zod";
13493
+ var acknowledgedDeviceSchema = z3.object({
13494
+ state: z3.enum(DEVICE_STATES),
13495
+ health: z3.enum(DEVICE_HEALTH_STATES).nullable(),
13496
+ harnesses: z3.array(z3.object({ runtime: z3.string(), state: z3.enum(HARNESS_STATES) }))
13497
+ });
13498
+ var heartbeatStatusSchema = z3.object({
13499
+ lastHeartbeatAckAt: z3.string().datetime().nullable().default(null),
13500
+ lastAcknowledgedDevice: acknowledgedDeviceSchema.nullable().default(null),
13501
+ lastHeartbeatRefused: z3.boolean().default(false)
13502
+ });
13503
+ var emptyHeartbeatStatus = () => ({
13504
+ lastHeartbeatAckAt: null,
13505
+ lastAcknowledgedDevice: null,
13506
+ lastHeartbeatRefused: false
13507
+ });
13508
+
13485
13509
  // src/supervisor.ts
13486
13510
  init_cabane_command();
13487
13511
  import { hostname as hostname2 } from "os";
@@ -13662,8 +13686,8 @@ import {
13662
13686
  writeFileSync as writeFileSync3
13663
13687
  } from "fs";
13664
13688
  import { join as join12 } from "path";
13665
- import { z as z4 } from "zod";
13666
- var credentialMapSchema = z4.record(z4.string(), z4.string());
13689
+ import { z as z5 } from "zod";
13690
+ var credentialMapSchema = z5.record(z5.string(), z5.string());
13667
13691
  function credentialsDir() {
13668
13692
  return join12(cabaneDir(), "credentials");
13669
13693
  }
@@ -15652,27 +15676,27 @@ function compareVersions(a, b) {
15652
15676
  init_config();
15653
15677
  import { mkdirSync as mkdirSync15, readFileSync as readFileSync15, renameSync as renameSync10, writeFileSync as writeFileSync10 } from "fs";
15654
15678
  import { dirname as dirname12, join as join28 } from "path";
15655
- import { z as z18 } from "zod";
15656
- var resultSchema = z18.object({
15657
- kind: z18.enum(["updated", "rolled_back", "failed"]),
15658
- from: z18.string(),
15659
- to: z18.string(),
15660
- at: z18.string(),
15661
- reason: z18.string().optional()
15679
+ import { z as z19 } from "zod";
15680
+ var resultSchema = z19.object({
15681
+ kind: z19.enum(["updated", "rolled_back", "failed"]),
15682
+ from: z19.string(),
15683
+ to: z19.string(),
15684
+ at: z19.string(),
15685
+ reason: z19.string().optional()
15662
15686
  });
15663
- var pendingSchema = z18.object({
15664
- from: z18.string(),
15687
+ var pendingSchema = z19.object({
15688
+ from: z19.string(),
15665
15689
  // The package root `current` pointed at before the swap, so a rollback can point it back.
15666
- fromRoot: z18.string(),
15667
- to: z18.string(),
15668
- at: z18.string(),
15669
- boots: z18.number().int().nonnegative()
15690
+ fromRoot: z19.string(),
15691
+ to: z19.string(),
15692
+ at: z19.string(),
15693
+ boots: z19.number().int().nonnegative()
15670
15694
  });
15671
- var stateSchema = z18.object({
15672
- server: z18.object({ latest: z18.string(), minimum: z18.string(), at: z18.string() }).optional(),
15695
+ var stateSchema = z19.object({
15696
+ server: z19.object({ latest: z19.string(), minimum: z19.string(), at: z19.string() }).optional(),
15673
15697
  pending: pendingSchema.optional(),
15674
15698
  lastResult: resultSchema.optional(),
15675
- failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
15699
+ failed: z19.object({ version: z19.string(), attempts: z19.number().int(), at: z19.string() }).optional()
15676
15700
  });
15677
15701
  function updateStatePath(dir2 = cabaneDir()) {
15678
15702
  return join28(dir2, "update.json");
@@ -16157,6 +16181,13 @@ var CompanionSupervisor = class {
16157
16181
  recoveryChecked = false;
16158
16182
  recovering = false;
16159
16183
  heartbeated = false;
16184
+ heartbeatStatus = emptyHeartbeatStatus();
16185
+ currentDetection() {
16186
+ return this.detection;
16187
+ }
16188
+ lastHeartbeat() {
16189
+ return { ...this.heartbeatStatus };
16190
+ }
16160
16191
  recoveryCursor;
16161
16192
  recoveredTurns = /* @__PURE__ */ new Set();
16162
16193
  // Agents assigned here, as `workspaceId|agentId`, whose sign-in to Cabane is missing and
@@ -16276,19 +16307,26 @@ var CompanionSupervisor = class {
16276
16307
  else this.log.error({ posture }, `runners: ${summary}`);
16277
16308
  void this.companionStores().refresh();
16278
16309
  }
16279
- const startupHarnessRefresh = this.detect().then(() => {
16310
+ if (this.config.deviceToken) {
16311
+ this.deviceApi = new DeviceApi({
16312
+ baseUrl: this.config.baseUrl,
16313
+ deviceToken: this.config.deviceToken
16314
+ });
16315
+ this.trackHeartbeat(this.sendHeartbeat(true));
16316
+ }
16317
+ const firstHeartbeat = this.inFlightHeartbeat;
16318
+ const startupHarnessRefresh = this.detect();
16319
+ void startupHarnessRefresh.then(async () => {
16320
+ await firstHeartbeat;
16321
+ if (this.stopped) return;
16322
+ await this.heartbeatNow();
16280
16323
  });
16281
- this.trackHeartbeat(startupHarnessRefresh);
16282
16324
  this.detectTimer = setInterval(() => void this.detect(), DETECT_INTERVAL_MS);
16283
16325
  this.detectTimer.unref?.();
16284
- if (!this.config.deviceToken) {
16285
- this.log.warn("This device isn't paired. Run cabane start to pair it.");
16326
+ if (!this.deviceApi || !this.config.deviceToken) {
16327
+ this.log.warn("This device isn't paired. Get a setup line from Settings \u2192 Devices.");
16286
16328
  return;
16287
16329
  }
16288
- this.deviceApi = new DeviceApi({
16289
- baseUrl: this.config.baseUrl,
16290
- deviceToken: this.config.deviceToken
16291
- });
16292
16330
  this.deviceSub = new SseSubscriber({
16293
16331
  baseUrl: this.config.baseUrl,
16294
16332
  workspaceId: "device-control",
@@ -16334,11 +16372,6 @@ var CompanionSupervisor = class {
16334
16372
  this.updater.start();
16335
16373
  await this.refreshAssignments();
16336
16374
  this.deviceSub.start();
16337
- const firstHeartbeat = startupHarnessRefresh.then(async () => {
16338
- if (this.stopped) return;
16339
- await this.sendHeartbeat();
16340
- });
16341
- this.trackHeartbeat(firstHeartbeat);
16342
16375
  this.heartbeatTimer = setInterval(() => this.kickHeartbeat(), HEARTBEAT_INTERVAL_MS);
16343
16376
  this.heartbeatTimer.unref?.();
16344
16377
  this.pollTimer = setInterval(() => void this.refreshAssignments(), ASSIGNMENTS_POLL_MS);
@@ -16359,15 +16392,16 @@ var CompanionSupervisor = class {
16359
16392
  if (this.inFlightHeartbeat === pending) this.inFlightHeartbeat = null;
16360
16393
  });
16361
16394
  }
16362
- async sendHeartbeat() {
16395
+ async sendHeartbeat(first = false) {
16363
16396
  if (!this.deviceApi) return;
16364
16397
  try {
16365
- if (!this.detection || advertisedRuntimes(this.detection).length === 0) await this.detect();
16398
+ if (!first && this.detection && advertisedRuntimes(this.detection).length === 0)
16399
+ await this.detect();
16366
16400
  const store = loadSecretStoreTolerant(
16367
16401
  (m) => this.log.warn({ err: m }, "Could not read the secrets file. Check ~/.cabane/secrets.json.")
16368
16402
  );
16369
16403
  const connectorReports = this.connectorReports();
16370
- const opencodeUrl = this.opencodeUrl();
16404
+ const opencodeUrl = first ? null : this.opencodeUrl();
16371
16405
  const opencodeModels = opencodeUrl ? await enumerateOpencodeModels(opencodeUrl) : null;
16372
16406
  const res = await this.deviceApi.heartbeat({
16373
16407
  bootId,
@@ -16376,31 +16410,39 @@ var CompanionSupervisor = class {
16376
16410
  // Each runtime only when this device can actually run it: what the last
16377
16411
  // detection found usable (signed in, or an API key, or a serve
16378
16412
  // answering) — a usable bundled binary is a present one.
16379
- manifest: buildCompanionManifest({
16380
- claudeCode: this.offered("claude-code"),
16381
- opencode: this.offered("opencode"),
16382
- codex: this.offered("codex"),
16383
- // WHY an allowed harness is absent above. Diagnostic only, and always
16384
- // sent, so an empty array clears a previously reported issue.
16385
- runtimeIssues: this.runtimeIssues(),
16386
- // Informational only; the server matches on name.
16387
- versions: this.harnessVersions,
16388
- // Shown on the device page.
16389
- ...runnersEnabled(this.config) ? { runner: this.runnerManifest() } : {}
16390
- }),
16413
+ ...!first && this.detection ? {
16414
+ manifest: buildCompanionManifest({
16415
+ claudeCode: this.offered("claude-code"),
16416
+ opencode: this.offered("opencode"),
16417
+ codex: this.offered("codex"),
16418
+ // WHY an allowed harness is absent above. Diagnostic only, and always
16419
+ // sent, so an empty array clears a previously reported issue.
16420
+ runtimeIssues: this.runtimeIssues(),
16421
+ // Informational only; the server matches on name.
16422
+ versions: this.harnessVersions,
16423
+ // Shown on the device page.
16424
+ ...runnersEnabled(this.config) ? { runner: this.runnerManifest() } : {}
16425
+ })
16426
+ } : {},
16391
16427
  // Omitted until the first observed failure or heal, so the server's
16392
16428
  // status-less manifest synthesis still runs for a fresh companion.
16393
- ...connectorReports.length > 0 ? { connectors: connectorReports } : {},
16429
+ ...!first && connectorReports.length > 0 ? { connectors: connectorReports } : {},
16394
16430
  // The complete harness inventory in the machine's words, once detection
16395
16431
  // has run; omitted before it, which the server reads as "nothing yet".
16396
- ...this.detection ? { harnesses: harnessEntriesFromDetection(this.detection) } : {},
16432
+ ...!first && this.detection ? { harnesses: harnessEntriesFromDetection(this.detection) } : {},
16397
16433
  ...opencodeModels !== null ? { models: opencodeModels } : {},
16398
- ...this.updater ? { update: this.updater.report() } : {}
16434
+ ...!first && this.updater ? { update: this.updater.report() } : {}
16399
16435
  });
16400
16436
  this.hub.setDevice({ deviceId: res.deviceId });
16401
16437
  this.deviceId = res.deviceId;
16402
16438
  this.checkVersionSkew(res.serverVersion);
16403
16439
  this.heartbeated = true;
16440
+ const view = acknowledgedDeviceSchema.safeParse(res.device);
16441
+ this.heartbeatStatus = {
16442
+ lastHeartbeatAckAt: (/* @__PURE__ */ new Date()).toISOString(),
16443
+ lastAcknowledgedDevice: view.success ? view.data : null,
16444
+ lastHeartbeatRefused: false
16445
+ };
16404
16446
  void this.recoverRuns();
16405
16447
  void this.updater?.onHeartbeat(res.companion, this.deviceConnected).catch(
16406
16448
  (err) => this.log.warn(
@@ -16409,9 +16451,10 @@ var CompanionSupervisor = class {
16409
16451
  )
16410
16452
  );
16411
16453
  } catch (err) {
16454
+ this.heartbeatStatus.lastHeartbeatRefused = err instanceof ApiError && (err.status === 401 || err.status === 403);
16412
16455
  this.log.warn(
16413
16456
  { ...apiErrorLogFields(err) },
16414
- "Couldn't check in with Cabane; trying again shortly."
16457
+ "Couldn't check in with the server; trying again shortly."
16415
16458
  );
16416
16459
  }
16417
16460
  }
@@ -17498,7 +17541,8 @@ var CompanionSupervisor = class {
17498
17541
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
17499
17542
  if (this.detectTimer) clearInterval(this.detectTimer);
17500
17543
  if (this.pollTimer) clearInterval(this.pollTimer);
17501
- const heartbeat = this.inFlightHeartbeat;
17544
+ const heartbeat = Promise.all([this.inFlightHeartbeat, this.detecting]).then(() => {
17545
+ });
17502
17546
  this.deviceSub?.stop();
17503
17547
  for (const wr of this.workspaces.values()) {
17504
17548
  for (const a of wr.agents.values()) a.cancelDrain();
@@ -17781,7 +17825,11 @@ async function createCompanionRuntime(opts = {}) {
17781
17825
  let dashboard = null;
17782
17826
  try {
17783
17827
  control = await startControlServer({
17784
- status: () => hub.statusJson(),
17828
+ status: () => ({
17829
+ ...hub.statusJson(),
17830
+ ...supervisor.lastHeartbeat(),
17831
+ detection: supervisor.currentDetection()
17832
+ }),
17785
17833
  detect: () => supervisor.redetect(),
17786
17834
  allow: (runtime) => supervisor.allowHarness(runtime),
17787
17835
  update: (auto) => supervisor.update(auto),
@@ -18274,7 +18322,7 @@ async function startDaemon(opts = {}, deps = {}) {
18274
18322
  const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
18275
18323
  const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18276
18324
  const now = deps.now ?? (() => Date.now());
18277
- const quiet = opts.report === "failures";
18325
+ const quiet = opts.report === "failures" || opts.report === "silent";
18278
18326
  const cfg = (deps.requireCfg ?? requireConfig)();
18279
18327
  ensureRuntime(cfg, {
18280
18328
  // The terminal already said what it found, in its own words. Saying it
@@ -18286,7 +18334,8 @@ async function startDaemon(opts = {}, deps = {}) {
18286
18334
  const existing = readState();
18287
18335
  if (existing) {
18288
18336
  if (await verify(existing) !== "stale") {
18289
- process.stdout.write(alreadyRunningLines(existing.pid).join("\n") + "\n");
18337
+ if (opts.report !== "silent")
18338
+ process.stdout.write(alreadyRunningLines(existing.pid).join("\n") + "\n");
18290
18339
  return { started: false, state: existing };
18291
18340
  }
18292
18341
  clearRuntimeState();
@@ -18315,13 +18364,15 @@ async function startDaemon(opts = {}, deps = {}) {
18315
18364
  const child = launchDetached();
18316
18365
  const state = await waitForReady();
18317
18366
  if (!state) {
18318
- process.stdout.write(
18319
- `Cabane Companion was launched (pid ${child?.pid ?? "?"}) but didn't report ready within ${STARTUP_TIMEOUT_MS / 1e3}s.
18367
+ const error = `The background process (pid ${child?.pid ?? "?"}) did not report ready within ${STARTUP_TIMEOUT_MS / 1e3}s.`;
18368
+ if (opts.report !== "silent")
18369
+ process.stdout.write(
18370
+ `Cabane Companion was launched (pid ${child?.pid ?? "?"}) but didn't report ready within ${STARTUP_TIMEOUT_MS / 1e3}s.
18320
18371
  Check ${companionLogPath()} for errors, or run \`${cabane("status")}\`.
18321
18372
  `
18322
- );
18373
+ );
18323
18374
  process.exitCode = 1;
18324
- return { started: false, state: null };
18375
+ return { started: false, state: null, error };
18325
18376
  }
18326
18377
  if (quiet) return { started: true, state };
18327
18378
  process.stdout.write(
@@ -18351,120 +18402,210 @@ function defaultSpawnDetached(args) {
18351
18402
  }
18352
18403
  }
18353
18404
 
18405
+ // src/terminal-copy.ts
18406
+ init_cabane_command();
18407
+ var UNPAIRED = "This device isn't paired yet. Get a setup line from a workspace's Settings \u2192 Devices and paste it here.";
18408
+ var lease = DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS + DEFAULT_DEVICE_RECONNECT_GRACE_MS;
18409
+ function age(at, now) {
18410
+ const seconds = Math.max(0, Math.floor((now - Date.parse(at)) / 1e3));
18411
+ return seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m` : `${Math.floor(seconds / 3600)}h`;
18412
+ }
18413
+ function deviceLine(f, now = Date.now()) {
18414
+ if (!f.running) return `not running \u2014 start it with ${cabane("start")}`;
18415
+ if (f.lastHeartbeatRefused)
18416
+ return `refused by the server \u2014 it may be deactivated; see ${f.devicesUrl}`;
18417
+ if (!f.lastHeartbeatAckAt) return "starting \u2014 hasn't checked in yet";
18418
+ const since = age(f.lastHeartbeatAckAt, now);
18419
+ if (now - Date.parse(f.lastHeartbeatAckAt) > lease)
18420
+ return `can't reach the server \u2014 last checked in ${since} ago`;
18421
+ const view = f.lastAcknowledgedDevice;
18422
+ if (!view) return `checked in ${since} ago`;
18423
+ const word = view.state === "online" && view.health ? view.health === "checking" ? "online, checking harnesses\u2026" : DEVICE_HEALTH_LABEL[view.health] : DEVICE_STATE_LABEL[view.state];
18424
+ return `${word.toLowerCase()} \u2014 checked in ${since} ago`;
18425
+ }
18426
+ function pairingLine(pair) {
18427
+ return `\u2713 Paired as "${pair.deviceLabel ?? "Computer"}" with ${pair.workspace?.name ?? "the workspace"}.`;
18428
+ }
18429
+ function pathLines(reload) {
18430
+ return reload ? [` To use cabane in this window: ${reload}`, ""] : [];
18431
+ }
18432
+ function agentLines(detection, opts) {
18433
+ const rows = agentRows(detection, opts);
18434
+ const names = rows.map((row3) => row3.usable ? `\u2713 ${row3.label}` : row3.label);
18435
+ const width = Math.max(...names.map((name) => name.length)) + 3;
18436
+ return rows.map((row3, i) => `${names[i].padEnd(width)}${row3.text}`);
18437
+ }
18438
+ var modeLines = {
18439
+ background: "Cabane is running in the background",
18440
+ service: "Cabane is running as a service",
18441
+ attached: "Cabane is running in this process",
18442
+ existing: "Cabane was already running"
18443
+ };
18444
+ function closingBlock(input, now = Date.now()) {
18445
+ const { mode, facts } = input;
18446
+ const device = deviceLine(facts, now);
18447
+ const noAck = !facts.lastHeartbeatAckAt && !facts.lastHeartbeatRefused;
18448
+ const stale = facts.lastHeartbeatAckAt !== null && now - Date.parse(facts.lastHeartbeatAckAt) > lease;
18449
+ const modeLine = modeLines[mode];
18450
+ const sentence = noAck ? `${mode === "background" ? "Cabane started in the background" : modeLine} but hasn't checked in yet.` : `${modeLine}. ${facts.lastHeartbeatRefused || stale ? device[0].toUpperCase() + device.slice(1) : `This device ${device.startsWith("checked in") || device.startsWith("needs attention") ? "" : "is "}${device}`}.`;
18451
+ const lines2 = [` ${sentence}`, "", ...pathLines(input.reload)];
18452
+ if (noAck || stale || facts.lastHeartbeatRefused) {
18453
+ lines2.push(
18454
+ ` See what it's doing: ${cabane("status")}${facts.lastHeartbeatRefused ? "" : ` Log: ${cabane("logs")}`}`
18455
+ );
18456
+ } else {
18457
+ lines2.push(" Harnesses on this device");
18458
+ lines2.push(
18459
+ ...(input.detection ? agentLines(input.detection, { signInHint: true }) : ["Claude Code checking", "Codex checking", "OpenCode checking"]).map((line) => ` ${line}`)
18460
+ );
18461
+ lines2.push(
18462
+ "",
18463
+ ` Status ${cabane("status")}${mode === "attached" ? "" : ` Stop ${cabane("stop")}`} Log ${cabane("logs")}`
18464
+ );
18465
+ if (!input.service && input.servicePlatform) {
18466
+ lines2.push(
18467
+ ` Start at login and keep Cabane updating itself: ${cabane("service install")}${input.servicePlatform === "launchd" ? " (macOS will ask you to allow a background item)" : ""}`
18468
+ );
18469
+ lines2.push(
18470
+ ` Updates itself only as a service; to turn that off: ${cabane("update --auto off")}`
18471
+ );
18472
+ } else if (input.service && input.autoUpdate)
18473
+ lines2.push(` Updating itself; to turn that off: ${cabane("update --auto off")}`);
18474
+ }
18475
+ if (input.address) lines2.push("", ` In Cabane: ${input.address}`);
18476
+ if (mode === "attached") lines2.push("", " Listening for messages\u2026");
18477
+ return lines2;
18478
+ }
18479
+ function startFailure(message2, reload) {
18480
+ return [
18481
+ ...pathLines(reload),
18482
+ `\u2717 Cabane couldn't start: ${message2}`,
18483
+ "",
18484
+ ` Log: ${cabane("logs")} Try again: ${cabane("start")}`
18485
+ ];
18486
+ }
18487
+
18488
+ // src/terminal-status.ts
18489
+ async function readTerminalStatus(socket, timeoutMs = 1e3) {
18490
+ if (!socket) return emptyHeartbeatStatus();
18491
+ try {
18492
+ const body = await controlRequest(socket, { cmd: "status" }, timeoutMs);
18493
+ const facts = heartbeatStatusSchema.safeParse(body);
18494
+ return facts.success ? { ...facts.data, detection: body.detection } : emptyHeartbeatStatus();
18495
+ } catch {
18496
+ return emptyHeartbeatStatus();
18497
+ }
18498
+ }
18499
+ async function waitForAcknowledgement(socket) {
18500
+ const deadline = Date.now() + 1e4;
18501
+ let status2 = emptyHeartbeatStatus();
18502
+ while (Date.now() < deadline) {
18503
+ status2 = await readTerminalStatus(socket, Math.min(1e3, deadline - Date.now()));
18504
+ if (status2.lastHeartbeatAckAt || status2.lastHeartbeatRefused) return status2;
18505
+ await new Promise(
18506
+ (resolve2) => setTimeout(resolve2, Math.min(100, Math.max(0, deadline - Date.now())))
18507
+ );
18508
+ }
18509
+ return status2;
18510
+ }
18511
+
18354
18512
  // src/commands/start.ts
18355
- async function start(opts = {}) {
18513
+ async function start(opts = {}, deps = {}) {
18356
18514
  setLogOverrides(opts);
18357
- const interactive = isInteractive();
18358
18515
  setConsoleLogging(false);
18359
18516
  try {
18360
- await startScript(opts, interactive);
18517
+ return await startScript(opts, deps);
18518
+ } catch (err) {
18519
+ for (const line of startFailure(err instanceof Error ? err.message : String(err), reloadLine()))
18520
+ write(line);
18521
+ process.exitCode = 1;
18522
+ return null;
18361
18523
  } finally {
18362
18524
  setConsoleLogging(true);
18363
18525
  }
18364
18526
  }
18365
- async function startScript(opts, interactive) {
18366
- const running = await liveCompanionState();
18367
- if (running) {
18368
- for (const line of alreadyRunningLines(running.pid)) write(`${INDENT}${line}`);
18369
- return;
18370
- }
18371
- const daemon = process.env.CABANE_COMPANION_DAEMON === "1";
18372
- if (opts.foreground || daemon) {
18373
- await startSupervised(opts, interactive, daemon);
18374
- return;
18375
- }
18527
+ async function startScript(opts, deps) {
18528
+ const interactive = deps.interactive ?? isInteractive();
18376
18529
  if (!isDevicePaired()) {
18377
- refuse([
18378
- `This device isn't set up yet. Run ${cabane("setup")} to sign in to a harness and pair it.`
18379
- ]);
18380
- return;
18530
+ for (const line of [...pathLines(reloadLine()), UNPAIRED]) write(line);
18531
+ process.exitCode = 1;
18532
+ return null;
18381
18533
  }
18382
18534
  const cfg = requireConfig();
18383
- const detection = await detectHarnesses(cfg);
18384
- if (!anyUsable(detection)) {
18385
- for (const line of nothingUsableLines(detection)) write(`${INDENT}${line}`);
18386
- }
18387
- if (serviceInstalled()) {
18388
- await startTheService();
18389
- return;
18535
+ let running = await (deps.live ?? liveCompanionState)();
18536
+ const installed = serviceInstalled();
18537
+ const daemon = process.env.CABANE_COMPANION_DAEMON === "1";
18538
+ let mode = "existing";
18539
+ let runtime = null;
18540
+ if (!running) {
18541
+ write("Starting Cabane\u2026");
18542
+ if (opts.foreground || daemon || !interactive && !installed) {
18543
+ const result = await createCompanionRuntime({
18544
+ onReadinessWarning: (message2) => getLogger().warn(message2)
18545
+ });
18546
+ if (!result.ok) {
18547
+ if (result.reason === "stray-companion")
18548
+ throw new Error(strayCompanionLines(result.strays, cabane("start")).join(" "));
18549
+ running = result.existing;
18550
+ if (!running) throw new Error("Another start owns this device but has not reported ready.");
18551
+ } else {
18552
+ runtime = result.runtime;
18553
+ mode = "attached";
18554
+ }
18555
+ } else if (installed) {
18556
+ const result = await startService();
18557
+ if (!result.ok) throw new Error(result.lines.join(" "));
18558
+ const deadline = Date.now() + 8e3;
18559
+ while (!running?.socket && Date.now() < deadline) {
18560
+ running = await (deps.live ?? liveCompanionState)();
18561
+ if (!running?.socket) await new Promise((resolve2) => setTimeout(resolve2, 150));
18562
+ }
18563
+ if (!running?.socket) throw new Error("The service did not report ready within 8s.");
18564
+ mode = "service";
18565
+ } else {
18566
+ const result = await startDaemon({ ...opts, report: "silent" });
18567
+ if (!result.state)
18568
+ throw new Error(result.error ?? "The background process did not report ready.");
18569
+ running = result.state;
18570
+ mode = result.started ? "background" : "existing";
18571
+ }
18390
18572
  }
18391
- if (!interactive) {
18392
- const runtime = await bringUp();
18393
- if (!runtime) return;
18394
- blank();
18395
- write(`${INDENT}Cabane is running. No terminal attached, so it stays in the foreground.`);
18396
- write(`${INDENT}Listening for messages\u2026`);
18573
+ const socket = runtime?.socketPath ?? running?.socket;
18574
+ let status2 = await (deps.waitAck ?? waitForAcknowledgement)(socket);
18575
+ let detection = status2.detection ?? null;
18576
+ if (!runtime && status2.lastHeartbeatAckAt && !status2.lastHeartbeatRefused) {
18577
+ write("Checking harnesses\u2026");
18578
+ try {
18579
+ detection = socket ? await (deps.detectRunning ?? detectOverSocket)(socket) : await (deps.detect ?? detectHarnesses)(cfg);
18580
+ } catch {
18581
+ detection = await (deps.detect ?? detectHarnesses)(cfg);
18582
+ }
18583
+ const latest = await readTerminalStatus(socket);
18584
+ if (latest.lastHeartbeatAckAt || latest.lastHeartbeatRefused) status2 = latest;
18585
+ }
18586
+ const personalUrl = `${cfg.baseUrl.replace(/\/$/, "")}/settings/devices`;
18587
+ const address = opts.pairing ? opts.pairing.devicesUrl : personalUrl;
18588
+ if (!daemon)
18589
+ for (const line of closingBlock({
18590
+ mode,
18591
+ facts: { ...status2, running: true, devicesUrl: address ?? personalUrl },
18592
+ detection,
18593
+ reload: reloadLine(),
18594
+ service: installed,
18595
+ servicePlatform: servicePlatform(),
18596
+ autoUpdate: cfg.update?.auto !== false,
18597
+ ...address ? { address } : {}
18598
+ }))
18599
+ write(line);
18600
+ if (runtime) {
18397
18601
  setConsoleLogging(true);
18398
18602
  await runAttached(runtime);
18399
- return;
18400
- }
18401
- const { started } = await startDaemon({
18402
- report: "failures",
18403
- ...opts.logLevel ? { logLevel: opts.logLevel } : {},
18404
- ...opts.logFormat ? { logFormat: opts.logFormat } : {}
18405
- });
18406
- if (!started) return;
18407
- write(INDENT + tick("Cabane is running in the background."));
18408
- write(` Stop: ${cabane("stop")} Logs: ${cabane("logs")}`);
18409
- }
18410
- function refuse(lines2) {
18411
- const reload = reloadLine();
18412
- if (reload) write(`${INDENT}Run ${reload} first.`);
18413
- for (const line of lines2) write(`${INDENT}${line}`);
18414
- process.exitCode = 1;
18415
- }
18416
- async function startSupervised(opts, interactive, daemon) {
18417
- if (daemon) write(`Running in the background (pid ${process.pid}). Stop with ${cabane("stop")}.`);
18418
- else blank();
18419
- if (!isDevicePaired()) {
18420
- refuse(["Get a setup line from your workspace's Settings \u2192 Devices, then paste it here."]);
18421
- return;
18422
- }
18423
- const runtime = await bringUp();
18424
- if (!runtime) return;
18425
- await runtime.heartbeatNow();
18426
- if (interactive) {
18427
- blank();
18428
- write(INDENT + tick("Cabane is running in this terminal."));
18429
- write(
18430
- ` Stop: Ctrl-C Logs: ${cabane("logs")} (${tildePath(companionLogPath())})`
18431
- );
18432
- blank();
18433
- write(`${INDENT}Listening for messages\u2026`);
18434
- } else if (!daemon) {
18435
- blank();
18436
- write(`${INDENT}Cabane is running. No terminal attached, so it stays in the foreground.`);
18437
- write(`${INDENT}Listening for messages\u2026`);
18438
- }
18439
- setConsoleLogging(true);
18440
- await runAttached(runtime);
18441
- }
18442
- async function bringUp() {
18443
- const log2 = getLogger();
18444
- const result = await createCompanionRuntime({
18445
- // The terminal says what it found in its own words; a stderr warning in
18446
- // the middle of it would be the same news, worse.
18447
- onReadinessWarning: (message2) => log2.warn(message2)
18448
- });
18449
- if (!result.ok && result.reason === "stray-companion") {
18450
- refuse(strayCompanionLines(result.strays, cabane("start")));
18451
- return null;
18452
- }
18453
- if (!result.ok) {
18454
- for (const line of alreadyRunningLines(result.existing?.pid)) write(`${INDENT}${line}`);
18455
- return null;
18456
18603
  }
18457
- return result.runtime;
18604
+ return detection;
18458
18605
  }
18459
- async function startTheService() {
18460
- const outcome = await startService();
18461
- for (const line of outcome.lines) write(`${INDENT}${line}`);
18462
- if (!outcome.ok) {
18463
- process.exitCode = 1;
18464
- return;
18465
- }
18466
- write(INDENT + tick("Cabane runs as a service here, and is starting."));
18467
- write(` Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
18606
+ async function detectOverSocket(socket) {
18607
+ const body = await controlRequest(socket, { cmd: "detect" }, 15e3);
18608
+ return body.detection;
18468
18609
  }
18469
18610
  async function runAttached(runtime) {
18470
18611
  await new Promise((resolve2) => {
@@ -18573,348 +18714,79 @@ function plainUi() {
18573
18714
 
18574
18715
  // src/commands/setup.ts
18575
18716
  init_term();
18576
-
18577
- // src/commands/service.ts
18578
- init_cabane_command();
18579
- init_config();
18580
- init_term();
18581
- init_version2();
18582
-
18583
- // src/commands/stop.ts
18584
- var TERM_GRACE_MS = 6e3;
18585
- var POLL_INTERVAL_MS2 = 150;
18586
- async function stop(deps = {}) {
18587
- if ((deps.serviceInstalled ?? serviceInstalled)()) {
18588
- const outcome = await (deps.stopService ?? stopService)();
18589
- for (const line of outcome.lines) process.stdout.write(`${line}
18590
- `);
18591
- if (!outcome.ok) {
18592
- process.exitCode = 1;
18593
- return;
18594
- }
18595
- process.stdout.write(
18596
- "companion: stopped the service. It starts again at boot, or with `cabane start`.\n"
18717
+ async function setup(opts = {}, deps = {}) {
18718
+ setLogOverrides(opts);
18719
+ setConsoleLogging(false);
18720
+ try {
18721
+ await setupScript(opts, deps);
18722
+ } catch (err) {
18723
+ for (const line of pathLines(reloadLine())) write(line);
18724
+ write(err instanceof Error ? err.message : String(err));
18725
+ write(
18726
+ "Check the server connection, then take a fresh setup line from the workspace's Settings \u2192 Devices."
18597
18727
  );
18598
- return;
18599
- }
18600
- const readState = deps.readState ?? readLiveRuntimeState;
18601
- const verify = deps.verify ?? ((s) => verifyRuntime(s));
18602
- const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
18603
- const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18604
- const now = deps.now ?? (() => Date.now());
18605
- const state = readState();
18606
- if (!state) {
18607
- process.stdout.write("companion: not running (nothing to stop).\n");
18608
- return;
18728
+ process.exitCode = 1;
18729
+ } finally {
18730
+ setConsoleLogging(true);
18609
18731
  }
18610
- if (await verify(state) === "stale") {
18611
- clearRuntimeState();
18612
- process.stdout.write("companion: not running (stale marker swept).\n");
18732
+ }
18733
+ async function setupScript(opts, deps) {
18734
+ const interactive = deps.interactive ?? isInteractive();
18735
+ const paired = isDevicePaired();
18736
+ if (!opts.code && !paired) {
18737
+ write(UNPAIRED);
18738
+ process.exitCode = 1;
18613
18739
  return;
18614
18740
  }
18615
- const { pid } = state;
18616
- process.stdout.write(`companion: stopping (pid ${pid})\u2026
18617
- `);
18618
- try {
18619
- kill(pid, "SIGTERM");
18620
- } catch {
18621
- clearRuntimeState();
18622
- process.stdout.write("companion: already stopped.\n");
18741
+ const ui = deps.ui ?? (interactive ? terminalUi() : plainUi());
18742
+ const running = await (deps.live ?? liveCompanionState)();
18743
+ const cfg = safeLoadConfig();
18744
+ const strays = (deps.strays ?? findStrayCompanions)(running ? [running.pid] : []);
18745
+ if (strays.length) {
18746
+ for (const line of pathLines(reloadLine())) write(line);
18747
+ ui.cancel(strayCompanionLines(strays, cabane("setup")).join("\n"));
18748
+ process.exitCode = 1;
18623
18749
  return;
18624
18750
  }
18625
- const deadline = now() + TERM_GRACE_MS;
18626
- while (now() < deadline) {
18627
- if (!isAlive(kill, pid)) {
18628
- process.stdout.write("companion: stopped.\n");
18751
+ let device;
18752
+ if (opts.code) {
18753
+ const baseUrl = resolvePairBaseUrl(opts.server ?? cfg?.baseUrl);
18754
+ if (paired && cfg && new URL(cfg.baseUrl).origin !== new URL(baseUrl).origin) {
18755
+ for (const line of pathLines(reloadLine())) write(line);
18756
+ write(
18757
+ "This device is paired to another Cabane server. Log out before setting it up on a different server."
18758
+ );
18759
+ write(`Run ${cabane("logout")} before using the new setup line.`);
18760
+ process.exitCode = 1;
18629
18761
  return;
18630
18762
  }
18631
- await sleep3(POLL_INTERVAL_MS2);
18763
+ device = await (deps.claim ?? claimSetupCode)(baseUrl, {
18764
+ userCode: opts.code,
18765
+ label: deviceLabelFromHostname(hostname3()) ?? "Computer",
18766
+ ...paired && cfg?.deviceToken ? { deviceToken: cfg.deviceToken } : {}
18767
+ });
18768
+ writePairedConfig(device);
18769
+ write(pairingLine(device));
18632
18770
  }
18633
- process.stdout.write(
18634
- `companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
18635
- `
18771
+ const detection = await start(
18772
+ {
18773
+ ...opts,
18774
+ ...device ? { pairing: device } : {},
18775
+ ...!interactive && device ? { foreground: true } : {}
18776
+ },
18777
+ { ...deps, interactive }
18636
18778
  );
18637
- try {
18638
- kill(pid, "SIGKILL");
18639
- } catch {
18779
+ if (interactive && detection && !anyUsable(detection)) {
18780
+ const choice = await ui.select(CHOOSE_AGENT, AGENT_CHOICES);
18781
+ if (choice) {
18782
+ const note2 = agentNote(choice, { reload: null });
18783
+ ui.note(note2.lines, note2.title);
18784
+ }
18640
18785
  }
18641
- clearRuntimeState();
18642
- process.stdout.write("companion: force-stopped.\n");
18643
18786
  }
18644
- function isAlive(kill, pid) {
18787
+ function safeLoadConfig() {
18645
18788
  try {
18646
- kill(pid, 0);
18647
- return true;
18648
- } catch {
18649
- return false;
18650
- }
18651
- }
18652
-
18653
- // src/commands/service.ts
18654
- var STARTUP_WAIT_MS = 2e4;
18655
- async function installAndStartService(deps = {}) {
18656
- const readState = deps.readState ?? readLiveRuntimeState;
18657
- const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18658
- const now = deps.now ?? Date.now;
18659
- const before = await serviceState(deps);
18660
- const running = readState();
18661
- if (running && running.pid !== before.pid) await (deps.stopRunning ?? (() => stop()))();
18662
- setRunsAsService(true);
18663
- const outcome = await installService(deps);
18664
- for (const line of outcome.lines) write(`${INDENT}${line}`);
18665
- if (!outcome.ok) {
18666
- setRunsAsService(false);
18667
- return false;
18668
- }
18669
- const deadline = now() + STARTUP_WAIT_MS;
18670
- while (now() < deadline) {
18671
- const state = readState();
18672
- if (state?.socket) return true;
18673
- await sleep3(250);
18674
- }
18675
- write(
18676
- `${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
18677
- );
18678
- return false;
18679
- }
18680
- async function serviceInstall(deps = {}) {
18681
- if (!await installAndStartService(deps)) {
18682
- process.exitCode = 1;
18683
- return;
18684
- }
18685
- write(
18686
- INDENT + tick(
18687
- "Cabane runs as a service. It starts at boot, restarts if it stops, and updates itself, usually between turns."
18688
- )
18689
- );
18690
- write(`${INDENT} Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
18691
- }
18692
- async function serviceUninstall(deps = {}) {
18693
- const outcome = await uninstallService(deps);
18694
- for (const line of outcome.lines) write(`${INDENT}${line}`);
18695
- if (outcome.lines.length > 0) return;
18696
- setRunsAsService(false);
18697
- write(
18698
- INDENT + tick("Removed the service. Cabane won't start at boot, and it isn't running now.")
18699
- );
18700
- write(`${INDENT} Start it again with ${cabane("start")}`);
18701
- }
18702
- async function serviceStatus(deps = {}) {
18703
- const state = await serviceState(deps);
18704
- for (const line of serviceRows(state)) write(line);
18705
- for (const line of updateRows(readUpdateState(), safeConfig()?.update?.auto, {
18706
- service: state.installed
18707
- }))
18708
- write(line);
18709
- if (state.installed && state.state !== "running") process.exitCode = 1;
18710
- }
18711
- var LABEL_WIDTH = 13;
18712
- var row = (label, value) => ` ${label.padEnd(LABEL_WIDTH)}${value}`;
18713
- function serviceRows(state) {
18714
- if (!state.installed) {
18715
- return [
18716
- row(
18717
- "Service",
18718
- state.platform ? `not installed \u2014 ${cabane("service install")} starts Cabane at boot` : "not available on this system"
18719
- )
18720
- ];
18721
- }
18722
- const via = state.platform === "systemd" ? "systemd" : "launchd";
18723
- const rows = [
18724
- row(
18725
- "Service",
18726
- state.state === "running" ? `running (${via}${state.pid ? `, pid ${state.pid}` : ""})` : `installed, not running (${via}: ${state.state})`
18727
- )
18728
- ];
18729
- if (!state.nodeOk) {
18730
- rows.push(
18731
- ` ! The service's Node at ${tildePath(state.node ?? "")} is gone. Run ${cabane("service install")} again.`
18732
- );
18733
- }
18734
- return rows;
18735
- }
18736
- function updateRows(state, auto, opts = {}) {
18737
- const rows = [];
18738
- if (opts.version !== false) rows.push(row("Version", COMPANION_VERSION));
18739
- if (state.server) {
18740
- rows.push(
18741
- row(
18742
- "Cabane wants",
18743
- `${state.server.latest} (the latest), ${state.server.minimum} or newer to run turns`
18744
- )
18745
- );
18746
- }
18747
- rows.push(
18748
- row(
18749
- "Auto-update",
18750
- !opts.service ? `off \u2014 runs with the service: ${cabane("service install")}` : auto === true ? "on \u2014 updates install between turns when they can, and roll back if they fail" : `off \u2014 ${cabane("update --auto on")} turns it on`
18751
- )
18752
- );
18753
- const last = state.lastResult;
18754
- if (last) {
18755
- const when = new Date(last.at).toLocaleDateString("en-GB", { day: "numeric", month: "short" });
18756
- const why = last.reason ? `: ${last.reason}` : "";
18757
- rows.push(
18758
- row(
18759
- "Last update",
18760
- last.kind === "updated" ? `${last.from} \u2192 ${last.to} on ${when}` : last.kind === "rolled_back" ? `${last.to} rolled back to ${last.from} on ${when}${why}` : `${last.to} failed on ${when}${why}`
18761
- )
18762
- );
18763
- }
18764
- return rows;
18765
- }
18766
- function safeConfig() {
18767
- try {
18768
- return loadConfig();
18769
- } catch {
18770
- return null;
18771
- }
18772
- }
18773
- function setRunsAsService(on) {
18774
- const cfg = safeConfig();
18775
- if (!cfg) return;
18776
- const { serviceDeclined: _declined, ...rest } = cfg;
18777
- saveConfig({ ...rest, update: { auto: on } });
18778
- }
18779
-
18780
- // src/commands/setup.ts
18781
- var DETECT_TIMEOUT_MS = 15e3;
18782
- async function setup(opts = {}, deps = {}) {
18783
- setLogOverrides(opts);
18784
- setConsoleLogging(false);
18785
- try {
18786
- await setupScript(opts, deps);
18787
- } finally {
18788
- setConsoleLogging(true);
18789
- }
18790
- }
18791
- async function setupScript(opts, deps) {
18792
- const interactive = deps.interactive ?? isInteractive();
18793
- const paired = isDevicePaired();
18794
- if (!opts.code && !paired) {
18795
- write("Get a setup line from your workspace's Settings \u2192 Devices, then paste it here.");
18796
- return;
18797
- }
18798
- const ui = deps.ui ?? (interactive ? terminalUi() : plainUi());
18799
- const running = await (deps.live ?? liveCompanionState)();
18800
- const cfg = safeLoadConfig();
18801
- if (opts.code) {
18802
- const strays2 = (deps.strays ?? findStrayCompanions)(running ? [running.pid] : []);
18803
- if (strays2.length) {
18804
- ui.cancel(strayCompanionLines(strays2, cabane("setup")).join("\n"));
18805
- process.exitCode = 1;
18806
- return;
18807
- }
18808
- const baseUrl = resolvePairBaseUrl(opts.server ?? cfg?.baseUrl);
18809
- if (paired && cfg && new URL(cfg.baseUrl).origin !== new URL(baseUrl).origin)
18810
- throw new CompanionError(
18811
- "This computer is paired to another Cabane server. Log out before setting it up on a different server."
18812
- );
18813
- const device = await (deps.claim ?? claimSetupCode)(baseUrl, {
18814
- userCode: opts.code,
18815
- label: deviceLabelFromHostname(hostname3()) ?? "Computer",
18816
- ...paired && cfg?.deviceToken && cfg.baseUrl.replace(/\/$/, "") === baseUrl.replace(/\/$/, "") ? { deviceToken: cfg.deviceToken } : {}
18817
- });
18818
- writePairedConfig(device);
18819
- const detection2 = await detect(safeLoadConfig(), running, deps);
18820
- if (!running && interactive && !await bringUp2(ui, opts)) return;
18821
- for (const line of agentLines(detection2, { signInHint: !anyUsable(detection2) })) write(line);
18822
- write("All set here. Back to the browser.");
18823
- if (!running && !interactive) await start({ ...opts, foreground: true });
18824
- return;
18825
- }
18826
- const detection = await detect(cfg, running, deps);
18827
- ui.intro("Cabane setup");
18828
- const usable = anyUsable(detection);
18829
- ui.step(["Harnesses on this device", ...agentLines(detection, { signInHint: !usable })]);
18830
- if (!usable && hasProblem(detection)) {
18831
- ui.cancel(`Fix the above, then run ${cabane("setup")} again.`);
18832
- process.exitCode = 1;
18833
- return;
18834
- }
18835
- if (!usable) {
18836
- const choice = await ui.select(CHOOSE_AGENT, AGENT_CHOICES);
18837
- if (!choice) {
18838
- ui.cancel(`Run ${cabane("setup")} when you're ready.`);
18839
- process.exitCode = 1;
18840
- return;
18841
- }
18842
- const note2 = agentNote(choice);
18843
- ui.note(note2.lines, note2.title);
18844
- ui.outro(["Come back when you're signed in."]);
18845
- process.exitCode = 1;
18846
- return;
18847
- }
18848
- if (paired && running) {
18849
- ui.outro(["Nothing left to set up."]);
18850
- return;
18851
- }
18852
- const strays = (deps.strays ?? findStrayCompanions)(running ? [running.pid] : []);
18853
- if (strays.length > 0) {
18854
- ui.cancel(strayCompanionLines(strays, cabane("setup")).join("\n"));
18855
- process.exitCode = 1;
18856
- return;
18857
- }
18858
- if (!await bringUp2(ui, opts)) return;
18859
- ui.outro(["All set here. Back to the browser."]);
18860
- }
18861
- async function detect(cfg, running, deps) {
18862
- const local = deps.detect ?? ((c) => detectHarnesses(c));
18863
- if (!running?.socket) return local(cfg);
18864
- try {
18865
- return await (deps.detectRunning ?? detectOverSocket)(running.socket);
18866
- } catch {
18867
- return local(cfg);
18868
- }
18869
- }
18870
- async function detectOverSocket(socket) {
18871
- const body = await controlRequest(
18872
- socket,
18873
- { cmd: "detect" },
18874
- DETECT_TIMEOUT_MS
18875
- );
18876
- if (!body.detection) throw new Error("the running companion sent no detection");
18877
- return body.detection;
18878
- }
18879
- function hasProblem(detection) {
18880
- return HARNESS_RUNTIMES.some((runtime) => detection[runtime].state === "problem");
18881
- }
18882
- function agentLines(detection, opts) {
18883
- const rows = agentRows(detection, opts);
18884
- const names = rows.map((row3) => row3.usable ? `\u2713 ${row3.label}` : row3.label);
18885
- const width = Math.max(...names.map((name) => name.length)) + 3;
18886
- return rows.map((row3, i) => `${names[i].padEnd(width)}${row3.text}`);
18887
- }
18888
- async function bringUp2(ui, opts) {
18889
- if (serviceInstalled()) {
18890
- const outcome = await startService();
18891
- if (!outcome.ok) {
18892
- ui.cancel(outcome.lines.join(" "));
18893
- process.exitCode = 1;
18894
- return null;
18895
- }
18896
- return { asService: true, declined: false };
18897
- }
18898
- if (servicePlatform()) {
18899
- const spinner = ui.spinner();
18900
- spinner.start("Installing the service\u2026");
18901
- if (await installAndStartService()) {
18902
- spinner.stop("Service installed.");
18903
- return { asService: true, declined: false };
18904
- }
18905
- spinner.stop("Running it in the background instead.");
18906
- }
18907
- const { started } = await startDaemon({
18908
- report: "failures",
18909
- ...opts.logLevel ? { logLevel: opts.logLevel } : {},
18910
- ...opts.logFormat ? { logFormat: opts.logFormat } : {}
18911
- });
18912
- if (!started) return null;
18913
- return { asService: false, declined: false };
18914
- }
18915
- function safeLoadConfig() {
18916
- try {
18917
- return loadConfig();
18789
+ return loadConfig();
18918
18790
  } catch {
18919
18791
  return null;
18920
18792
  }
@@ -19086,6 +18958,7 @@ async function logs(opts = {}) {
19086
18958
  }
19087
18959
 
19088
18960
  // src/commands/status.ts
18961
+ init_cabane_command();
19089
18962
  init_install_kind();
19090
18963
  init_cabane_command();
19091
18964
  import { readdirSync as readdirSync7, realpathSync as realpathSync8, existsSync as existsSync21 } from "fs";
@@ -19097,6 +18970,211 @@ init_secrets();
19097
18970
  init_term();
19098
18971
  init_transcript_writer();
19099
18972
  init_version2();
18973
+
18974
+ // src/commands/service.ts
18975
+ init_cabane_command();
18976
+ init_config();
18977
+ init_term();
18978
+ init_version2();
18979
+
18980
+ // src/commands/stop.ts
18981
+ var TERM_GRACE_MS = 6e3;
18982
+ var POLL_INTERVAL_MS2 = 150;
18983
+ async function stop(deps = {}) {
18984
+ if ((deps.serviceInstalled ?? serviceInstalled)()) {
18985
+ const outcome = await (deps.stopService ?? stopService)();
18986
+ for (const line of outcome.lines) process.stdout.write(`${line}
18987
+ `);
18988
+ if (!outcome.ok) {
18989
+ process.exitCode = 1;
18990
+ return;
18991
+ }
18992
+ process.stdout.write(
18993
+ "companion: stopped the service. It starts again at boot, or with `cabane start`.\n"
18994
+ );
18995
+ return;
18996
+ }
18997
+ const readState = deps.readState ?? readLiveRuntimeState;
18998
+ const verify = deps.verify ?? ((s) => verifyRuntime(s));
18999
+ const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
19000
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
19001
+ const now = deps.now ?? (() => Date.now());
19002
+ const state = readState();
19003
+ if (!state) {
19004
+ process.stdout.write("companion: not running (nothing to stop).\n");
19005
+ return;
19006
+ }
19007
+ if (await verify(state) === "stale") {
19008
+ clearRuntimeState();
19009
+ process.stdout.write("companion: not running (stale marker swept).\n");
19010
+ return;
19011
+ }
19012
+ const { pid } = state;
19013
+ process.stdout.write(`companion: stopping (pid ${pid})\u2026
19014
+ `);
19015
+ try {
19016
+ kill(pid, "SIGTERM");
19017
+ } catch {
19018
+ clearRuntimeState();
19019
+ process.stdout.write("companion: already stopped.\n");
19020
+ return;
19021
+ }
19022
+ const deadline = now() + TERM_GRACE_MS;
19023
+ while (now() < deadline) {
19024
+ if (!isAlive(kill, pid)) {
19025
+ process.stdout.write("companion: stopped.\n");
19026
+ return;
19027
+ }
19028
+ await sleep3(POLL_INTERVAL_MS2);
19029
+ }
19030
+ process.stdout.write(
19031
+ `companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
19032
+ `
19033
+ );
19034
+ try {
19035
+ kill(pid, "SIGKILL");
19036
+ } catch {
19037
+ }
19038
+ clearRuntimeState();
19039
+ process.stdout.write("companion: force-stopped.\n");
19040
+ }
19041
+ function isAlive(kill, pid) {
19042
+ try {
19043
+ kill(pid, 0);
19044
+ return true;
19045
+ } catch {
19046
+ return false;
19047
+ }
19048
+ }
19049
+
19050
+ // src/commands/service.ts
19051
+ var STARTUP_WAIT_MS = 2e4;
19052
+ async function installAndStartService(deps = {}) {
19053
+ const readState = deps.readState ?? readLiveRuntimeState;
19054
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
19055
+ const now = deps.now ?? Date.now;
19056
+ const before = await serviceState(deps);
19057
+ const running = readState();
19058
+ if (running && running.pid !== before.pid) await (deps.stopRunning ?? (() => stop()))();
19059
+ setRunsAsService(true);
19060
+ const outcome = await installService(deps);
19061
+ for (const line of outcome.lines) write(`${INDENT}${line}`);
19062
+ if (!outcome.ok) {
19063
+ setRunsAsService(false);
19064
+ return false;
19065
+ }
19066
+ const deadline = now() + STARTUP_WAIT_MS;
19067
+ while (now() < deadline) {
19068
+ const state = readState();
19069
+ if (state?.socket) return true;
19070
+ await sleep3(250);
19071
+ }
19072
+ write(
19073
+ `${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
19074
+ );
19075
+ return false;
19076
+ }
19077
+ async function serviceInstall(deps = {}) {
19078
+ if (!await installAndStartService(deps)) {
19079
+ process.exitCode = 1;
19080
+ return;
19081
+ }
19082
+ write(
19083
+ INDENT + tick(
19084
+ "Cabane runs as a service. It starts at boot, restarts if it stops, and updates itself, usually between turns."
19085
+ )
19086
+ );
19087
+ write(`${INDENT} Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
19088
+ }
19089
+ async function serviceUninstall(deps = {}) {
19090
+ const outcome = await uninstallService(deps);
19091
+ for (const line of outcome.lines) write(`${INDENT}${line}`);
19092
+ if (outcome.lines.length > 0) return;
19093
+ setRunsAsService(false);
19094
+ write(
19095
+ INDENT + tick("Removed the service. Cabane won't start at boot, and it isn't running now.")
19096
+ );
19097
+ write(`${INDENT} Start it again with ${cabane("start")}`);
19098
+ }
19099
+ async function serviceStatus(deps = {}) {
19100
+ const state = await serviceState(deps);
19101
+ for (const line of serviceRows(state)) write(line);
19102
+ for (const line of updateRows(readUpdateState(), safeConfig()?.update?.auto, {
19103
+ service: state.installed
19104
+ }))
19105
+ write(line);
19106
+ if (state.installed && state.state !== "running") process.exitCode = 1;
19107
+ }
19108
+ var LABEL_WIDTH = 13;
19109
+ var row = (label, value) => ` ${label.padEnd(LABEL_WIDTH)}${value}`;
19110
+ function serviceRows(state) {
19111
+ if (!state.installed) {
19112
+ return [
19113
+ row(
19114
+ "Service",
19115
+ state.platform ? `not installed \u2014 ${cabane("service install")} starts Cabane at boot` : "not available on this system"
19116
+ )
19117
+ ];
19118
+ }
19119
+ const via = state.platform === "systemd" ? "systemd" : "launchd";
19120
+ const rows = [
19121
+ row(
19122
+ "Service",
19123
+ state.state === "running" ? `running (${via}${state.pid ? `, pid ${state.pid}` : ""})` : `installed, not running (${via}: ${state.state})`
19124
+ )
19125
+ ];
19126
+ if (!state.nodeOk) {
19127
+ rows.push(
19128
+ ` ! The service's Node at ${tildePath(state.node ?? "")} is gone. Run ${cabane("service install")} again.`
19129
+ );
19130
+ }
19131
+ return rows;
19132
+ }
19133
+ function updateRows(state, auto, opts = {}) {
19134
+ const rows = [];
19135
+ if (opts.version !== false) rows.push(row("Version", COMPANION_VERSION));
19136
+ if (state.server) {
19137
+ rows.push(
19138
+ row(
19139
+ "Cabane wants",
19140
+ `${state.server.latest} (the latest), ${state.server.minimum} or newer to run turns`
19141
+ )
19142
+ );
19143
+ }
19144
+ rows.push(
19145
+ row(
19146
+ "Auto-update",
19147
+ !opts.service ? `off \u2014 runs with the service: ${cabane("service install")}` : auto === true ? "on \u2014 updates install between turns when they can, and roll back if they fail" : `off \u2014 ${cabane("update --auto on")} turns it on`
19148
+ )
19149
+ );
19150
+ const last = state.lastResult;
19151
+ if (last) {
19152
+ const when = new Date(last.at).toLocaleDateString("en-GB", { day: "numeric", month: "short" });
19153
+ const why = last.reason ? `: ${last.reason}` : "";
19154
+ rows.push(
19155
+ row(
19156
+ "Last update",
19157
+ last.kind === "updated" ? `${last.from} \u2192 ${last.to} on ${when}` : last.kind === "rolled_back" ? `${last.to} rolled back to ${last.from} on ${when}${why}` : `${last.to} failed on ${when}${why}`
19158
+ )
19159
+ );
19160
+ }
19161
+ return rows;
19162
+ }
19163
+ function safeConfig() {
19164
+ try {
19165
+ return loadConfig();
19166
+ } catch {
19167
+ return null;
19168
+ }
19169
+ }
19170
+ function setRunsAsService(on) {
19171
+ const cfg = safeConfig();
19172
+ if (!cfg) return;
19173
+ const { serviceDeclined: _declined, ...rest } = cfg;
19174
+ saveConfig({ ...rest, update: { auto: on } });
19175
+ }
19176
+
19177
+ // src/commands/status.ts
19100
19178
  var LABEL_WIDTH2 = 13;
19101
19179
  function row2(label, value) {
19102
19180
  return ` ${label.padEnd(LABEL_WIDTH2)}${value}`;
@@ -19105,6 +19183,7 @@ async function status(deps = {}) {
19105
19183
  const out = (line) => void process.stdout.write(`${line}
19106
19184
  `);
19107
19185
  const running = await liveState();
19186
+ for (const line of pathLines(reloadLine())) out(line);
19108
19187
  const strays = strayStatusLines(running, deps.strays);
19109
19188
  out(` ${versionLine()}`);
19110
19189
  for (const line of otherInstallLines(running)) out(line);
@@ -19115,7 +19194,9 @@ async function status(deps = {}) {
19115
19194
  cfg = null;
19116
19195
  }
19117
19196
  if (!cfg || !cfg.deviceToken) {
19118
- out(` Not paired yet. Run ${cabane("setup")} to set up this device.`);
19197
+ out(
19198
+ " Not paired yet. Get a setup line from a workspace's Settings \u2192 Devices and paste it here."
19199
+ );
19119
19200
  for (const line of strays) out(line);
19120
19201
  process.exitCode = 1;
19121
19202
  return;
@@ -19130,6 +19211,16 @@ async function status(deps = {}) {
19130
19211
  } else {
19131
19212
  out(row2("Running", `no \u2014 start it with ${cabane("start")}`));
19132
19213
  }
19214
+ out(
19215
+ row2(
19216
+ "Device",
19217
+ deviceLine({
19218
+ ...await readTerminalStatus(running?.socket),
19219
+ running: !!running,
19220
+ devicesUrl: `${cfg.baseUrl.replace(/\/$/, "")}/settings/devices`
19221
+ })
19222
+ )
19223
+ );
19133
19224
  for (const line of strays) out(line);
19134
19225
  const service2 = await serviceState();
19135
19226
  for (const line of serviceRows(service2)) out(line);