@cabane/companion 0.6.178 → 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 +888 -797
- package/dist/pairing-config.js +2 -0
- package/dist/runtime.js +393 -336
- package/package.json +1 -1
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
|
|
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 =
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
1760
|
+
hostFs: z6.boolean(),
|
|
1764
1761
|
// Public web (`WebSearch` / `WebFetch`). Not host reach; a grant like the rest.
|
|
1765
|
-
web:
|
|
1762
|
+
web: z6.boolean(),
|
|
1766
1763
|
// Browser automation (the Playwright MCP surface). Varies by host.
|
|
1767
|
-
browser:
|
|
1764
|
+
browser: z6.boolean(),
|
|
1768
1765
|
// User-configured MCP servers permitted.
|
|
1769
|
-
userMcp:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1805
|
-
type:
|
|
1806
|
-
state:
|
|
1807
|
-
degraded:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1828
|
-
type:
|
|
1829
|
-
id:
|
|
1830
|
-
name:
|
|
1831
|
-
phase:
|
|
1832
|
-
summary:
|
|
1833
|
-
input:
|
|
1834
|
-
result:
|
|
1835
|
-
mcpServer:
|
|
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
|
-
|
|
1857
|
-
type:
|
|
1858
|
-
ok:
|
|
1859
|
-
reason:
|
|
1860
|
-
usage:
|
|
1861
|
-
inputTokens:
|
|
1862
|
-
outputTokens:
|
|
1863
|
-
cacheReadTokens:
|
|
1864
|
-
cacheCreationTokens:
|
|
1865
|
-
contextTokens:
|
|
1866
|
-
contextWindow:
|
|
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:
|
|
1870
|
-
contextCategories:
|
|
1871
|
-
|
|
1872
|
-
name:
|
|
1873
|
-
tokens:
|
|
1874
|
-
kind:
|
|
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:
|
|
1878
|
-
|
|
1879
|
-
trigger:
|
|
1880
|
-
preTokens:
|
|
1881
|
-
postTokens:
|
|
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:
|
|
1886
|
-
resolvedConfig:
|
|
1887
|
-
effort:
|
|
1888
|
-
thinking:
|
|
1889
|
-
reasoningEffort:
|
|
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:
|
|
1895
|
-
initReceived:
|
|
1896
|
-
servers:
|
|
1897
|
-
toolCount:
|
|
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
|
|
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 =
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
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 =
|
|
1938
|
-
outcome:
|
|
1934
|
+
turnDiagnosticsSchema = z8.object({
|
|
1935
|
+
outcome: z8.enum(turnOutcomes),
|
|
1939
1936
|
resultReason: turnResultReasonSchema.nullable(),
|
|
1940
|
-
sessionMode:
|
|
1941
|
-
sessionFingerprint:
|
|
1942
|
-
eventCounts:
|
|
1943
|
-
session:
|
|
1944
|
-
text:
|
|
1945
|
-
runtime_notice:
|
|
1946
|
-
thinking:
|
|
1947
|
-
tool:
|
|
1948
|
-
result:
|
|
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:
|
|
1951
|
-
finalSource:
|
|
1952
|
-
mcpInventory:
|
|
1953
|
-
initReceived:
|
|
1954
|
-
servers:
|
|
1955
|
-
toolCount:
|
|
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
|
|
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 =
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
type:
|
|
2039
|
-
source:
|
|
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
|
-
|
|
2042
|
-
type:
|
|
2043
|
-
source:
|
|
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 =
|
|
2047
|
-
resolvedRunConfigSchema =
|
|
2048
|
-
model:
|
|
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:
|
|
2047
|
+
runtimeOptions: z9.record(z9.string(), z9.unknown()).optional()
|
|
2051
2048
|
});
|
|
2052
|
-
resolvedMcpServerSchema =
|
|
2053
|
-
|
|
2054
|
-
type:
|
|
2055
|
-
command:
|
|
2056
|
-
args:
|
|
2057
|
-
env:
|
|
2058
|
-
envFromEnv:
|
|
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
|
-
|
|
2061
|
-
type:
|
|
2062
|
-
url:
|
|
2063
|
-
headers:
|
|
2064
|
-
headersFromEnv:
|
|
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
|
-
|
|
2067
|
-
type:
|
|
2068
|
-
url:
|
|
2069
|
-
headers:
|
|
2070
|
-
headersFromEnv:
|
|
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 =
|
|
2074
|
-
hostInjectedServersSchema =
|
|
2075
|
-
turnRequestSchema =
|
|
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:
|
|
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:
|
|
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:
|
|
2083
|
+
prompt: z9.string(),
|
|
2087
2084
|
// The multi-block user-message body (text + vision).
|
|
2088
|
-
content:
|
|
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:
|
|
2091
|
+
session: z9.string().nullable(),
|
|
2095
2092
|
// The cabane control-plane coordinates for this turn's MCP + post-back.
|
|
2096
|
-
cabane:
|
|
2097
|
-
mcpUrl:
|
|
2098
|
-
bearer:
|
|
2099
|
-
activeConversationId:
|
|
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:
|
|
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:
|
|
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:
|
|
2113
|
-
cwd:
|
|
2114
|
-
env:
|
|
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:
|
|
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:
|
|
2121
|
+
claudeCode: z9.object({ autoMemory: z9.boolean().optional() }).optional()
|
|
2125
2122
|
}),
|
|
2126
2123
|
// Host-owned injected servers (host-filled).
|
|
2127
|
-
extra:
|
|
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
|
|
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 =
|
|
2159
|
-
name:
|
|
2155
|
+
conformanceFixtureSchema = z10.object({
|
|
2156
|
+
name: z10.string(),
|
|
2160
2157
|
request: turnRequestSchema,
|
|
2161
|
-
nativeStream:
|
|
2162
|
-
expected:
|
|
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
|
|
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 =
|
|
2720
|
-
thinking:
|
|
2721
|
-
|
|
2722
|
-
type:
|
|
2723
|
-
display:
|
|
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
|
-
|
|
2726
|
-
type:
|
|
2727
|
-
budgetTokens:
|
|
2728
|
-
display:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
4336
|
-
agent:
|
|
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
|
|
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 =
|
|
5655
|
-
modelReasoningEffort:
|
|
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
|
|
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 =
|
|
7247
|
-
secretRef =
|
|
7248
|
-
envCredentialSchema =
|
|
7249
|
-
gitCredentialSchema =
|
|
7250
|
-
host:
|
|
7251
|
-
username:
|
|
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 =
|
|
7255
|
-
hosts:
|
|
7256
|
-
allow:
|
|
7257
|
-
deny:
|
|
7258
|
-
defaults:
|
|
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:
|
|
7261
|
-
mode:
|
|
7262
|
-
denyRead:
|
|
7263
|
-
allowRead:
|
|
7264
|
-
allowWrite:
|
|
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:
|
|
7267
|
-
env:
|
|
7268
|
-
git:
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
7794
|
-
type:
|
|
7795
|
-
command:
|
|
7796
|
-
args:
|
|
7797
|
-
env:
|
|
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:
|
|
7797
|
+
envFromEnv: z16.record(z16.string(), z16.string()).optional()
|
|
7801
7798
|
});
|
|
7802
|
-
mcpHttpServerSchema =
|
|
7803
|
-
type:
|
|
7804
|
-
url:
|
|
7805
|
-
headers:
|
|
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:
|
|
7806
|
+
headersFromEnv: z16.record(z16.string(), z16.string()).optional()
|
|
7810
7807
|
});
|
|
7811
|
-
mcpSseServerSchema =
|
|
7812
|
-
type:
|
|
7813
|
-
url:
|
|
7814
|
-
headers:
|
|
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:
|
|
7815
|
+
headersFromEnv: z16.record(z16.string(), z16.string()).optional()
|
|
7819
7816
|
});
|
|
7820
|
-
mcpServerDefSchema =
|
|
7821
|
-
thinkingConfigSchema =
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
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 =
|
|
7827
|
-
runConfigSchema =
|
|
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:
|
|
7833
|
-
mcpServers:
|
|
7834
|
-
model:
|
|
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
|
|
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 =
|
|
7927
|
-
quotaState:
|
|
7928
|
-
limitedUntil:
|
|
7929
|
-
lastError:
|
|
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 =
|
|
7932
|
-
deviceId:
|
|
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:
|
|
7936
|
-
workspaceId:
|
|
7937
|
-
workspaceSlug:
|
|
7938
|
-
agentId:
|
|
7939
|
-
agentUsername:
|
|
7940
|
-
agentName:
|
|
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:
|
|
7944
|
-
claudeCodeAvailable:
|
|
7945
|
-
codexAvailable:
|
|
7946
|
-
idleTimeoutMs:
|
|
7947
|
-
totalTimeoutMs:
|
|
7948
|
-
leaseRenewalMs:
|
|
7949
|
-
preparingRowDelayMs:
|
|
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:
|
|
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:
|
|
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:
|
|
7962
|
-
logLevel:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
7975
|
+
secretsFromLauncher: z17.boolean().optional()
|
|
7979
7976
|
});
|
|
7980
|
-
dispatchSchema =
|
|
7981
|
-
type:
|
|
7982
|
-
agentId:
|
|
7983
|
-
deviceId:
|
|
7984
|
-
conversationId:
|
|
7985
|
-
messageId:
|
|
7986
|
-
episode:
|
|
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 =
|
|
7989
|
-
credential:
|
|
7990
|
-
sandboxCredentials:
|
|
7991
|
-
|
|
7992
|
-
name:
|
|
7993
|
-
value:
|
|
7994
|
-
hosts:
|
|
7995
|
-
gitHost:
|
|
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
|
|
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:
|
|
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:
|
|
8514
|
-
question:
|
|
8515
|
-
headline:
|
|
8516
|
-
options:
|
|
8517
|
-
questions:
|
|
8518
|
-
|
|
8519
|
-
headline:
|
|
8520
|
-
body:
|
|
8521
|
-
options:
|
|
8522
|
-
|
|
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:
|
|
8527
|
-
askId:
|
|
8528
|
-
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:
|
|
8570
|
-
at:
|
|
8571
|
-
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:
|
|
8606
|
-
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
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
13666
|
-
var credentialMapSchema =
|
|
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
|
|
15656
|
-
var resultSchema =
|
|
15657
|
-
kind:
|
|
15658
|
-
from:
|
|
15659
|
-
to:
|
|
15660
|
-
at:
|
|
15661
|
-
reason:
|
|
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 =
|
|
15664
|
-
from:
|
|
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:
|
|
15667
|
-
to:
|
|
15668
|
-
at:
|
|
15669
|
-
boots:
|
|
15690
|
+
fromRoot: z19.string(),
|
|
15691
|
+
to: z19.string(),
|
|
15692
|
+
at: z19.string(),
|
|
15693
|
+
boots: z19.number().int().nonnegative()
|
|
15670
15694
|
});
|
|
15671
|
-
var stateSchema =
|
|
15672
|
-
server:
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
16380
|
-
|
|
16381
|
-
|
|
16382
|
-
|
|
16383
|
-
|
|
16384
|
-
|
|
16385
|
-
|
|
16386
|
-
|
|
16387
|
-
|
|
16388
|
-
|
|
16389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16432
|
+
...!first && this.detection ? { harnesses: harnessEntriesFromDetection(this.detection) } : {},
|
|
16397
16433
|
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
16398
|
-
|
|
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
|
|
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: () =>
|
|
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
|
-
|
|
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
|
|
18319
|
-
|
|
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,
|
|
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,
|
|
18366
|
-
const
|
|
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
|
-
|
|
18378
|
-
|
|
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
|
-
|
|
18384
|
-
|
|
18385
|
-
|
|
18386
|
-
|
|
18387
|
-
|
|
18388
|
-
|
|
18389
|
-
|
|
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
|
-
|
|
18392
|
-
|
|
18393
|
-
|
|
18394
|
-
|
|
18395
|
-
write(
|
|
18396
|
-
|
|
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
|
|
18604
|
+
return detection;
|
|
18458
18605
|
}
|
|
18459
|
-
async function
|
|
18460
|
-
const
|
|
18461
|
-
|
|
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
|
-
|
|
18578
|
-
|
|
18579
|
-
|
|
18580
|
-
|
|
18581
|
-
|
|
18582
|
-
|
|
18583
|
-
|
|
18584
|
-
|
|
18585
|
-
|
|
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
|
-
|
|
18599
|
-
}
|
|
18600
|
-
|
|
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
|
-
|
|
18611
|
-
|
|
18612
|
-
|
|
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
|
|
18616
|
-
|
|
18617
|
-
|
|
18618
|
-
|
|
18619
|
-
|
|
18620
|
-
|
|
18621
|
-
|
|
18622
|
-
process.
|
|
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
|
-
|
|
18626
|
-
|
|
18627
|
-
|
|
18628
|
-
|
|
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
|
|
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
|
-
|
|
18634
|
-
|
|
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
|
-
|
|
18638
|
-
|
|
18639
|
-
|
|
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
|
|
18787
|
+
function safeLoadConfig() {
|
|
18645
18788
|
try {
|
|
18646
|
-
|
|
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(
|
|
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);
|