@cabane/companion 0.6.35 → 0.6.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -36
- package/dist/cli.js +416 -789
- package/dist/runtime.js +274 -119
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -806,8 +806,12 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
806
806
|
...claudeInstalled ? ["Claude Code"] : [],
|
|
807
807
|
...codexInstalled ? ["Codex"] : []
|
|
808
808
|
];
|
|
809
|
+
const connectCommands = [
|
|
810
|
+
...claudeInstalled ? ["`cabane-companion connect claude-code`"] : [],
|
|
811
|
+
...codexInstalled ? ["`cabane-companion connect codex`"] : []
|
|
812
|
+
];
|
|
809
813
|
warn(
|
|
810
|
-
"No harness is connected on this device yet, so no agent turn can run here. " + (installed.length > 0 ? `We found ${installed.join(" and ")} on this machine \u2014 connect ${installed.length > 1 ? "one" : "it"}
|
|
814
|
+
"No harness is connected on this device yet, so no agent turn can run here. " + (installed.length > 0 ? `We found ${installed.join(" and ")} on this machine \u2014 connect ${installed.length > 1 ? "one with " : "it with "}${connectCommands.join(" or ")} and turns start routing here.` : "Install a harness and sign in \u2014 Claude Code (`npm i -g @anthropic-ai/claude-code`), the Codex CLI (`codex login`), or `opencode serve` \u2014 then connect it with `cabane-companion connect <harness>` (pass `--url <url>` for opencode).")
|
|
811
815
|
);
|
|
812
816
|
}
|
|
813
817
|
|
|
@@ -1663,30 +1667,10 @@ async function pair(opts = {}) {
|
|
|
1663
1667
|
}
|
|
1664
1668
|
|
|
1665
1669
|
// src/cli.ts
|
|
1666
|
-
import { readFileSync as
|
|
1667
|
-
|
|
1668
|
-
// src/service/index.ts
|
|
1669
|
-
import { dirname as dirname5 } from "path";
|
|
1670
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
1670
1671
|
|
|
1671
|
-
// src/
|
|
1672
|
-
import {
|
|
1673
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync4, rmSync as rmSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
1674
|
-
import { homedir as homedir3 } from "os";
|
|
1675
|
-
|
|
1676
|
-
// src/cli-entry.ts
|
|
1677
|
-
import { existsSync as existsSync5 } from "fs";
|
|
1678
|
-
import { fileURLToPath } from "url";
|
|
1679
|
-
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
1680
|
-
function companionCliEntry(deps = {}) {
|
|
1681
|
-
const exists = deps.exists ?? existsSync5;
|
|
1682
|
-
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath(new URL(rel, import.meta.url)));
|
|
1683
|
-
for (const candidate of candidates) {
|
|
1684
|
-
if (exists(candidate)) return candidate;
|
|
1685
|
-
}
|
|
1686
|
-
const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
|
|
1687
|
-
if (argv1 && exists(argv1)) return argv1;
|
|
1688
|
-
return candidates[0] ?? "";
|
|
1689
|
-
}
|
|
1672
|
+
// src/commands/start.ts
|
|
1673
|
+
import { hostname as hostname2 } from "os";
|
|
1690
1674
|
|
|
1691
1675
|
// src/logger.ts
|
|
1692
1676
|
import { createWriteStream, mkdirSync as mkdirSync5 } from "fs";
|
|
@@ -1752,455 +1736,19 @@ function getLogger() {
|
|
|
1752
1736
|
return cached;
|
|
1753
1737
|
}
|
|
1754
1738
|
|
|
1755
|
-
// src/service/host.ts
|
|
1756
|
-
var RUN_TIMEOUT_MS = 1e4;
|
|
1757
|
-
function defaultServiceHost() {
|
|
1758
|
-
if (process.env.VITEST) {
|
|
1759
|
-
throw new Error(
|
|
1760
|
-
"defaultServiceHost() was reached during a test run \u2014 it shells out to the real service manager. Inject a fake host (apps/companion/test/service-host-fake.ts) instead."
|
|
1761
|
-
);
|
|
1762
|
-
}
|
|
1763
|
-
return {
|
|
1764
|
-
platform: process.platform,
|
|
1765
|
-
env: process.env,
|
|
1766
|
-
home: homedir3(),
|
|
1767
|
-
uid: process.getuid?.() ?? 0,
|
|
1768
|
-
execPath: process.execPath,
|
|
1769
|
-
cliPath: companionCliEntry(),
|
|
1770
|
-
logPath: companionLogPath(),
|
|
1771
|
-
fs: {
|
|
1772
|
-
read: (path) => {
|
|
1773
|
-
try {
|
|
1774
|
-
return readFileSync4(path, "utf8");
|
|
1775
|
-
} catch {
|
|
1776
|
-
return null;
|
|
1777
|
-
}
|
|
1778
|
-
},
|
|
1779
|
-
write: (path, contents) => writeFileSync4(path, contents, "utf8"),
|
|
1780
|
-
remove: (path) => rmSync5(path, { force: true }),
|
|
1781
|
-
exists: (path) => existsSync6(path),
|
|
1782
|
-
mkdirp: (dir2) => {
|
|
1783
|
-
mkdirSync6(dir2, { recursive: true });
|
|
1784
|
-
}
|
|
1785
|
-
},
|
|
1786
|
-
run: (cmd, args) => {
|
|
1787
|
-
const res = spawnSync(cmd, args, { encoding: "utf8", timeout: RUN_TIMEOUT_MS });
|
|
1788
|
-
return {
|
|
1789
|
-
ok: res.status === 0,
|
|
1790
|
-
stdout: res.stdout ?? "",
|
|
1791
|
-
stderr: res.stderr ?? (res.error ? res.error.message : "")
|
|
1792
|
-
};
|
|
1793
|
-
}
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
// src/service/launchd.ts
|
|
1798
|
-
import { join as join6 } from "path";
|
|
1799
|
-
var LAUNCHD_LABEL = "ai.cabane.companion";
|
|
1800
|
-
function launchAgentPath(home) {
|
|
1801
|
-
return join6(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
1802
|
-
}
|
|
1803
|
-
function renderPlist(input) {
|
|
1804
|
-
const args = input.programArguments.map((a) => ` <string>${xml(a)}</string>`).join("\n");
|
|
1805
|
-
const env = Object.entries(input.environment).map(([k, v]) => ` <key>${xml(k)}</key>
|
|
1806
|
-
<string>${xml(v)}</string>`).join("\n");
|
|
1807
|
-
return [
|
|
1808
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
1809
|
-
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
1810
|
-
'<plist version="1.0">',
|
|
1811
|
-
"<dict>",
|
|
1812
|
-
" <key>Label</key>",
|
|
1813
|
-
` <string>${LAUNCHD_LABEL}</string>`,
|
|
1814
|
-
" <key>ProgramArguments</key>",
|
|
1815
|
-
" <array>",
|
|
1816
|
-
args,
|
|
1817
|
-
" </array>",
|
|
1818
|
-
" <key>RunAtLoad</key>",
|
|
1819
|
-
" <true/>",
|
|
1820
|
-
" <key>KeepAlive</key>",
|
|
1821
|
-
" <dict>",
|
|
1822
|
-
" <key>SuccessfulExit</key>",
|
|
1823
|
-
" <false/>",
|
|
1824
|
-
" </dict>",
|
|
1825
|
-
" <key>EnvironmentVariables</key>",
|
|
1826
|
-
" <dict>",
|
|
1827
|
-
env,
|
|
1828
|
-
" </dict>",
|
|
1829
|
-
" <key>StandardOutPath</key>",
|
|
1830
|
-
` <string>${xml(input.logPath)}</string>`,
|
|
1831
|
-
" <key>StandardErrorPath</key>",
|
|
1832
|
-
` <string>${xml(input.logPath)}</string>`,
|
|
1833
|
-
"</dict>",
|
|
1834
|
-
"</plist>",
|
|
1835
|
-
""
|
|
1836
|
-
].join("\n");
|
|
1837
|
-
}
|
|
1838
|
-
function domain(host) {
|
|
1839
|
-
return `gui/${host.uid}`;
|
|
1840
|
-
}
|
|
1841
|
-
function target(host) {
|
|
1842
|
-
return `${domain(host)}/${LAUNCHD_LABEL}`;
|
|
1843
|
-
}
|
|
1844
|
-
function launchdStart(host, plistPath) {
|
|
1845
|
-
host.run("launchctl", ["bootout", target(host)]);
|
|
1846
|
-
const res = host.run("launchctl", ["bootstrap", domain(host), plistPath]);
|
|
1847
|
-
return res.ok ? { ok: true } : { ok: false, detail: firstLine(res.stderr || res.stdout) };
|
|
1848
|
-
}
|
|
1849
|
-
function launchdStop(host) {
|
|
1850
|
-
const res = host.run("launchctl", ["bootout", target(host)]);
|
|
1851
|
-
if (res.ok || notLoaded(res.stderr + res.stdout)) return { ok: true };
|
|
1852
|
-
return { ok: false, detail: firstLine(res.stderr || res.stdout) };
|
|
1853
|
-
}
|
|
1854
|
-
function launchdProbe(host) {
|
|
1855
|
-
const res = host.run("launchctl", ["print", target(host)]);
|
|
1856
|
-
if (res.ok) {
|
|
1857
|
-
const state = /state\s*=\s*(\w+)/.exec(res.stdout)?.[1];
|
|
1858
|
-
return { ownership: "held", running: state ? state === "running" : "unknown" };
|
|
1859
|
-
}
|
|
1860
|
-
return notLoaded(res.stderr + res.stdout) ? { ownership: "clear", running: false } : { ownership: "unknown", running: "unknown" };
|
|
1861
|
-
}
|
|
1862
|
-
function launchdRemove(host, plistPath) {
|
|
1863
|
-
const stopped = launchdStop(host);
|
|
1864
|
-
if (!stopped.ok) return stopped;
|
|
1865
|
-
host.fs.remove(plistPath);
|
|
1866
|
-
return { ok: true };
|
|
1867
|
-
}
|
|
1868
|
-
var NOT_LOADED = new RegExp(
|
|
1869
|
-
`no such process|(could not find|not find service)[^\\n]*${LAUNCHD_LABEL.replace(
|
|
1870
|
-
/[.*+?^${}()|[\]\\]/g,
|
|
1871
|
-
"\\$&"
|
|
1872
|
-
)}`,
|
|
1873
|
-
"i"
|
|
1874
|
-
);
|
|
1875
|
-
function notLoaded(output) {
|
|
1876
|
-
return NOT_LOADED.test(output);
|
|
1877
|
-
}
|
|
1878
|
-
function firstLine(s) {
|
|
1879
|
-
return s.trim().split("\n")[0] ?? "";
|
|
1880
|
-
}
|
|
1881
|
-
function xml(value) {
|
|
1882
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
// src/service/systemd.ts
|
|
1886
|
-
import { dirname as dirname4, join as join7 } from "path";
|
|
1887
|
-
var SYSTEMD_UNIT = "cabane-companion.service";
|
|
1888
|
-
function systemdUnitPath(host) {
|
|
1889
|
-
const configHome = host.env.XDG_CONFIG_HOME?.trim() ? host.env.XDG_CONFIG_HOME.trim() : join7(host.home, ".config");
|
|
1890
|
-
return join7(configHome, "systemd", "user", SYSTEMD_UNIT);
|
|
1891
|
-
}
|
|
1892
|
-
function renderUnit(input) {
|
|
1893
|
-
const [exec, ...rest] = input.programArguments;
|
|
1894
|
-
const execStart = [quote(exec ?? ""), ...rest.map(quote)].join(" ");
|
|
1895
|
-
const env = Object.entries(input.environment).map(([k, v]) => `Environment=${k}=${v}`);
|
|
1896
|
-
return [
|
|
1897
|
-
"[Unit]",
|
|
1898
|
-
"Description=Cabane Companion \u2014 keeps this device answering while you are logged in",
|
|
1899
|
-
"After=network-online.target",
|
|
1900
|
-
"Wants=network-online.target",
|
|
1901
|
-
"",
|
|
1902
|
-
"[Service]",
|
|
1903
|
-
"Type=simple",
|
|
1904
|
-
...env,
|
|
1905
|
-
`ExecStart=${execStart}`,
|
|
1906
|
-
"Restart=on-failure",
|
|
1907
|
-
"RestartSec=5",
|
|
1908
|
-
"",
|
|
1909
|
-
"[Install]",
|
|
1910
|
-
"WantedBy=default.target"
|
|
1911
|
-
].join("\n") + "\n";
|
|
1912
|
-
}
|
|
1913
|
-
function systemdReload(host) {
|
|
1914
|
-
host.run("systemctl", ["--user", "daemon-reload"]);
|
|
1915
|
-
}
|
|
1916
|
-
function systemdStart(host) {
|
|
1917
|
-
systemdReload(host);
|
|
1918
|
-
const enabled = host.run("systemctl", ["--user", "enable", SYSTEMD_UNIT]);
|
|
1919
|
-
if (!enabled.ok) return { ok: false, detail: firstLine2(enabled.stderr || enabled.stdout) };
|
|
1920
|
-
const started = host.run("systemctl", ["--user", "restart", SYSTEMD_UNIT]);
|
|
1921
|
-
if (!started.ok) return { ok: false, detail: firstLine2(started.stderr || started.stdout) };
|
|
1922
|
-
return { ok: true };
|
|
1923
|
-
}
|
|
1924
|
-
function systemdStop(host) {
|
|
1925
|
-
const res = host.run("systemctl", ["--user", "stop", SYSTEMD_UNIT]);
|
|
1926
|
-
if (res.ok || notLoaded2(res.stderr + res.stdout)) return { ok: true };
|
|
1927
|
-
return { ok: false, detail: firstLine2(res.stderr || res.stdout) };
|
|
1928
|
-
}
|
|
1929
|
-
function systemdProbe(host) {
|
|
1930
|
-
const state = host.run("systemctl", ["--user", "is-active", SYSTEMD_UNIT]).stdout.trim();
|
|
1931
|
-
if (state === "active") return { ownership: "held", running: true };
|
|
1932
|
-
if (state === "activating" || state === "reloading" || state === "deactivating") {
|
|
1933
|
-
return { ownership: "held", running: "transitional" };
|
|
1934
|
-
}
|
|
1935
|
-
if (state === "inactive" || state === "failed") return { ownership: "clear", running: false };
|
|
1936
|
-
return { ownership: "unknown", running: "unknown" };
|
|
1937
|
-
}
|
|
1938
|
-
function systemdWantsLinkPath(host) {
|
|
1939
|
-
return join7(dirname4(systemdUnitPath(host)), "default.target.wants", SYSTEMD_UNIT);
|
|
1940
|
-
}
|
|
1941
|
-
function systemdInstalled(host) {
|
|
1942
|
-
return host.fs.exists(systemdUnitPath(host)) || host.fs.exists(systemdWantsLinkPath(host));
|
|
1943
|
-
}
|
|
1944
|
-
function systemdRemove(host, unitPath) {
|
|
1945
|
-
const stopped = host.run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
|
|
1946
|
-
if (!(stopped.ok || notLoaded2(stopped.stderr + stopped.stdout))) {
|
|
1947
|
-
return { ok: false, detail: firstLine2(stopped.stderr || stopped.stdout) };
|
|
1948
|
-
}
|
|
1949
|
-
host.fs.remove(unitPath);
|
|
1950
|
-
const link = systemdWantsLinkPath(host);
|
|
1951
|
-
if (host.fs.exists(link)) host.fs.remove(link);
|
|
1952
|
-
systemdReload(host);
|
|
1953
|
-
return { ok: true };
|
|
1954
|
-
}
|
|
1955
|
-
var NOT_LOADED2 = new RegExp(
|
|
1956
|
-
`unit (file )?${SYSTEMD_UNIT.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} (not loaded|does not exist|not found|could not be found)`,
|
|
1957
|
-
"i"
|
|
1958
|
-
);
|
|
1959
|
-
function notLoaded2(output) {
|
|
1960
|
-
return NOT_LOADED2.test(output);
|
|
1961
|
-
}
|
|
1962
|
-
function readLinger(host) {
|
|
1963
|
-
const res = host.run("loginctl", ["show-user", String(host.uid), "-p", "Linger"]);
|
|
1964
|
-
if (!res.ok) return "unknown";
|
|
1965
|
-
const match = /Linger=(\w+)/.exec(res.stdout);
|
|
1966
|
-
if (!match) return "unknown";
|
|
1967
|
-
return match[1] === "yes" ? "yes" : "no";
|
|
1968
|
-
}
|
|
1969
|
-
function tryEnableLinger(host) {
|
|
1970
|
-
host.run("loginctl", ["enable-linger", String(host.uid)]);
|
|
1971
|
-
return readLinger(host);
|
|
1972
|
-
}
|
|
1973
|
-
function isRemoteSession(env) {
|
|
1974
|
-
return Boolean(env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT);
|
|
1975
|
-
}
|
|
1976
|
-
function quote(value) {
|
|
1977
|
-
return `"${value.replace(/(["\\])/g, "\\$1")}"`;
|
|
1978
|
-
}
|
|
1979
|
-
function firstLine2(s) {
|
|
1980
|
-
return s.trim().split("\n")[0] ?? "";
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
// src/service/index.ts
|
|
1984
|
-
function detectServiceManager(host = defaultServiceHost()) {
|
|
1985
|
-
if (host.platform === "darwin") return "launchd";
|
|
1986
|
-
if (host.platform !== "linux") return "none";
|
|
1987
|
-
return host.run("systemctl", ["--user", "show-environment"]).ok ? "systemd-user" : "none";
|
|
1988
|
-
}
|
|
1989
|
-
function programArguments(host, _opts) {
|
|
1990
|
-
return [host.execPath, host.cliPath, "start", "--foreground"];
|
|
1991
|
-
}
|
|
1992
|
-
function environment(host) {
|
|
1993
|
-
return {
|
|
1994
|
-
// PATH CAPTURE. launchd's default PATH is `/usr/bin:/bin:/usr/sbin:/sbin`
|
|
1995
|
-
// and a systemd user manager's is barely better — neither has `claude`,
|
|
1996
|
-
// `codex` or `opencode` on it. A companion that runs but can't find its
|
|
1997
|
-
// harness is worse than one that isn't running, because the device reads
|
|
1998
|
-
// Online. So we bake in the PATH of the shell that ran `start`, and because
|
|
1999
|
-
// it's part of the rendered content, a PATH that later drifts re-renders on
|
|
2000
|
-
// the next `start` like any other change.
|
|
2001
|
-
PATH: host.env.PATH ?? "",
|
|
2002
|
-
CABANE_COMPANION_DAEMON: "1"
|
|
2003
|
-
};
|
|
2004
|
-
}
|
|
2005
|
-
function unitPathFor(host, manager) {
|
|
2006
|
-
if (manager === "launchd") return launchAgentPath(host.home);
|
|
2007
|
-
if (manager === "systemd-user") return systemdUnitPath(host);
|
|
2008
|
-
return null;
|
|
2009
|
-
}
|
|
2010
|
-
function renderFor(host, manager, opts) {
|
|
2011
|
-
const input = { programArguments: programArguments(host, opts), environment: environment(host) };
|
|
2012
|
-
return manager === "launchd" ? renderPlist({ ...input, logPath: host.logPath }) : renderUnit(input);
|
|
2013
|
-
}
|
|
2014
|
-
function installService(opts = {}, host = defaultServiceHost()) {
|
|
2015
|
-
const manager = detectServiceManager(host);
|
|
2016
|
-
if (manager === "none") return { installed: false, reason: "unsupported" };
|
|
2017
|
-
const path = unitPathFor(host, manager);
|
|
2018
|
-
if (!path) return { installed: false, reason: "unsupported" };
|
|
2019
|
-
const desired = renderFor(host, manager, opts);
|
|
2020
|
-
const existing = host.fs.read(path);
|
|
2021
|
-
const hadUnit = existing !== null;
|
|
2022
|
-
const changed = existing !== desired;
|
|
2023
|
-
if (changed) {
|
|
2024
|
-
host.fs.mkdirp(dirname5(path));
|
|
2025
|
-
host.fs.write(path, desired);
|
|
2026
|
-
}
|
|
2027
|
-
if (manager === "systemd-user") {
|
|
2028
|
-
const linger = ensureLinger(host);
|
|
2029
|
-
if (linger !== "yes" && isRemoteSession(host.env)) {
|
|
2030
|
-
systemdRemove(host, path);
|
|
2031
|
-
return failedInstall(
|
|
2032
|
-
host,
|
|
2033
|
-
manager,
|
|
2034
|
-
"linger-unavailable",
|
|
2035
|
-
"a systemd --user service would stop when this SSH session ends (lingering is off and could not be enabled)"
|
|
2036
|
-
);
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
const started = manager === "launchd" ? launchdStart(host, path) : systemdStart(host);
|
|
2040
|
-
if (!started.ok) {
|
|
2041
|
-
if (!hadUnit) removeService(host, manager, path);
|
|
2042
|
-
return failedInstall(host, manager, "command-failed", started.detail);
|
|
2043
|
-
}
|
|
2044
|
-
return { installed: true, manager, changed };
|
|
2045
|
-
}
|
|
2046
|
-
function failedInstall(host, manager, reason, detail) {
|
|
2047
|
-
return {
|
|
2048
|
-
installed: false,
|
|
2049
|
-
reason,
|
|
2050
|
-
...detail ? { detail } : {},
|
|
2051
|
-
...managerOwnership(host, manager) === "clear" ? {} : { leftBehind: manager }
|
|
2052
|
-
};
|
|
2053
|
-
}
|
|
2054
|
-
function managerProbe(host, manager) {
|
|
2055
|
-
if (manager === "launchd") return launchdProbe(host);
|
|
2056
|
-
if (manager === "systemd-user") return systemdProbe(host);
|
|
2057
|
-
return { ownership: "clear", running: false };
|
|
2058
|
-
}
|
|
2059
|
-
function managerOwnership(host, manager) {
|
|
2060
|
-
return managerProbe(host, manager).ownership;
|
|
2061
|
-
}
|
|
2062
|
-
function refreshInstalledService(opts = {}, host = defaultServiceHost()) {
|
|
2063
|
-
const manager = detectServiceManager(host);
|
|
2064
|
-
const path = unitPathFor(host, manager);
|
|
2065
|
-
if (!path || !host.fs.exists(path)) return false;
|
|
2066
|
-
const desired = renderFor(host, manager, opts);
|
|
2067
|
-
if (host.fs.read(path) === desired) return false;
|
|
2068
|
-
host.fs.write(path, desired);
|
|
2069
|
-
if (manager === "systemd-user") systemdReload(host);
|
|
2070
|
-
return true;
|
|
2071
|
-
}
|
|
2072
|
-
function stopService(host = defaultServiceHost()) {
|
|
2073
|
-
const manager = detectServiceManager(host);
|
|
2074
|
-
const path = unitPathFor(host, manager);
|
|
2075
|
-
if (manager === "none" || !path || !managerHasClaim(host, manager, path)) {
|
|
2076
|
-
return { handled: false, ok: true };
|
|
2077
|
-
}
|
|
2078
|
-
const res = manager === "launchd" ? launchdStop(host) : systemdStop(host);
|
|
2079
|
-
return {
|
|
2080
|
-
handled: true,
|
|
2081
|
-
ok: res.ok,
|
|
2082
|
-
manager,
|
|
2083
|
-
...res.detail ? { detail: res.detail } : {}
|
|
2084
|
-
};
|
|
2085
|
-
}
|
|
2086
|
-
function disableService(host = defaultServiceHost()) {
|
|
2087
|
-
const manager = detectServiceManager(host);
|
|
2088
|
-
const path = unitPathFor(host, manager);
|
|
2089
|
-
if (manager === "none" || !path) return { handled: false, ok: true };
|
|
2090
|
-
if (!managerHasClaim(host, manager, path)) return { handled: false, ok: true, manager };
|
|
2091
|
-
const res = removeService(host, manager, path);
|
|
2092
|
-
return { handled: true, ok: res.ok, manager, ...res.detail ? { detail: res.detail } : {} };
|
|
2093
|
-
}
|
|
2094
|
-
function managerHasClaim(host, manager, path) {
|
|
2095
|
-
if (manager === "none") return false;
|
|
2096
|
-
if (host.fs.exists(path)) return true;
|
|
2097
|
-
if (manager === "systemd-user" && systemdInstalled(host)) return true;
|
|
2098
|
-
return managerOwnership(host, manager) !== "clear";
|
|
2099
|
-
}
|
|
2100
|
-
function removeService(host, manager, path) {
|
|
2101
|
-
return manager === "launchd" ? launchdRemove(host, path) : systemdRemove(host, path);
|
|
2102
|
-
}
|
|
2103
|
-
function serviceStatus(host = defaultServiceHost()) {
|
|
2104
|
-
const manager = detectServiceManager(host);
|
|
2105
|
-
const path = unitPathFor(host, manager);
|
|
2106
|
-
const probe = managerProbe(host, manager);
|
|
2107
|
-
const installed = manager === "none" || !path ? false : host.fs.exists(path) || probe.ownership === "held" || manager === "systemd-user" && systemdInstalled(host);
|
|
2108
|
-
const running = manager === "none" ? false : probe.running;
|
|
2109
|
-
return {
|
|
2110
|
-
manager,
|
|
2111
|
-
unitPath: path,
|
|
2112
|
-
installed,
|
|
2113
|
-
running,
|
|
2114
|
-
linger: manager === "systemd-user" ? readLinger(host) : null,
|
|
2115
|
-
remoteSession: isRemoteSession(host.env)
|
|
2116
|
-
};
|
|
2117
|
-
}
|
|
2118
|
-
function ensureLinger(host) {
|
|
2119
|
-
const current = readLinger(host);
|
|
2120
|
-
if (current === "yes") return current;
|
|
2121
|
-
return tryEnableLinger(host);
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
// src/commands/service.ts
|
|
2125
|
-
function serviceStatusCommand(deps = {}) {
|
|
2126
|
-
const status2 = (deps.status ?? serviceStatus)();
|
|
2127
|
-
if (status2.manager === "none") {
|
|
2128
|
-
process.stdout.write(
|
|
2129
|
-
"service: not supported on this machine yet \u2014 `cabane-companion start` runs the companion detached instead (it stops at the next reboot).\n"
|
|
2130
|
-
);
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
process.stdout.write(`manager: ${status2.manager}
|
|
2134
|
-
`);
|
|
2135
|
-
process.stdout.write(
|
|
2136
|
-
`unit: ${status2.unitPath}${status2.installed ? "" : " (not written)"}
|
|
2137
|
-
`
|
|
2138
|
-
);
|
|
2139
|
-
process.stdout.write(`installed: ${status2.installed ? "yes" : "no"}
|
|
2140
|
-
`);
|
|
2141
|
-
process.stdout.write(`running: ${runningLine(status2.running)}
|
|
2142
|
-
`);
|
|
2143
|
-
if (status2.manager === "systemd-user") {
|
|
2144
|
-
process.stdout.write(`linger: ${status2.linger}${lingerNote(status2)}
|
|
2145
|
-
`);
|
|
2146
|
-
}
|
|
2147
|
-
if (status2.installed) {
|
|
2148
|
-
process.stdout.write("disable: cabane-companion service disable\n");
|
|
2149
|
-
} else {
|
|
2150
|
-
process.stdout.write("install: cabane-companion start (installs it for you)\n");
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
function serviceDisableCommand(deps = {}) {
|
|
2154
|
-
const disable = deps.disable ?? disableService;
|
|
2155
|
-
const res = disable();
|
|
2156
|
-
if (!res.handled) {
|
|
2157
|
-
process.stdout.write("service: nothing installed (nothing to remove).\n");
|
|
2158
|
-
return;
|
|
2159
|
-
}
|
|
2160
|
-
if (!res.ok) {
|
|
2161
|
-
process.stdout.write(
|
|
2162
|
-
`service: nothing was removed \u2014 ${res.detail ?? "the service manager reported an error"}.
|
|
2163
|
-
The unit is still installed and may still be running; \`cabane-companion service status\` shows where it stands.
|
|
2164
|
-
`
|
|
2165
|
-
);
|
|
2166
|
-
process.exitCode = 1;
|
|
2167
|
-
return;
|
|
2168
|
-
}
|
|
2169
|
-
process.stdout.write(
|
|
2170
|
-
"service: stopped and removed. The companion no longer starts at login; `cabane-companion start` sets it up again.\n"
|
|
2171
|
-
);
|
|
2172
|
-
}
|
|
2173
|
-
function runningLine(running) {
|
|
2174
|
-
if (running === "transitional") {
|
|
2175
|
-
return "starting or stopping \u2014 the manager reported a state in transition; ask again in a moment";
|
|
2176
|
-
}
|
|
2177
|
-
if (running === "unknown") return "unknown \u2014 the service manager didn't answer";
|
|
2178
|
-
return running ? "yes" : "no";
|
|
2179
|
-
}
|
|
2180
|
-
function lingerNote(status2) {
|
|
2181
|
-
if (status2.linger === "yes") return " (survives logout)";
|
|
2182
|
-
if (status2.remoteSession) {
|
|
2183
|
-
return " \u2014 without lingering a --user service stops when this SSH session ends";
|
|
2184
|
-
}
|
|
2185
|
-
return " \u2014 the service runs while you are logged in";
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
// src/commands/start.ts
|
|
2189
|
-
import { hostname as hostname2 } from "os";
|
|
2190
|
-
|
|
2191
1739
|
// src/runtime.ts
|
|
2192
1740
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2193
1741
|
|
|
2194
1742
|
// src/dashboard/server.ts
|
|
2195
|
-
import { dirname as
|
|
2196
|
-
import { fileURLToPath
|
|
1743
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
1744
|
+
import { fileURLToPath } from "url";
|
|
2197
1745
|
import { serve } from "@hono/node-server";
|
|
2198
1746
|
import { Hono } from "hono";
|
|
2199
1747
|
|
|
2200
1748
|
// src/dashboard/routes.ts
|
|
2201
|
-
import { openSync as openSync2, readSync, closeSync as closeSync2, fstatSync, existsSync as
|
|
1749
|
+
import { openSync as openSync2, readSync, closeSync as closeSync2, fstatSync, existsSync as existsSync5 } from "fs";
|
|
2202
1750
|
import { readFile } from "fs/promises";
|
|
2203
|
-
import { extname, join as
|
|
1751
|
+
import { extname, join as join6, normalize } from "path";
|
|
2204
1752
|
import { streamSSE } from "hono/streaming";
|
|
2205
1753
|
|
|
2206
1754
|
// src/state.ts
|
|
@@ -2449,14 +1997,14 @@ var CONTENT_TYPES = {
|
|
|
2449
1997
|
function registerRoutes(app, deps) {
|
|
2450
1998
|
const { supervisor, hub, staticDir } = deps;
|
|
2451
1999
|
app.get("/", async (c) => {
|
|
2452
|
-
const html = await readFile(
|
|
2000
|
+
const html = await readFile(join6(staticDir, "index.html"), "utf8");
|
|
2453
2001
|
return c.html(html);
|
|
2454
2002
|
});
|
|
2455
2003
|
app.get("/static/:file", async (c) => {
|
|
2456
2004
|
const file = c.req.param("file");
|
|
2457
2005
|
const safe3 = normalize(file).replace(/^(\.\.[/\\])+/, "");
|
|
2458
|
-
const full =
|
|
2459
|
-
if (!full.startsWith(staticDir) || !
|
|
2006
|
+
const full = join6(staticDir, safe3);
|
|
2007
|
+
if (!full.startsWith(staticDir) || !existsSync5(full)) return c.notFound();
|
|
2460
2008
|
const body = await readFile(full);
|
|
2461
2009
|
const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
|
|
2462
2010
|
c.header("content-type", type);
|
|
@@ -2583,7 +2131,7 @@ function clampLimit(raw, fallback, max = 200) {
|
|
|
2583
2131
|
return Math.min(Math.floor(n), max);
|
|
2584
2132
|
}
|
|
2585
2133
|
function tailFile(path, lines) {
|
|
2586
|
-
if (!
|
|
2134
|
+
if (!existsSync5(path)) return [];
|
|
2587
2135
|
const MAX_BYTES = 256 * 1024;
|
|
2588
2136
|
let fd;
|
|
2589
2137
|
try {
|
|
@@ -2672,7 +2220,7 @@ function isAddrInUse(err) {
|
|
|
2672
2220
|
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
|
|
2673
2221
|
}
|
|
2674
2222
|
function resolveStaticDir() {
|
|
2675
|
-
return
|
|
2223
|
+
return join7(dirname4(fileURLToPath(import.meta.url)), "static");
|
|
2676
2224
|
}
|
|
2677
2225
|
|
|
2678
2226
|
// src/api.ts
|
|
@@ -3148,21 +2696,21 @@ function errorMessage2(status2, body) {
|
|
|
3148
2696
|
}
|
|
3149
2697
|
|
|
3150
2698
|
// src/cursor.ts
|
|
3151
|
-
import { mkdirSync as
|
|
3152
|
-
import { join as
|
|
2699
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
2700
|
+
import { join as join8 } from "path";
|
|
3153
2701
|
function pathFor(workspaceId) {
|
|
3154
|
-
return
|
|
2702
|
+
return join8(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
3155
2703
|
}
|
|
3156
2704
|
function readCursor(workspaceId) {
|
|
3157
2705
|
const path = pathFor(workspaceId);
|
|
3158
|
-
if (!
|
|
3159
|
-
const raw =
|
|
2706
|
+
if (!existsSync6(path)) return null;
|
|
2707
|
+
const raw = readFileSync4(path, "utf8").trim();
|
|
3160
2708
|
return raw.length > 0 ? raw : null;
|
|
3161
2709
|
}
|
|
3162
2710
|
function writeCursor(workspaceId, eventId) {
|
|
3163
2711
|
const path = pathFor(workspaceId);
|
|
3164
|
-
|
|
3165
|
-
|
|
2712
|
+
mkdirSync6(join8(cabaneDir(), "cursors"), { recursive: true });
|
|
2713
|
+
writeFileSync4(path, eventId + "\n", "utf8");
|
|
3166
2714
|
}
|
|
3167
2715
|
|
|
3168
2716
|
// src/cursor-tracker.ts
|
|
@@ -3205,20 +2753,20 @@ var CursorTracker = class {
|
|
|
3205
2753
|
};
|
|
3206
2754
|
|
|
3207
2755
|
// src/dispatch-dedupe.ts
|
|
3208
|
-
import { mkdirSync as
|
|
3209
|
-
import { join as
|
|
2756
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
|
|
2757
|
+
import { join as join9 } from "path";
|
|
3210
2758
|
var MAX_IDS = 256;
|
|
3211
2759
|
function dir(log) {
|
|
3212
|
-
return
|
|
2760
|
+
return join9(cabaneDir(), log);
|
|
3213
2761
|
}
|
|
3214
2762
|
function pathFor2(log, workspaceId) {
|
|
3215
|
-
return
|
|
2763
|
+
return join9(dir(log), encodeURIComponent(workspaceId));
|
|
3216
2764
|
}
|
|
3217
2765
|
function readIds(log, workspaceId) {
|
|
3218
2766
|
const path = pathFor2(log, workspaceId);
|
|
3219
|
-
if (!
|
|
2767
|
+
if (!existsSync7(path)) return [];
|
|
3220
2768
|
try {
|
|
3221
|
-
return
|
|
2769
|
+
return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3222
2770
|
} catch {
|
|
3223
2771
|
return [];
|
|
3224
2772
|
}
|
|
@@ -3231,8 +2779,8 @@ function mark(log, workspaceId, eventId) {
|
|
|
3231
2779
|
if (ids.includes(eventId)) return;
|
|
3232
2780
|
ids.push(eventId);
|
|
3233
2781
|
const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
|
|
3234
|
-
|
|
3235
|
-
|
|
2782
|
+
mkdirSync7(dir(log), { recursive: true });
|
|
2783
|
+
writeFileSync5(pathFor2(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
|
|
3236
2784
|
}
|
|
3237
2785
|
function hasDispatched(workspaceId, eventId) {
|
|
3238
2786
|
return has("dispatched", workspaceId, eventId);
|
|
@@ -3248,17 +2796,17 @@ function markCompleted(workspaceId, eventId) {
|
|
|
3248
2796
|
}
|
|
3249
2797
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
3250
2798
|
function resumeDir() {
|
|
3251
|
-
return
|
|
2799
|
+
return join9(cabaneDir(), "resume-attempts");
|
|
3252
2800
|
}
|
|
3253
2801
|
function resumePathFor(workspaceId) {
|
|
3254
|
-
return
|
|
2802
|
+
return join9(resumeDir(), encodeURIComponent(workspaceId));
|
|
3255
2803
|
}
|
|
3256
2804
|
function readResumeCounts(workspaceId) {
|
|
3257
2805
|
const out = /* @__PURE__ */ new Map();
|
|
3258
2806
|
const path = resumePathFor(workspaceId);
|
|
3259
|
-
if (!
|
|
2807
|
+
if (!existsSync7(path)) return out;
|
|
3260
2808
|
try {
|
|
3261
|
-
for (const line of
|
|
2809
|
+
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
3262
2810
|
const trimmed = line.trim();
|
|
3263
2811
|
if (!trimmed) continue;
|
|
3264
2812
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -3278,8 +2826,8 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
3278
2826
|
counts.set(eventId, next);
|
|
3279
2827
|
const entries = [...counts.entries()];
|
|
3280
2828
|
const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
|
|
3281
|
-
|
|
3282
|
-
|
|
2829
|
+
mkdirSync7(resumeDir(), { recursive: true });
|
|
2830
|
+
writeFileSync5(
|
|
3283
2831
|
resumePathFor(workspaceId),
|
|
3284
2832
|
trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
|
|
3285
2833
|
"utf8"
|
|
@@ -3451,13 +2999,55 @@ var turnEventSchema = z5.discriminatedUnion("type", [
|
|
|
3451
2999
|
})
|
|
3452
3000
|
]);
|
|
3453
3001
|
|
|
3454
|
-
// packages/agent-runtime/src/
|
|
3002
|
+
// packages/agent-runtime/src/turn-diagnostics.ts
|
|
3455
3003
|
import { z as z6 } from "zod";
|
|
3456
|
-
var
|
|
3004
|
+
var turnResultReasonSchema = z6.discriminatedUnion("kind", [
|
|
3457
3005
|
z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
|
|
3458
3006
|
z6.object({ kind: z6.literal("rate_limited") }),
|
|
3459
3007
|
z6.object({ kind: z6.literal("server_error") }),
|
|
3460
|
-
z6.object({ kind: z6.literal("auth_expired") })
|
|
3008
|
+
z6.object({ kind: z6.literal("auth_expired") }),
|
|
3009
|
+
z6.object({ kind: z6.literal("no_result") }),
|
|
3010
|
+
z6.object({ kind: z6.literal("empty_result") }),
|
|
3011
|
+
z6.object({ kind: z6.literal("empty_result_unverified") }),
|
|
3012
|
+
z6.object({ kind: z6.literal("timeout_idle") }),
|
|
3013
|
+
z6.object({ kind: z6.literal("timeout_total") }),
|
|
3014
|
+
z6.object({ kind: z6.literal("cancelled") }),
|
|
3015
|
+
z6.object({ kind: z6.literal("skipped") }),
|
|
3016
|
+
z6.object({ kind: z6.literal("runtime_error") })
|
|
3017
|
+
]);
|
|
3018
|
+
var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
|
|
3019
|
+
var turnSessionModes = ["fresh", "resumed", "degraded"];
|
|
3020
|
+
var turnRuntimeResultKinds = ["success", "error", "no_terminal"];
|
|
3021
|
+
var turnFinalSources = [
|
|
3022
|
+
"runtime_text",
|
|
3023
|
+
"progress_promotion",
|
|
3024
|
+
"host_fallback",
|
|
3025
|
+
"marker",
|
|
3026
|
+
"none"
|
|
3027
|
+
];
|
|
3028
|
+
var turnDiagnosticsSchema = z6.object({
|
|
3029
|
+
outcome: z6.enum(turnOutcomes),
|
|
3030
|
+
resultReason: turnResultReasonSchema.nullable(),
|
|
3031
|
+
sessionMode: z6.enum(turnSessionModes),
|
|
3032
|
+
sessionFingerprint: z6.string().regex(/^[a-f0-9]{16}$/).nullable(),
|
|
3033
|
+
eventCounts: z6.object({
|
|
3034
|
+
session: z6.number().int().nonnegative(),
|
|
3035
|
+
text: z6.number().int().nonnegative(),
|
|
3036
|
+
thinking: z6.number().int().nonnegative(),
|
|
3037
|
+
tool: z6.number().int().nonnegative(),
|
|
3038
|
+
result: z6.number().int().nonnegative()
|
|
3039
|
+
}),
|
|
3040
|
+
runtimeResultKind: z6.enum(turnRuntimeResultKinds).nullable(),
|
|
3041
|
+
finalSource: z6.enum(turnFinalSources)
|
|
3042
|
+
});
|
|
3043
|
+
|
|
3044
|
+
// packages/agent-runtime/src/failure.ts
|
|
3045
|
+
import { z as z7 } from "zod";
|
|
3046
|
+
var turnFailureSchema = z7.discriminatedUnion("kind", [
|
|
3047
|
+
z7.object({ kind: z7.literal("usage_capped"), resetsAt: z7.string().optional() }),
|
|
3048
|
+
z7.object({ kind: z7.literal("rate_limited") }),
|
|
3049
|
+
z7.object({ kind: z7.literal("server_error") }),
|
|
3050
|
+
z7.object({ kind: z7.literal("auth_expired") })
|
|
3461
3051
|
]);
|
|
3462
3052
|
var USAGE_CAPPED = "usage_capped";
|
|
3463
3053
|
var RATE_LIMITED = "rate_limited";
|
|
@@ -3562,64 +3152,89 @@ var NEGATED_CAP = /not (your|a) usage limit/g;
|
|
|
3562
3152
|
var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
|
|
3563
3153
|
var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
|
|
3564
3154
|
|
|
3155
|
+
// packages/agent-runtime/src/turn-diagnostics-normalize.ts
|
|
3156
|
+
function normalizeTurnResultReason(reason) {
|
|
3157
|
+
const classified = decodeFailureReason(reason);
|
|
3158
|
+
if (classified) return classified;
|
|
3159
|
+
switch (reason) {
|
|
3160
|
+
case "no_result":
|
|
3161
|
+
case "no_terminal":
|
|
3162
|
+
return { kind: "no_result" };
|
|
3163
|
+
case "empty_result":
|
|
3164
|
+
return { kind: "empty_result" };
|
|
3165
|
+
case "empty_result_unverified":
|
|
3166
|
+
return { kind: "empty_result_unverified" };
|
|
3167
|
+
case "timeout_idle":
|
|
3168
|
+
return { kind: "timeout_idle" };
|
|
3169
|
+
case "timeout_total":
|
|
3170
|
+
return { kind: "timeout_total" };
|
|
3171
|
+
case "cancelled":
|
|
3172
|
+
return { kind: "cancelled" };
|
|
3173
|
+
case "skipped":
|
|
3174
|
+
return { kind: "skipped" };
|
|
3175
|
+
default:
|
|
3176
|
+
return { kind: "runtime_error" };
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3565
3180
|
// packages/agent-runtime/src/turn-request.ts
|
|
3566
|
-
import { z as
|
|
3567
|
-
var contentBlockSchema =
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
type:
|
|
3571
|
-
source:
|
|
3181
|
+
import { z as z8 } from "zod";
|
|
3182
|
+
var contentBlockSchema = z8.discriminatedUnion("type", [
|
|
3183
|
+
z8.object({ type: z8.literal("text"), text: z8.string() }),
|
|
3184
|
+
z8.object({
|
|
3185
|
+
type: z8.literal("image"),
|
|
3186
|
+
source: z8.object({ type: z8.literal("url"), url: z8.string() })
|
|
3572
3187
|
}),
|
|
3573
|
-
|
|
3574
|
-
type:
|
|
3575
|
-
source:
|
|
3188
|
+
z8.object({
|
|
3189
|
+
type: z8.literal("document"),
|
|
3190
|
+
source: z8.object({ type: z8.literal("url"), url: z8.string() })
|
|
3576
3191
|
})
|
|
3577
3192
|
]);
|
|
3578
|
-
var effortLevelSchema =
|
|
3579
|
-
var resolvedRunConfigSchema =
|
|
3580
|
-
model:
|
|
3193
|
+
var effortLevelSchema = z8.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
3194
|
+
var resolvedRunConfigSchema = z8.object({
|
|
3195
|
+
model: z8.string().nullable(),
|
|
3581
3196
|
effort: effortLevelSchema.optional(),
|
|
3582
|
-
runtimeOptions:
|
|
3197
|
+
runtimeOptions: z8.record(z8.string(), z8.unknown()).optional()
|
|
3583
3198
|
});
|
|
3584
|
-
var resolvedMcpServerSchema =
|
|
3585
|
-
|
|
3586
|
-
type:
|
|
3587
|
-
command:
|
|
3588
|
-
args:
|
|
3589
|
-
env:
|
|
3199
|
+
var resolvedMcpServerSchema = z8.union([
|
|
3200
|
+
z8.object({
|
|
3201
|
+
type: z8.literal("stdio").optional(),
|
|
3202
|
+
command: z8.string(),
|
|
3203
|
+
args: z8.array(z8.string()).optional(),
|
|
3204
|
+
env: z8.record(z8.string(), z8.string()).optional()
|
|
3590
3205
|
}),
|
|
3591
|
-
|
|
3592
|
-
type:
|
|
3593
|
-
url:
|
|
3594
|
-
headers:
|
|
3206
|
+
z8.object({
|
|
3207
|
+
type: z8.literal("http"),
|
|
3208
|
+
url: z8.string(),
|
|
3209
|
+
headers: z8.record(z8.string(), z8.string()).optional()
|
|
3595
3210
|
}),
|
|
3596
|
-
|
|
3597
|
-
type:
|
|
3598
|
-
url:
|
|
3599
|
-
headers:
|
|
3211
|
+
z8.object({
|
|
3212
|
+
type: z8.literal("sse"),
|
|
3213
|
+
url: z8.string(),
|
|
3214
|
+
headers: z8.record(z8.string(), z8.string()).optional()
|
|
3600
3215
|
})
|
|
3601
3216
|
]);
|
|
3602
|
-
var resolvedMcpServersSchema =
|
|
3603
|
-
var hostInjectedServersSchema =
|
|
3604
|
-
var turnRequestSchema =
|
|
3217
|
+
var resolvedMcpServersSchema = z8.record(z8.string(), resolvedMcpServerSchema);
|
|
3218
|
+
var hostInjectedServersSchema = z8.record(z8.string(), z8.unknown());
|
|
3219
|
+
var turnRequestSchema = z8.object({
|
|
3605
3220
|
// Server-composed system prompt (core + capability prose + adapter addendum +
|
|
3606
3221
|
// charter). One string to the adapter.
|
|
3607
|
-
systemPrompt:
|
|
3222
|
+
systemPrompt: z8.string(),
|
|
3608
3223
|
// Server-composed per-turn user text (anchor reminder + the triggering message).
|
|
3609
|
-
prompt:
|
|
3224
|
+
prompt: z8.string(),
|
|
3610
3225
|
// The multi-block user-message body (text + vision).
|
|
3611
|
-
content:
|
|
3226
|
+
content: z8.array(contentBlockSchema),
|
|
3612
3227
|
// Portable-or-dialect run-config (above).
|
|
3613
3228
|
config: resolvedRunConfigSchema,
|
|
3614
3229
|
// Abstract capability grants; the adapter maps them to tool names.
|
|
3615
3230
|
policy: hostPolicySchema,
|
|
3616
3231
|
// Prior opaque session state, or null for a fresh session.
|
|
3617
|
-
session:
|
|
3232
|
+
session: z8.string().nullable(),
|
|
3618
3233
|
// The cabane control-plane coordinates for this turn's MCP + post-back.
|
|
3619
|
-
cabane:
|
|
3620
|
-
mcpUrl:
|
|
3621
|
-
bearer:
|
|
3622
|
-
activeConversationId:
|
|
3234
|
+
cabane: z8.object({
|
|
3235
|
+
mcpUrl: z8.string(),
|
|
3236
|
+
bearer: z8.string(),
|
|
3237
|
+
activeConversationId: z8.string(),
|
|
3623
3238
|
// CT714: the scoped TURN-CONTROL MCP endpoint (`/api/turn-control`). The
|
|
3624
3239
|
// EXTERNAL adapters (Codex / opencode) mount it by URL under the key
|
|
3625
3240
|
// `cabane_companion` — using the same `bearer` (the turn token) and the same
|
|
@@ -3629,7 +3244,7 @@ var turnRequestSchema = z7.object({
|
|
|
3629
3244
|
// claude-code ignores it (it mounts the in-process instance instead), and
|
|
3630
3245
|
// every existing `cabane`-block fixture keeps parsing unchanged; the
|
|
3631
3246
|
// companion always populates it (`build-options.ts`).
|
|
3632
|
-
turnControlUrl:
|
|
3247
|
+
turnControlUrl: z8.string().optional(),
|
|
3633
3248
|
// CT598: the workspace this turn runs in. The claude-code/opencode/codex
|
|
3634
3249
|
// adapters never need it (they reach Cabane through the `cabane` MCP server,
|
|
3635
3250
|
// which takes `workspaceId` as a per-tool arg the model supplies); the
|
|
@@ -3639,39 +3254,39 @@ var turnRequestSchema = z7.object({
|
|
|
3639
3254
|
// adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
|
|
3640
3255
|
// always populates it (`build-options.ts`), and the native adapter fails the
|
|
3641
3256
|
// turn loudly when it is somehow absent rather than guessing.
|
|
3642
|
-
workspaceId:
|
|
3257
|
+
workspaceId: z8.string().optional(),
|
|
3643
3258
|
// CT752: the server-resolved workspace surface this credential exposes.
|
|
3644
3259
|
// Readiness uses this explicit fact to require `sdk` for code mode and the
|
|
3645
3260
|
// granular floor for classic mode; inventory contents alone cannot infer it
|
|
3646
3261
|
// because `sdk` is intentionally also available on the classic surface.
|
|
3647
|
-
workspaceToolSurface:
|
|
3262
|
+
workspaceToolSurface: z8.enum(["code", "classic"]).optional()
|
|
3648
3263
|
}),
|
|
3649
3264
|
// Machine-local resolution (host-filled): the checkout cwd, extra env from a
|
|
3650
3265
|
// prepare hook, and the resolved user MCP servers.
|
|
3651
|
-
local:
|
|
3652
|
-
cwd:
|
|
3653
|
-
env:
|
|
3266
|
+
local: z8.object({
|
|
3267
|
+
cwd: z8.string().optional(),
|
|
3268
|
+
env: z8.record(z8.string(), z8.string()).optional(),
|
|
3654
3269
|
mcpServers: resolvedMcpServersSchema.optional(),
|
|
3655
3270
|
// CT289: machine-local claude-code adapter knobs the operator sets on a
|
|
3656
3271
|
// companion they run themselves — the auto-memory escape hatch. `autoMemory:
|
|
3657
3272
|
// true` opts back into Claude Code's auto-memory (governed by the operator's
|
|
3658
3273
|
// own `.claude/settings.json`); absent/false leaves the adapter's force-off
|
|
3659
3274
|
// default in place (see `buildClaudeCodeOptions`).
|
|
3660
|
-
claudeCode:
|
|
3275
|
+
claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional()
|
|
3661
3276
|
}),
|
|
3662
3277
|
// Host-owned injected servers (host-filled) — e.g. the summon server.
|
|
3663
|
-
extra:
|
|
3278
|
+
extra: z8.object({
|
|
3664
3279
|
mcpServers: hostInjectedServersSchema
|
|
3665
3280
|
})
|
|
3666
3281
|
});
|
|
3667
3282
|
|
|
3668
3283
|
// packages/agent-runtime/src/conformance.ts
|
|
3669
|
-
import { z as
|
|
3670
|
-
var conformanceFixtureSchema =
|
|
3671
|
-
name:
|
|
3284
|
+
import { z as z9 } from "zod";
|
|
3285
|
+
var conformanceFixtureSchema = z9.object({
|
|
3286
|
+
name: z9.string(),
|
|
3672
3287
|
request: turnRequestSchema,
|
|
3673
|
-
nativeStream:
|
|
3674
|
-
expected:
|
|
3288
|
+
nativeStream: z9.array(z9.unknown()),
|
|
3289
|
+
expected: z9.array(turnEventSchema)
|
|
3675
3290
|
});
|
|
3676
3291
|
|
|
3677
3292
|
// packages/agent-runtime/src/transcript.ts
|
|
@@ -3891,6 +3506,7 @@ var TurnPump = class {
|
|
|
3891
3506
|
if (evt.final) {
|
|
3892
3507
|
this.emittedFinal = true;
|
|
3893
3508
|
this.finalReplyBody = body;
|
|
3509
|
+
this.emittedFinalSource = "runtime_text";
|
|
3894
3510
|
} else {
|
|
3895
3511
|
this.lastProgressBody = body;
|
|
3896
3512
|
}
|
|
@@ -3929,6 +3545,7 @@ var TurnPump = class {
|
|
|
3929
3545
|
lastProgressBody = null;
|
|
3930
3546
|
// The turn's final reply text, captured for the dashboard feed / logging.
|
|
3931
3547
|
finalReplyBody = null;
|
|
3548
|
+
emittedFinalSource = "none";
|
|
3932
3549
|
// CT11: each tool's persisted seq so the `done`/`error` frame republishes the
|
|
3933
3550
|
// same seq the `start` row got (the host's upsert keeps the original seq).
|
|
3934
3551
|
toolSeqByUseId = /* @__PURE__ */ new Map();
|
|
@@ -3951,6 +3568,7 @@ var TurnPump = class {
|
|
|
3951
3568
|
await this.opts.commit.commitMessage({ body, kind: "final", seq });
|
|
3952
3569
|
this.emittedFinal = true;
|
|
3953
3570
|
this.finalReplyBody = body;
|
|
3571
|
+
this.emittedFinalSource = this.lastProgressBody ? "progress_promotion" : "host_fallback";
|
|
3954
3572
|
} catch (err) {
|
|
3955
3573
|
this.opts.onError?.(err, "empty-final");
|
|
3956
3574
|
}
|
|
@@ -3965,6 +3583,9 @@ var TurnPump = class {
|
|
|
3965
3583
|
get replyBody() {
|
|
3966
3584
|
return this.finalReplyBody;
|
|
3967
3585
|
}
|
|
3586
|
+
get finalSource() {
|
|
3587
|
+
return this.emittedFinalSource;
|
|
3588
|
+
}
|
|
3968
3589
|
};
|
|
3969
3590
|
|
|
3970
3591
|
// packages/agent-runtime/src/claude-code/index.ts
|
|
@@ -3986,7 +3607,7 @@ var CLAUDE_CODE_ADDENDUM = [
|
|
|
3986
3607
|
].join(" ");
|
|
3987
3608
|
|
|
3988
3609
|
// packages/agent-runtime/src/claude-code/policy.ts
|
|
3989
|
-
import { z as
|
|
3610
|
+
import { z as z10 } from "zod";
|
|
3990
3611
|
var HOST_FS_TOOLS = [
|
|
3991
3612
|
// shell + local filesystem
|
|
3992
3613
|
"Bash",
|
|
@@ -4036,18 +3657,18 @@ function withThinkingSummaries(thinking) {
|
|
|
4036
3657
|
if (thinking.type === "disabled") return thinking;
|
|
4037
3658
|
return { display: "summarized", ...thinking };
|
|
4038
3659
|
}
|
|
4039
|
-
var claudeCodeDialectSchema =
|
|
4040
|
-
thinking:
|
|
4041
|
-
|
|
4042
|
-
type:
|
|
4043
|
-
display:
|
|
3660
|
+
var claudeCodeDialectSchema = z10.object({
|
|
3661
|
+
thinking: z10.discriminatedUnion("type", [
|
|
3662
|
+
z10.object({
|
|
3663
|
+
type: z10.literal("adaptive"),
|
|
3664
|
+
display: z10.enum(["summarized", "omitted"]).optional()
|
|
4044
3665
|
}),
|
|
4045
|
-
|
|
4046
|
-
type:
|
|
4047
|
-
budgetTokens:
|
|
4048
|
-
display:
|
|
3666
|
+
z10.object({
|
|
3667
|
+
type: z10.literal("enabled"),
|
|
3668
|
+
budgetTokens: z10.number().int().positive().optional(),
|
|
3669
|
+
display: z10.enum(["summarized", "omitted"]).optional()
|
|
4049
3670
|
}),
|
|
4050
|
-
|
|
3671
|
+
z10.object({ type: z10.literal("disabled") })
|
|
4051
3672
|
]).optional()
|
|
4052
3673
|
}).loose();
|
|
4053
3674
|
function readThinking(runtimeOptions) {
|
|
@@ -4989,7 +4610,7 @@ function sealHeld(held, terminal) {
|
|
|
4989
4610
|
}
|
|
4990
4611
|
|
|
4991
4612
|
// packages/agent-runtime/src/opencode/policy.ts
|
|
4992
|
-
import { z as
|
|
4613
|
+
import { z as z11 } from "zod";
|
|
4993
4614
|
var OPENCODE_HOST_TOOLS = [
|
|
4994
4615
|
"bash",
|
|
4995
4616
|
"edit",
|
|
@@ -5016,8 +4637,8 @@ function opencodeToolPolicy(policy) {
|
|
|
5016
4637
|
deny(OPENCODE_UI_PROMPT_TOOLS);
|
|
5017
4638
|
return { tools, allowAllHostTools: policy.hostFs };
|
|
5018
4639
|
}
|
|
5019
|
-
var opencodeDialectSchema =
|
|
5020
|
-
agent:
|
|
4640
|
+
var opencodeDialectSchema = z11.object({
|
|
4641
|
+
agent: z11.string().min(1).optional()
|
|
5021
4642
|
}).loose();
|
|
5022
4643
|
function readOpencodeDialect(runtimeOptions) {
|
|
5023
4644
|
const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
|
|
@@ -6027,7 +5648,7 @@ function sealHeld2(held, terminal) {
|
|
|
6027
5648
|
}
|
|
6028
5649
|
|
|
6029
5650
|
// packages/agent-runtime/src/codex/policy.ts
|
|
6030
|
-
import { z as
|
|
5651
|
+
import { z as z12 } from "zod";
|
|
6031
5652
|
function codexToolPolicy(policy) {
|
|
6032
5653
|
return policy.hostFs ? {
|
|
6033
5654
|
sandboxMode: "danger-full-access",
|
|
@@ -6046,8 +5667,8 @@ function codexToolPolicy(policy) {
|
|
|
6046
5667
|
};
|
|
6047
5668
|
}
|
|
6048
5669
|
var CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
|
|
6049
|
-
var codexDialectSchema =
|
|
6050
|
-
modelReasoningEffort:
|
|
5670
|
+
var codexDialectSchema = z12.object({
|
|
5671
|
+
modelReasoningEffort: z12.enum(CODEX_REASONING_EFFORTS).optional()
|
|
6051
5672
|
}).loose();
|
|
6052
5673
|
function readCodexDialect(runtimeOptions) {
|
|
6053
5674
|
const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
|
|
@@ -6976,12 +6597,12 @@ var ConnectorHealthStore = class {
|
|
|
6976
6597
|
};
|
|
6977
6598
|
|
|
6978
6599
|
// src/dispatcher.ts
|
|
6979
|
-
import { randomUUID } from "crypto";
|
|
6980
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
6981
|
-
import { join as
|
|
6600
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
6601
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync10, readdirSync as readdirSync2, statSync } from "fs";
|
|
6602
|
+
import { join as join14 } from "path";
|
|
6982
6603
|
|
|
6983
6604
|
// src/summon.ts
|
|
6984
|
-
import { z as
|
|
6605
|
+
import { z as z13 } from "zod";
|
|
6985
6606
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
6986
6607
|
var SUMMON_AGENT_TOOL = "summon_agent";
|
|
6987
6608
|
var SUMMON_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUMMON_AGENT_TOOL}`;
|
|
@@ -7029,7 +6650,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7029
6650
|
SUMMON_AGENT_TOOL,
|
|
7030
6651
|
"Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Use it to hand part of the work to a teammate, or pull in an expert, without leaving the conversation. Pass the peer's `agentId` \u2014 every agent's handle and id is on the roster in your turn context. The peer is dispatched on your turn's final reply, so write the context/ask into that reply first \u2014 it receives your message + this conversation to work from. Writing `@handle` in your prose does NOT summon anyone (agent prose never dispatches); this tool is the only in-thread lever. Single target \u2014 the last call wins. Summoning yourself is a no-op. Reach for it when the human wants the peer's answer right HERE, in front of them \u2014 the reply lands in this thread, so there's no return to wire (a return is for work YOU consume, never a courtesy notification). A handoff to a DIFFERENT conversation is `cabane.conversations.create` / `cabane.conversations.post` with their `dispatch` field instead.",
|
|
7031
6652
|
{
|
|
7032
|
-
agentId:
|
|
6653
|
+
agentId: z13.string().uuid().describe(
|
|
7033
6654
|
"The peer agent to summon \u2014 a workspace agent id, from your turn context's roster."
|
|
7034
6655
|
)
|
|
7035
6656
|
},
|
|
@@ -7046,7 +6667,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7046
6667
|
SKIP_TURN_TOOL,
|
|
7047
6668
|
`End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or a pile-on where someone else has it. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
|
|
7048
6669
|
{
|
|
7049
|
-
reason:
|
|
6670
|
+
reason: z13.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
|
|
7050
6671
|
},
|
|
7051
6672
|
async (args) => {
|
|
7052
6673
|
skipState.skipped = true;
|
|
@@ -7063,23 +6684,23 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7063
6684
|
ASK_TOOL,
|
|
7064
6685
|
"Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass `targetUserId` (a workspace member's user id \u2014 every person's id is on the roster in your turn context). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label or a whole sentence. Provide EITHER `question` (single) or `questions` (array), never both. The ask is a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
|
|
7065
6686
|
{
|
|
7066
|
-
targetUserId:
|
|
6687
|
+
targetUserId: z13.string().uuid().describe(
|
|
7067
6688
|
"The workspace member (human) to ask \u2014 a user id, from your turn context's roster."
|
|
7068
6689
|
),
|
|
7069
|
-
question:
|
|
6690
|
+
question: z13.string().min(1).max(400).optional().describe(
|
|
7070
6691
|
"SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (capped, inline markdown only). Provide EITHER this or `questions`, not both. Put the crisp one-sentence question in `headline`."
|
|
7071
6692
|
),
|
|
7072
|
-
headline:
|
|
6693
|
+
headline: z13.string().min(1).max(120).optional().describe(
|
|
7073
6694
|
'SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in `?` ("Do we go to prod?"). What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in `question`). Strongly encouraged.'
|
|
7074
6695
|
),
|
|
7075
|
-
options:
|
|
7076
|
-
questions:
|
|
7077
|
-
|
|
7078
|
-
headline:
|
|
6696
|
+
options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
|
|
6697
|
+
questions: z13.array(
|
|
6698
|
+
z13.object({
|
|
6699
|
+
headline: z13.string().min(1).max(120).describe(
|
|
7079
6700
|
'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
|
|
7080
6701
|
),
|
|
7081
|
-
body:
|
|
7082
|
-
options:
|
|
6702
|
+
body: z13.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
|
|
6703
|
+
options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
|
|
7083
6704
|
})
|
|
7084
6705
|
).min(1).max(5).optional().describe(
|
|
7085
6706
|
"MULTI-question form: 1\u20135 questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or `question`/`headline`/`options`, not both."
|
|
@@ -7136,13 +6757,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7136
6757
|
SUB_AGENT_TOOL,
|
|
7137
6758
|
"Spawn a sub-agent \u2014 hand a piece of work to a private worker with a fresh context window, whose result comes back to you automatically. Modeled on the Task tool, with ONE deliberate difference: it does NOT return the result inline. A callee's turn can run for minutes and no turn may hold an unbounded wait, so the shape is spawn-now, results-on-wake \u2014 this returns immediately with the child's `conversationId`, and the outcome lands LATER as a message in THIS conversation; you're woken once every sub-agent you have out in this conversation has returned. So DON'T wait for it: after spawning, finish whatever else this turn can do and end your turn (never poll the child with reads in a loop \u2014 the wake is automatic). Parallel fan-out = call this N times in one turn (they run concurrently; ONE wake when all are in); series = one call per turn. `prompt` is the child's opening instruction \u2014 make it self-contained (the sub-agent starts fresh, with only this prompt + the thread it lands in). `agentId` (optional) dispatches a PEER instead of yourself \u2014 same mechanics, a different mind (use for capability/context you lack); default (self) is the pure sub-worker with a clean context window. `title` (optional) names the child thread (results link it, so a legible title helps). A single sub-agent has no wall-clock advantage (you idle either way) \u2014 it pays when the callee has capability/context you lack, or to isolate a big read from your own session; the real win is fan-out. Don't spawn one for a lookup you can do in-turn with your own tools. The result returns to YOU to act on \u2014 reach for it when you're the consumer of the output, not as a way to notify a human: if a person just wants to read the result, dispatch a plain (no-return) conversation and link it instead of spawning a sub-agent.",
|
|
7138
6759
|
{
|
|
7139
|
-
prompt:
|
|
6760
|
+
prompt: z13.string().min(1).max(65536).describe(
|
|
7140
6761
|
"The sub-agent's opening instruction \u2014 self-contained (it starts with a fresh context window; only this prompt + the thread it lands in)."
|
|
7141
6762
|
),
|
|
7142
|
-
agentId:
|
|
6763
|
+
agentId: z13.string().uuid().optional().describe(
|
|
7143
6764
|
"Optional peer to run the sub-agent as (a workspace agent id, from your turn context's roster); omit to spawn yourself with a fresh context window."
|
|
7144
6765
|
),
|
|
7145
|
-
title:
|
|
6766
|
+
title: z13.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
|
|
7146
6767
|
},
|
|
7147
6768
|
async (args) => {
|
|
7148
6769
|
const result = await subAgentCreate(args);
|
|
@@ -7172,13 +6793,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
7172
6793
|
WAKE_ME_TOOL,
|
|
7173
6794
|
"Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for \"wait until X\": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like \"tomorrow morning\"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check (\"check whether CT441 merged yet\"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an `ask` to the human instead. If a wake can't be armed you're re-dispatched with a note explaining why \u2014 never a silent drop.",
|
|
7174
6795
|
{
|
|
7175
|
-
afterSeconds:
|
|
6796
|
+
afterSeconds: z13.number().int().positive().optional().describe(
|
|
7176
6797
|
"Relative delay in seconds from when this turn ends (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side."
|
|
7177
6798
|
),
|
|
7178
|
-
at:
|
|
6799
|
+
at: z13.string().datetime({ offset: true }).optional().describe(
|
|
7179
6800
|
"Absolute ISO-8601 timestamp WITH a zone (`Z` or `\xB1HH:MM`), e.g. `2026-07-16T09:00:00-07:00`. YOU compute it from a natural-language phrase using the current datetime in your turn context. Provide EITHER this or `afterSeconds`, not both."
|
|
7180
6801
|
),
|
|
7181
|
-
note:
|
|
6802
|
+
note: z13.string().min(1).max(2e3).describe(
|
|
7182
6803
|
'A note to your future self \u2014 becomes the body of the wake message that re-dispatches you. Write the condition to re-check ("check whether the PR merged").'
|
|
7183
6804
|
)
|
|
7184
6805
|
},
|
|
@@ -7311,11 +6932,11 @@ function trimSlash2(s) {
|
|
|
7311
6932
|
// src/codex-instructions.ts
|
|
7312
6933
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
7313
6934
|
import { tmpdir } from "os";
|
|
7314
|
-
import { join as
|
|
6935
|
+
import { join as join10 } from "path";
|
|
7315
6936
|
var PREFIX = "cabane-codex-instructions-";
|
|
7316
6937
|
async function writeCodexInstructionsFile(contents) {
|
|
7317
|
-
const dir2 = await mkdtemp(
|
|
7318
|
-
const path =
|
|
6938
|
+
const dir2 = await mkdtemp(join10(tmpdir(), PREFIX));
|
|
6939
|
+
const path = join10(dir2, "instructions.md");
|
|
7319
6940
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
7320
6941
|
return {
|
|
7321
6942
|
path,
|
|
@@ -7326,22 +6947,22 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
7326
6947
|
}
|
|
7327
6948
|
|
|
7328
6949
|
// src/prepared.ts
|
|
7329
|
-
import { mkdirSync as
|
|
7330
|
-
import { join as
|
|
6950
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
6951
|
+
import { join as join11 } from "path";
|
|
7331
6952
|
function dirFor(workspaceId) {
|
|
7332
|
-
return
|
|
6953
|
+
return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
7333
6954
|
}
|
|
7334
6955
|
function conversationDir(workspaceId, conversationId) {
|
|
7335
|
-
return
|
|
6956
|
+
return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
7336
6957
|
}
|
|
7337
6958
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
7338
|
-
return
|
|
6959
|
+
return join11(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
7339
6960
|
}
|
|
7340
6961
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
7341
6962
|
const path = pathFor3(workspaceId, conversationId, agentId);
|
|
7342
|
-
if (!
|
|
6963
|
+
if (!existsSync8(path)) return null;
|
|
7343
6964
|
try {
|
|
7344
|
-
const parsed = JSON.parse(
|
|
6965
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
7345
6966
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
7346
6967
|
return {
|
|
7347
6968
|
cwd: parsed.cwd,
|
|
@@ -7354,32 +6975,32 @@ function readPrepared(workspaceId, conversationId, agentId) {
|
|
|
7354
6975
|
}
|
|
7355
6976
|
}
|
|
7356
6977
|
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
7357
|
-
|
|
7358
|
-
|
|
6978
|
+
mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
6979
|
+
writeFileSync6(
|
|
7359
6980
|
pathFor3(workspaceId, conversationId, agentId),
|
|
7360
6981
|
JSON.stringify(result) + "\n",
|
|
7361
6982
|
"utf8"
|
|
7362
6983
|
);
|
|
7363
6984
|
}
|
|
7364
6985
|
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
7365
|
-
|
|
6986
|
+
rmSync5(pathFor3(workspaceId, conversationId, agentId), { force: true });
|
|
7366
6987
|
}
|
|
7367
6988
|
|
|
7368
6989
|
// src/secrets.ts
|
|
7369
|
-
import { existsSync as
|
|
7370
|
-
import { join as
|
|
7371
|
-
import { z as
|
|
6990
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
6991
|
+
import { join as join12 } from "path";
|
|
6992
|
+
import { z as z14 } from "zod";
|
|
7372
6993
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7373
6994
|
function secretsPath() {
|
|
7374
|
-
return
|
|
6995
|
+
return join12(cabaneDir(), "secrets.json");
|
|
7375
6996
|
}
|
|
7376
|
-
var secretStoreSchema =
|
|
6997
|
+
var secretStoreSchema = z14.record(z14.string(), z14.string());
|
|
7377
6998
|
function loadSecretStore() {
|
|
7378
6999
|
const path = secretsPath();
|
|
7379
|
-
if (!
|
|
7000
|
+
if (!existsSync9(path)) return makeStore({});
|
|
7380
7001
|
let raw;
|
|
7381
7002
|
try {
|
|
7382
|
-
raw =
|
|
7003
|
+
raw = readFileSync7(path, "utf8");
|
|
7383
7004
|
} catch (err) {
|
|
7384
7005
|
throw new ConfigError(
|
|
7385
7006
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -7457,21 +7078,22 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
7457
7078
|
}
|
|
7458
7079
|
|
|
7459
7080
|
// src/transcript-writer.ts
|
|
7460
|
-
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as
|
|
7461
|
-
import { join as
|
|
7081
|
+
import { appendFileSync, chmodSync as chmodSync3, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
|
|
7082
|
+
import { basename, dirname as dirname5, join as join13 } from "path";
|
|
7462
7083
|
function transcriptsDir() {
|
|
7463
|
-
return
|
|
7084
|
+
return join13(cabaneDir(), "transcripts");
|
|
7464
7085
|
}
|
|
7465
7086
|
var RETAIN = 200;
|
|
7087
|
+
var ANOMALY_RETAIN = 50;
|
|
7466
7088
|
var TranscriptWriter = class {
|
|
7467
7089
|
path;
|
|
7468
7090
|
broken = false;
|
|
7469
7091
|
onWarn;
|
|
7470
7092
|
constructor(dir2, meta, onWarn) {
|
|
7471
7093
|
this.onWarn = onWarn;
|
|
7472
|
-
this.path =
|
|
7094
|
+
this.path = join13(dir2, fileName(meta));
|
|
7473
7095
|
try {
|
|
7474
|
-
|
|
7096
|
+
mkdirSync9(dir2, { recursive: true });
|
|
7475
7097
|
try {
|
|
7476
7098
|
chmodSync3(dir2, 448);
|
|
7477
7099
|
} catch {
|
|
@@ -7490,6 +7112,25 @@ var TranscriptWriter = class {
|
|
|
7490
7112
|
close(outcome) {
|
|
7491
7113
|
this.line({ type: "_outcome", ...outcome });
|
|
7492
7114
|
}
|
|
7115
|
+
// CT1145: keep an independent copy of an empty successful envelope after the
|
|
7116
|
+
// footer has landed. The ordinary 200-file FIFO keeps rotating as before;
|
|
7117
|
+
// this bucket retains the newest 50 anomalies regardless of ordinary traffic.
|
|
7118
|
+
// Best-effort like every other transcript operation: observability can never
|
|
7119
|
+
// fail the turn.
|
|
7120
|
+
preserveAnomaly() {
|
|
7121
|
+
if (this.broken) return;
|
|
7122
|
+
try {
|
|
7123
|
+
const dir2 = join13(dirname5(this.path), "anomalies");
|
|
7124
|
+
mkdirSync9(dir2, { recursive: true, mode: 448 });
|
|
7125
|
+
chmodSync3(dir2, 448);
|
|
7126
|
+
const target = join13(dir2, basename(this.path));
|
|
7127
|
+
copyFileSync(this.path, target);
|
|
7128
|
+
chmodSync3(target, 384);
|
|
7129
|
+
pruneOld(dir2, ANOMALY_RETAIN);
|
|
7130
|
+
} catch (err) {
|
|
7131
|
+
this.fail(err);
|
|
7132
|
+
}
|
|
7133
|
+
}
|
|
7493
7134
|
line(obj) {
|
|
7494
7135
|
if (this.broken) return;
|
|
7495
7136
|
try {
|
|
@@ -7528,7 +7169,7 @@ function pruneOld(dir2, retain) {
|
|
|
7528
7169
|
const drop = files.sort().slice(0, files.length - retain);
|
|
7529
7170
|
for (const f of drop) {
|
|
7530
7171
|
try {
|
|
7531
|
-
|
|
7172
|
+
rmSync6(join13(dir2, f), { force: true });
|
|
7532
7173
|
} catch {
|
|
7533
7174
|
}
|
|
7534
7175
|
}
|
|
@@ -7647,13 +7288,16 @@ var TurnCommitter = class {
|
|
|
7647
7288
|
get replyBody() {
|
|
7648
7289
|
return this.pump.replyBody;
|
|
7649
7290
|
}
|
|
7291
|
+
get finalSource() {
|
|
7292
|
+
return this.pump.finalSource;
|
|
7293
|
+
}
|
|
7650
7294
|
// CT183: resolve the in-thread summon into the `dispatch` field for a `final`
|
|
7651
7295
|
// commit. A self-target is stripped here (mirror of the in-app self-strip); the
|
|
7652
7296
|
// server strips it again and resolves / ignores an unknown id.
|
|
7653
7297
|
summonField() {
|
|
7654
|
-
const
|
|
7655
|
-
if (!
|
|
7656
|
-
return { dispatch:
|
|
7298
|
+
const target = this.deps.summonState.agentId;
|
|
7299
|
+
if (!target || target === this.deps.agentId) return {};
|
|
7300
|
+
return { dispatch: target };
|
|
7657
7301
|
}
|
|
7658
7302
|
// CT326: resolve the per-turn ask into the `ask` field for a `final` commit.
|
|
7659
7303
|
// The server validates the target (must be a workspace member/owner) and
|
|
@@ -7874,7 +7518,7 @@ var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
|
7874
7518
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
7875
7519
|
function checkoutState(cwd) {
|
|
7876
7520
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
7877
|
-
if (!
|
|
7521
|
+
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
7878
7522
|
let entries;
|
|
7879
7523
|
try {
|
|
7880
7524
|
entries = readdirSync2(cwd);
|
|
@@ -7884,15 +7528,15 @@ function checkoutState(cwd) {
|
|
|
7884
7528
|
if (entries.length === 0) {
|
|
7885
7529
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
7886
7530
|
}
|
|
7887
|
-
const gitPath =
|
|
7888
|
-
if (!
|
|
7531
|
+
const gitPath = join14(cwd, ".git");
|
|
7532
|
+
if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
|
|
7889
7533
|
let stat;
|
|
7890
7534
|
try {
|
|
7891
7535
|
stat = statSync(gitPath);
|
|
7892
7536
|
} catch (error) {
|
|
7893
7537
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
7894
7538
|
}
|
|
7895
|
-
if (stat.isDirectory() && !
|
|
7539
|
+
if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
|
|
7896
7540
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
7897
7541
|
return { ok: true, reason: "usable" };
|
|
7898
7542
|
}
|
|
@@ -8083,7 +7727,7 @@ var Dispatcher = class {
|
|
|
8083
7727
|
let seqCounter = 0;
|
|
8084
7728
|
const nextSeq = () => ++seqCounter;
|
|
8085
7729
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
8086
|
-
if (effectiveCwd && !
|
|
7730
|
+
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
8087
7731
|
turnLog.warn(
|
|
8088
7732
|
{ cwd: effectiveCwd },
|
|
8089
7733
|
"dispatcher: configured working directory does not exist on this device \u2014 falling back to the process cwd"
|
|
@@ -8347,11 +7991,11 @@ ${reason}`,
|
|
|
8347
7991
|
const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
|
|
8348
7992
|
const closeTurnReceipt = (ok, reason) => {
|
|
8349
7993
|
if (!turnReceiptPath) return;
|
|
8350
|
-
const
|
|
7994
|
+
const target = turnReceiptPath;
|
|
8351
7995
|
turnReceiptPath = null;
|
|
8352
7996
|
try {
|
|
8353
7997
|
appendFileSync2(
|
|
8354
|
-
|
|
7998
|
+
target,
|
|
8355
7999
|
`${JSON.stringify({
|
|
8356
8000
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8357
8001
|
event: "settled",
|
|
@@ -8391,7 +8035,7 @@ ${reason}`,
|
|
|
8391
8035
|
}
|
|
8392
8036
|
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
8393
8037
|
}
|
|
8394
|
-
const receiptPath =
|
|
8038
|
+
const receiptPath = join14(effectiveCwd, ".git", "cabane", "readiness.jsonl");
|
|
8395
8039
|
const receiptLine = (fields) => `${JSON.stringify({
|
|
8396
8040
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8397
8041
|
taskId: hookEnv.CABANE_TASK_ID,
|
|
@@ -8415,7 +8059,7 @@ ${reason}`,
|
|
|
8415
8059
|
})}
|
|
8416
8060
|
`;
|
|
8417
8061
|
try {
|
|
8418
|
-
|
|
8062
|
+
mkdirSync10(join14(effectiveCwd, ".git", "cabane"), { recursive: true });
|
|
8419
8063
|
appendFileSync2(
|
|
8420
8064
|
receiptPath,
|
|
8421
8065
|
// `starting` is the honest classification before the proof has run. The
|
|
@@ -8510,6 +8154,16 @@ ${reason}`,
|
|
|
8510
8154
|
let turnUsage;
|
|
8511
8155
|
let turnResolvedModel;
|
|
8512
8156
|
let turnResolvedConfig;
|
|
8157
|
+
const eventCounts = {
|
|
8158
|
+
session: 0,
|
|
8159
|
+
text: 0,
|
|
8160
|
+
thinking: 0,
|
|
8161
|
+
tool: 0,
|
|
8162
|
+
result: 0
|
|
8163
|
+
};
|
|
8164
|
+
let runtimeResultKind = null;
|
|
8165
|
+
let latestSessionState = request.session;
|
|
8166
|
+
let settledDiagnostics = null;
|
|
8513
8167
|
const committer = new TurnCommitter({
|
|
8514
8168
|
api: this.opts.api,
|
|
8515
8169
|
workspaceId,
|
|
@@ -8605,6 +8259,7 @@ ${reason}`,
|
|
|
8605
8259
|
try {
|
|
8606
8260
|
for await (const event of adapter.runTurn(request, abortController.signal)) {
|
|
8607
8261
|
transcript2?.write(event);
|
|
8262
|
+
eventCounts[event.type] += 1;
|
|
8608
8263
|
armIdle();
|
|
8609
8264
|
if (abortController.signal.aborted) {
|
|
8610
8265
|
turnLog.info("dispatcher: aborted mid-turn");
|
|
@@ -8613,6 +8268,7 @@ ${reason}`,
|
|
|
8613
8268
|
break;
|
|
8614
8269
|
}
|
|
8615
8270
|
if (event.type === "session") {
|
|
8271
|
+
latestSessionState = event.state;
|
|
8616
8272
|
if (event.degraded) sessionDegraded = true;
|
|
8617
8273
|
if (!sessionWritten) {
|
|
8618
8274
|
sessionWritten = true;
|
|
@@ -8651,6 +8307,7 @@ ${reason}`,
|
|
|
8651
8307
|
turnUsage = event.usage;
|
|
8652
8308
|
turnResolvedModel = event.resolvedModel;
|
|
8653
8309
|
turnResolvedConfig = event.resolvedConfig;
|
|
8310
|
+
runtimeResultKind = event.ok ? "success" : event.reason === "no_terminal" ? "no_terminal" : "error";
|
|
8654
8311
|
} else if (event.type === "text" && skipState.skipped) {
|
|
8655
8312
|
} else {
|
|
8656
8313
|
if (event.type === "text" && event.terminal) {
|
|
@@ -8773,6 +8430,34 @@ ${reason}`,
|
|
|
8773
8430
|
body.sessionWriteRejected = true;
|
|
8774
8431
|
this.sessionWriteNotified.add(key);
|
|
8775
8432
|
}
|
|
8433
|
+
const outcome = skipState.skipped ? "skipped" : userCancelled ? "cancelled" : okResult ? "success" : "failure";
|
|
8434
|
+
const noContentEvents = eventCounts.text === 0 && eventCounts.thinking === 0 && eventCounts.tool === 0;
|
|
8435
|
+
const emptySuccessfulEnvelope = outcome === "success" && eventCounts.result > 0 && noContentEvents;
|
|
8436
|
+
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? { kind: "cancelled" } : emptySuccessfulEnvelope ? turnUsage?.inputTokens === 0 && turnUsage.outputTokens === 0 ? { kind: "empty_result" } : { kind: "empty_result_unverified" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
|
|
8437
|
+
settledDiagnostics = {
|
|
8438
|
+
outcome,
|
|
8439
|
+
resultReason: diagnosticReason,
|
|
8440
|
+
sessionMode: sessionDegraded ? "degraded" : request.session ? "resumed" : "fresh",
|
|
8441
|
+
sessionFingerprint: fingerprintSessionState(latestSessionState),
|
|
8442
|
+
eventCounts,
|
|
8443
|
+
runtimeResultKind,
|
|
8444
|
+
finalSource: outcome === "skipped" || outcome === "cancelled" ? "marker" : committer.finalSource
|
|
8445
|
+
};
|
|
8446
|
+
body.diagnostics = settledDiagnostics;
|
|
8447
|
+
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
|
8448
|
+
diagnosticReason.kind
|
|
8449
|
+
)) {
|
|
8450
|
+
turnLog.warn(
|
|
8451
|
+
{
|
|
8452
|
+
turnId,
|
|
8453
|
+
conversationId: payload.conversationId,
|
|
8454
|
+
agentId: payload.agentId,
|
|
8455
|
+
runtime: turnRuntime,
|
|
8456
|
+
diagnostics: settledDiagnostics
|
|
8457
|
+
},
|
|
8458
|
+
"dispatcher: anomalous turn settled"
|
|
8459
|
+
);
|
|
8460
|
+
}
|
|
8776
8461
|
this.opts.connectorHealth?.recordSettle(turnRuntime, {
|
|
8777
8462
|
ok: okResult,
|
|
8778
8463
|
errorReason: body.errorReason ?? null
|
|
@@ -8805,6 +8490,9 @@ ${reason}`,
|
|
|
8805
8490
|
if (!okResult && resultReason !== "cancelled") {
|
|
8806
8491
|
turnLog.info(`turn failed \u2014 full transcript: ${transcript2.path}`);
|
|
8807
8492
|
}
|
|
8493
|
+
if (settledDiagnostics?.resultReason?.kind === "empty_result" || settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
|
|
8494
|
+
transcript2.preserveAnomaly();
|
|
8495
|
+
}
|
|
8808
8496
|
}
|
|
8809
8497
|
if (okResult) {
|
|
8810
8498
|
turnLog.debug({ durationMs }, "dispatcher: turn end (ok)");
|
|
@@ -8842,6 +8530,17 @@ ${reason}`,
|
|
|
8842
8530
|
return true;
|
|
8843
8531
|
}
|
|
8844
8532
|
};
|
|
8533
|
+
function fingerprintSessionState(state) {
|
|
8534
|
+
if (!state) return null;
|
|
8535
|
+
let opaqueId = state;
|
|
8536
|
+
try {
|
|
8537
|
+
const parsed = JSON.parse(state);
|
|
8538
|
+
const candidate = parsed.sdkSessionId ?? parsed.threadId ?? parsed.sessionId;
|
|
8539
|
+
if (typeof candidate === "string" && candidate.length > 0) opaqueId = candidate;
|
|
8540
|
+
} catch {
|
|
8541
|
+
}
|
|
8542
|
+
return createHash2("sha256").update(opaqueId).digest("hex").slice(0, 16);
|
|
8543
|
+
}
|
|
8845
8544
|
|
|
8846
8545
|
// src/opencode-models.ts
|
|
8847
8546
|
var OPENCODE_RUNTIME = "opencode";
|
|
@@ -8886,15 +8585,15 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
8886
8585
|
|
|
8887
8586
|
// src/outbox.ts
|
|
8888
8587
|
import {
|
|
8889
|
-
existsSync as
|
|
8890
|
-
mkdirSync as
|
|
8588
|
+
existsSync as existsSync11,
|
|
8589
|
+
mkdirSync as mkdirSync11,
|
|
8891
8590
|
readdirSync as readdirSync3,
|
|
8892
|
-
readFileSync as
|
|
8591
|
+
readFileSync as readFileSync8,
|
|
8893
8592
|
renameSync as renameSync3,
|
|
8894
|
-
rmSync as
|
|
8895
|
-
writeFileSync as
|
|
8593
|
+
rmSync as rmSync7,
|
|
8594
|
+
writeFileSync as writeFileSync7
|
|
8896
8595
|
} from "fs";
|
|
8897
|
-
import { join as
|
|
8596
|
+
import { join as join15 } from "path";
|
|
8898
8597
|
var MAX_ENTRIES = 2e3;
|
|
8899
8598
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
8900
8599
|
var Outbox = class {
|
|
@@ -8907,25 +8606,25 @@ var Outbox = class {
|
|
|
8907
8606
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
8908
8607
|
// cases route writes at the right tmpdir.
|
|
8909
8608
|
dir() {
|
|
8910
|
-
return
|
|
8609
|
+
return join15(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
8911
8610
|
}
|
|
8912
8611
|
fileFor(turnId, seq) {
|
|
8913
|
-
return
|
|
8612
|
+
return join15(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
8914
8613
|
}
|
|
8915
8614
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
8916
8615
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
8917
8616
|
// per-workspace bounds.
|
|
8918
8617
|
persist(entry) {
|
|
8919
8618
|
const dir2 = this.dir();
|
|
8920
|
-
|
|
8921
|
-
const
|
|
8922
|
-
const tmp = `${
|
|
8619
|
+
mkdirSync11(dir2, { recursive: true });
|
|
8620
|
+
const target = this.fileFor(entry.turnId, entry.seq);
|
|
8621
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
8923
8622
|
try {
|
|
8924
|
-
|
|
8925
|
-
renameSync3(tmp,
|
|
8623
|
+
writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
8624
|
+
renameSync3(tmp, target);
|
|
8926
8625
|
} catch (err) {
|
|
8927
8626
|
try {
|
|
8928
|
-
|
|
8627
|
+
rmSync7(tmp, { force: true });
|
|
8929
8628
|
} catch {
|
|
8930
8629
|
}
|
|
8931
8630
|
this.log?.warn(
|
|
@@ -8942,7 +8641,7 @@ var Outbox = class {
|
|
|
8942
8641
|
// wedging the drain.
|
|
8943
8642
|
list() {
|
|
8944
8643
|
const dir2 = this.dir();
|
|
8945
|
-
if (!
|
|
8644
|
+
if (!existsSync11(dir2)) return [];
|
|
8946
8645
|
let names;
|
|
8947
8646
|
try {
|
|
8948
8647
|
names = readdirSync3(dir2);
|
|
@@ -8952,9 +8651,9 @@ var Outbox = class {
|
|
|
8952
8651
|
const entries = [];
|
|
8953
8652
|
for (const name of names) {
|
|
8954
8653
|
if (!name.endsWith(".json")) continue;
|
|
8955
|
-
const full =
|
|
8654
|
+
const full = join15(dir2, name);
|
|
8956
8655
|
try {
|
|
8957
|
-
const parsed = JSON.parse(
|
|
8656
|
+
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
8958
8657
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
8959
8658
|
entries.push(parsed);
|
|
8960
8659
|
} else {
|
|
@@ -8972,13 +8671,13 @@ var Outbox = class {
|
|
|
8972
8671
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
8973
8672
|
remove(turnId, seq) {
|
|
8974
8673
|
try {
|
|
8975
|
-
|
|
8674
|
+
rmSync7(this.fileFor(turnId, seq), { force: true });
|
|
8976
8675
|
} catch {
|
|
8977
8676
|
}
|
|
8978
8677
|
}
|
|
8979
8678
|
size() {
|
|
8980
8679
|
const dir2 = this.dir();
|
|
8981
|
-
if (!
|
|
8680
|
+
if (!existsSync11(dir2)) return 0;
|
|
8982
8681
|
try {
|
|
8983
8682
|
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
8984
8683
|
} catch {
|
|
@@ -8991,7 +8690,7 @@ var Outbox = class {
|
|
|
8991
8690
|
"companion outbox: dropping unreadable entry"
|
|
8992
8691
|
);
|
|
8993
8692
|
try {
|
|
8994
|
-
|
|
8693
|
+
rmSync7(full, { force: true });
|
|
8995
8694
|
} catch {
|
|
8996
8695
|
}
|
|
8997
8696
|
}
|
|
@@ -9026,43 +8725,43 @@ var Outbox = class {
|
|
|
9026
8725
|
};
|
|
9027
8726
|
|
|
9028
8727
|
// src/run-config.ts
|
|
9029
|
-
import { z as
|
|
9030
|
-
var mcpStdioServerSchema =
|
|
9031
|
-
type:
|
|
9032
|
-
command:
|
|
9033
|
-
args:
|
|
9034
|
-
env:
|
|
8728
|
+
import { z as z15 } from "zod";
|
|
8729
|
+
var mcpStdioServerSchema = z15.object({
|
|
8730
|
+
type: z15.literal("stdio").optional(),
|
|
8731
|
+
command: z15.string().min(1),
|
|
8732
|
+
args: z15.array(z15.string()).optional(),
|
|
8733
|
+
env: z15.record(z15.string(), z15.string()).optional()
|
|
9035
8734
|
});
|
|
9036
|
-
var mcpHttpServerSchema =
|
|
9037
|
-
type:
|
|
9038
|
-
url:
|
|
9039
|
-
headers:
|
|
8735
|
+
var mcpHttpServerSchema = z15.object({
|
|
8736
|
+
type: z15.literal("http"),
|
|
8737
|
+
url: z15.string().url(),
|
|
8738
|
+
headers: z15.record(z15.string(), z15.string()).optional()
|
|
9040
8739
|
});
|
|
9041
|
-
var mcpSseServerSchema =
|
|
9042
|
-
type:
|
|
9043
|
-
url:
|
|
9044
|
-
headers:
|
|
8740
|
+
var mcpSseServerSchema = z15.object({
|
|
8741
|
+
type: z15.literal("sse"),
|
|
8742
|
+
url: z15.string().url(),
|
|
8743
|
+
headers: z15.record(z15.string(), z15.string()).optional()
|
|
9045
8744
|
});
|
|
9046
|
-
var mcpServerDefSchema =
|
|
8745
|
+
var mcpServerDefSchema = z15.union([
|
|
9047
8746
|
mcpHttpServerSchema,
|
|
9048
8747
|
mcpSseServerSchema,
|
|
9049
8748
|
mcpStdioServerSchema
|
|
9050
8749
|
]);
|
|
9051
|
-
var thinkingConfigSchema =
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
8750
|
+
var thinkingConfigSchema = z15.discriminatedUnion("type", [
|
|
8751
|
+
z15.object({ type: z15.literal("adaptive") }),
|
|
8752
|
+
z15.object({ type: z15.literal("enabled"), budgetTokens: z15.number().int().positive().optional() }),
|
|
8753
|
+
z15.object({ type: z15.literal("disabled") })
|
|
9055
8754
|
]);
|
|
9056
|
-
var effortSchema =
|
|
9057
|
-
var runConfigSchema =
|
|
8755
|
+
var effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
8756
|
+
var runConfigSchema = z15.object({
|
|
9058
8757
|
// CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
|
|
9059
8758
|
// trio + its custom tool lists — `true` grants the host filesystem/shell, absent
|
|
9060
8759
|
// is the locked surface. Kept in lockstep with `@cabane/shared`'s
|
|
9061
8760
|
// `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
|
|
9062
8761
|
// stripped, so an older companion riding a newer server never rejects the config).
|
|
9063
|
-
hostAccess:
|
|
9064
|
-
mcpServers:
|
|
9065
|
-
model:
|
|
8762
|
+
hostAccess: z15.boolean().optional(),
|
|
8763
|
+
mcpServers: z15.record(z15.string(), mcpServerDefSchema).optional(),
|
|
8764
|
+
model: z15.string().min(1).optional(),
|
|
9066
8765
|
thinking: thinkingConfigSchema.optional(),
|
|
9067
8766
|
effort: effortSchema.optional()
|
|
9068
8767
|
});
|
|
@@ -10153,22 +9852,22 @@ function handleUncaught(log, err, origin) {
|
|
|
10153
9852
|
}
|
|
10154
9853
|
|
|
10155
9854
|
// src/crash-marker.ts
|
|
10156
|
-
import { existsSync as
|
|
10157
|
-
import { join as
|
|
9855
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
9856
|
+
import { join as join16 } from "path";
|
|
10158
9857
|
function crashMarkerPath() {
|
|
10159
|
-
return
|
|
9858
|
+
return join16(cabaneDir(), "last-error.json");
|
|
10160
9859
|
}
|
|
10161
9860
|
function recordCrash(rec2) {
|
|
10162
9861
|
try {
|
|
10163
|
-
|
|
10164
|
-
|
|
9862
|
+
mkdirSync12(cabaneDir(), { recursive: true });
|
|
9863
|
+
writeFileSync8(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
|
|
10165
9864
|
} catch {
|
|
10166
9865
|
}
|
|
10167
9866
|
}
|
|
10168
9867
|
function clearCrash() {
|
|
10169
9868
|
try {
|
|
10170
9869
|
const path = crashMarkerPath();
|
|
10171
|
-
if (
|
|
9870
|
+
if (existsSync12(path)) rmSync8(path, { force: true });
|
|
10172
9871
|
} catch {
|
|
10173
9872
|
}
|
|
10174
9873
|
}
|
|
@@ -10320,7 +10019,24 @@ async function closeSurfaces(control, dashboard) {
|
|
|
10320
10019
|
|
|
10321
10020
|
// src/commands/daemon.ts
|
|
10322
10021
|
import { spawn as spawn5 } from "child_process";
|
|
10323
|
-
import { closeSync as closeSync3, mkdirSync as
|
|
10022
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync13, openSync as openSync3 } from "fs";
|
|
10023
|
+
|
|
10024
|
+
// src/cli-entry.ts
|
|
10025
|
+
import { existsSync as existsSync13 } from "fs";
|
|
10026
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10027
|
+
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
10028
|
+
function companionCliEntry(deps = {}) {
|
|
10029
|
+
const exists = deps.exists ?? existsSync13;
|
|
10030
|
+
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath2(new URL(rel, import.meta.url)));
|
|
10031
|
+
for (const candidate of candidates) {
|
|
10032
|
+
if (exists(candidate)) return candidate;
|
|
10033
|
+
}
|
|
10034
|
+
const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
|
|
10035
|
+
if (argv1 && exists(argv1)) return argv1;
|
|
10036
|
+
return candidates[0] ?? "";
|
|
10037
|
+
}
|
|
10038
|
+
|
|
10039
|
+
// src/commands/daemon.ts
|
|
10324
10040
|
var STARTUP_TIMEOUT_MS = 8e3;
|
|
10325
10041
|
var POLL_INTERVAL_MS = 150;
|
|
10326
10042
|
async function startDaemon(opts = {}, deps = {}) {
|
|
@@ -10330,11 +10046,6 @@ async function startDaemon(opts = {}, deps = {}) {
|
|
|
10330
10046
|
const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
|
|
10331
10047
|
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
10332
10048
|
const now = deps.now ?? (() => Date.now());
|
|
10333
|
-
const install = deps.installService ?? ((o) => installService(o));
|
|
10334
|
-
const refresh = deps.refreshService ?? ((o) => refreshInstalledService(o));
|
|
10335
|
-
const disable = deps.disableService ?? (() => disableService());
|
|
10336
|
-
const isTty = deps.isTty ?? (() => Boolean(process.stdin.isTTY));
|
|
10337
|
-
const serviceOpts = {};
|
|
10338
10049
|
const quiet = opts.report === "failures";
|
|
10339
10050
|
const { cfg, claudeOnPath: claudeOnPath2 } = await requireStartConfig({
|
|
10340
10051
|
// Unset → `requireStartConfig`'s own `requireConfig`, the real one.
|
|
@@ -10362,22 +10073,16 @@ async function startDaemon(opts = {}, deps = {}) {
|
|
|
10362
10073
|
const existing = readState();
|
|
10363
10074
|
if (existing) {
|
|
10364
10075
|
if (await verify(existing) !== "stale") {
|
|
10365
|
-
const rerendered = isTty() ? refresh(serviceOpts) : false;
|
|
10366
10076
|
process.stdout.write(
|
|
10367
10077
|
`Cabane Companion is already running (pid ${existing.pid}).
|
|
10368
10078
|
Stop it first with \`cabane-companion stop\` if you want to relaunch.
|
|
10369
10079
|
Connect a harness to the running companion: cabane-companion connect claude-code
|
|
10370
|
-
`
|
|
10080
|
+
`
|
|
10371
10081
|
);
|
|
10372
|
-
return {
|
|
10373
|
-
started: false,
|
|
10374
|
-
service: { installed: false, reason: "unsupported" },
|
|
10375
|
-
state: existing
|
|
10376
|
-
};
|
|
10082
|
+
return { started: false, state: existing };
|
|
10377
10083
|
}
|
|
10378
10084
|
clearRuntimeState();
|
|
10379
10085
|
}
|
|
10380
|
-
let service2 = isTty() ? install(serviceOpts) : { installed: false, reason: "unsupported" };
|
|
10381
10086
|
const args = ["start", "--foreground"];
|
|
10382
10087
|
const launchDetached = () => {
|
|
10383
10088
|
const spawned = spawnDetached(args);
|
|
@@ -10394,29 +10099,8 @@ Connect a harness to the running companion: cabane-companion connect claude-code
|
|
|
10394
10099
|
}
|
|
10395
10100
|
return ready(seen) ? seen : null;
|
|
10396
10101
|
};
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
process.exitCode = 1;
|
|
10400
|
-
return { started: false, service: service2, state: null };
|
|
10401
|
-
}
|
|
10402
|
-
let child = service2.installed ? null : launchDetached();
|
|
10403
|
-
let state = await waitForReady();
|
|
10404
|
-
if (!state && service2.installed) {
|
|
10405
|
-
process.stdout.write(
|
|
10406
|
-
`${service2.manager} started the companion but it didn't report ready within ${STARTUP_TIMEOUT_MS / 1e3}s \u2014 removing the login service.
|
|
10407
|
-
`
|
|
10408
|
-
);
|
|
10409
|
-
const removed = disable();
|
|
10410
|
-
if (!removed.handled || !removed.ok) {
|
|
10411
|
-
refuseDouble(service2.manager, removed.detail);
|
|
10412
|
-
process.exitCode = 1;
|
|
10413
|
-
return { started: false, service: service2, state: null };
|
|
10414
|
-
}
|
|
10415
|
-
process.stdout.write("Launching it directly instead.\n");
|
|
10416
|
-
service2 = { installed: false, reason: "command-failed", detail: "the service never came up" };
|
|
10417
|
-
child = launchDetached();
|
|
10418
|
-
state = await waitForReady();
|
|
10419
|
-
}
|
|
10102
|
+
const child = launchDetached();
|
|
10103
|
+
const state = await waitForReady();
|
|
10420
10104
|
if (!state) {
|
|
10421
10105
|
process.stdout.write(
|
|
10422
10106
|
`Cabane Companion was launched (pid ${child?.pid ?? "?"}) but didn't report ready within ${STARTUP_TIMEOUT_MS / 1e3}s.
|
|
@@ -10424,9 +10108,9 @@ Check ${companionLogPath()} for errors, or run \`cabane-companion status\`.
|
|
|
10424
10108
|
`
|
|
10425
10109
|
);
|
|
10426
10110
|
process.exitCode = 1;
|
|
10427
|
-
return { started: false,
|
|
10111
|
+
return { started: false, state: null };
|
|
10428
10112
|
}
|
|
10429
|
-
if (quiet) return { started: true,
|
|
10113
|
+
if (quiet) return { started: true, state };
|
|
10430
10114
|
process.stdout.write(
|
|
10431
10115
|
`Cabane Companion started in the background (pid ${state.pid}).
|
|
10432
10116
|
Logs: ${companionLogPath()}
|
|
@@ -10434,27 +10118,11 @@ Status: cabane-companion status
|
|
|
10434
10118
|
Stop: cabane-companion stop
|
|
10435
10119
|
`
|
|
10436
10120
|
);
|
|
10437
|
-
|
|
10438
|
-
process.stdout.write(`Autostart: enabled (${service2.manager}) \u2014 it starts again at login.
|
|
10439
|
-
`);
|
|
10440
|
-
} else if (service2.reason !== "unsupported") {
|
|
10441
|
-
process.stdout.write(
|
|
10442
|
-
`Autostart: not enabled \u2014 ${service2.detail ?? "the service manager refused the install"}. Running detached instead; it won't come back after a reboot.
|
|
10443
|
-
`
|
|
10444
|
-
);
|
|
10445
|
-
}
|
|
10446
|
-
return { started: true, service: service2, state };
|
|
10447
|
-
}
|
|
10448
|
-
function refuseDouble(manager, detail) {
|
|
10449
|
-
process.stdout.write(
|
|
10450
|
-
`${manager} still has the login service${detail ? ` (${detail})` : ""} \u2014 not launching a second companion beside a service that may still own one.
|
|
10451
|
-
Check \`cabane-companion service status\`, then \`cabane-companion service disable\`, and run \`cabane-companion start --daemon\` again.
|
|
10452
|
-
`
|
|
10453
|
-
);
|
|
10121
|
+
return { started: true, state };
|
|
10454
10122
|
}
|
|
10455
10123
|
function defaultSpawnDetached(args) {
|
|
10456
10124
|
const cliPath = companionCliEntry();
|
|
10457
|
-
|
|
10125
|
+
mkdirSync13(cabaneDir(), { recursive: true });
|
|
10458
10126
|
const logFd = openSync3(companionLogPath(), "a");
|
|
10459
10127
|
try {
|
|
10460
10128
|
return spawn5(process.execPath, [cliPath, ...args], {
|
|
@@ -10675,11 +10343,7 @@ async function handOffToBackground(runtime, ctx) {
|
|
|
10675
10343
|
return;
|
|
10676
10344
|
}
|
|
10677
10345
|
if (ctx.spaceAbove) blank();
|
|
10678
|
-
write(
|
|
10679
|
-
INDENT + tick(
|
|
10680
|
-
outcome.service.installed ? "Cabane companion is running in the background, and will start again when you log in." : "Cabane companion is running in the background."
|
|
10681
|
-
)
|
|
10682
|
-
);
|
|
10346
|
+
write(INDENT + tick("Cabane companion is running in the background."));
|
|
10683
10347
|
write(` Stop: cabane-companion stop Logs: ${tildePath(companionLogPath())}`);
|
|
10684
10348
|
if (ctx.justPaired) {
|
|
10685
10349
|
blank();
|
|
@@ -10743,8 +10407,7 @@ companion: deploy drain requested (${graceMs}ms grace)\u2026
|
|
|
10743
10407
|
|
|
10744
10408
|
// src/commands/status.ts
|
|
10745
10409
|
import { readdirSync as readdirSync4 } from "fs";
|
|
10746
|
-
async function status(
|
|
10747
|
-
const readService = deps.service ?? serviceStatus;
|
|
10410
|
+
async function status() {
|
|
10748
10411
|
const cfg = loadConfig();
|
|
10749
10412
|
if (!cfg || !cfg.deviceToken) {
|
|
10750
10413
|
process.stdout.write(
|
|
@@ -10780,13 +10443,6 @@ async function status(deps = {}) {
|
|
|
10780
10443
|
"companion: not running \u2014 `cabane-companion start` (backgrounds itself; `--foreground` stays attached)\n"
|
|
10781
10444
|
);
|
|
10782
10445
|
}
|
|
10783
|
-
const svc = readService();
|
|
10784
|
-
if (svc.installed) {
|
|
10785
|
-
process.stdout.write(
|
|
10786
|
-
`autostart: ${svc.manager} \u2014 starts at login` + (svc.manager === "systemd-user" && svc.linger !== "yes" ? ", while you are logged in" : "") + ` (details: cabane-companion service status)
|
|
10787
|
-
`
|
|
10788
|
-
);
|
|
10789
|
-
}
|
|
10790
10446
|
process.stdout.write(
|
|
10791
10447
|
"agents: pulled from cabane at runtime \u2014 see this device in Cabane for its assigned agents, their run state, and any missing-secret warnings.\n"
|
|
10792
10448
|
);
|
|
@@ -10833,34 +10489,12 @@ function formatUptime(startedAt) {
|
|
|
10833
10489
|
// src/commands/stop.ts
|
|
10834
10490
|
var TERM_GRACE_MS = 6e3;
|
|
10835
10491
|
var POLL_INTERVAL_MS2 = 150;
|
|
10836
|
-
var SERVICE_STOP_GRACE_MS = 3e3;
|
|
10837
10492
|
async function stop(deps = {}) {
|
|
10838
10493
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
10839
10494
|
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
10840
10495
|
const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
|
|
10841
10496
|
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
10842
10497
|
const now = deps.now ?? (() => Date.now());
|
|
10843
|
-
const stopSvc = deps.stopService ?? (() => stopService());
|
|
10844
|
-
const svc = stopSvc();
|
|
10845
|
-
if (svc.handled) {
|
|
10846
|
-
if (!svc.ok) {
|
|
10847
|
-
process.stdout.write(
|
|
10848
|
-
`companion: ${svc.manager} wouldn't stop the service${svc.detail ? ` (${svc.detail})` : ""}.
|
|
10849
|
-
The service manager owns this process \u2014 stopping it directly would just be restarted.
|
|
10850
|
-
Check \`cabane-companion service status\`, or remove it with \`cabane-companion service disable\`.
|
|
10851
|
-
`
|
|
10852
|
-
);
|
|
10853
|
-
process.exitCode = 1;
|
|
10854
|
-
return;
|
|
10855
|
-
}
|
|
10856
|
-
const deadline2 = now() + SERVICE_STOP_GRACE_MS;
|
|
10857
|
-
while (readState() && now() < deadline2) await sleep4(POLL_INTERVAL_MS2);
|
|
10858
|
-
if (!readState()) {
|
|
10859
|
-
process.stdout.write(`companion: stopped (${svc.manager} service stopped).
|
|
10860
|
-
`);
|
|
10861
|
-
return;
|
|
10862
|
-
}
|
|
10863
|
-
}
|
|
10864
10498
|
const state = readState();
|
|
10865
10499
|
if (!state) {
|
|
10866
10500
|
process.stdout.write("companion: not running (nothing to stop).\n");
|
|
@@ -10910,8 +10544,8 @@ function isAlive(kill, pid) {
|
|
|
10910
10544
|
}
|
|
10911
10545
|
|
|
10912
10546
|
// src/commands/transcript.ts
|
|
10913
|
-
import { existsSync as
|
|
10914
|
-
import { isAbsolute, join as
|
|
10547
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, readdirSync as readdirSync5 } from "fs";
|
|
10548
|
+
import { isAbsolute, join as join17 } from "path";
|
|
10915
10549
|
async function transcript(opts = {}) {
|
|
10916
10550
|
const dir2 = transcriptsDir();
|
|
10917
10551
|
if (opts.follow) {
|
|
@@ -10928,7 +10562,7 @@ async function transcript(opts = {}) {
|
|
|
10928
10562
|
process.stdout.write(emptyMessage(dir2));
|
|
10929
10563
|
return;
|
|
10930
10564
|
}
|
|
10931
|
-
process.stdout.write(renderFile(
|
|
10565
|
+
process.stdout.write(renderFile(join17(dir2, newest)) + "\n");
|
|
10932
10566
|
return;
|
|
10933
10567
|
}
|
|
10934
10568
|
printList(dir2);
|
|
@@ -11011,7 +10645,7 @@ function isComplete(content) {
|
|
|
11011
10645
|
async function followTranscripts(dir2) {
|
|
11012
10646
|
const follower = new TranscriptFollower({
|
|
11013
10647
|
listFiles: () => listFiles(dir2),
|
|
11014
|
-
read: (f) =>
|
|
10648
|
+
read: (f) => readFileSync10(join17(dir2, f), "utf8"),
|
|
11015
10649
|
write: (s) => process.stdout.write(s),
|
|
11016
10650
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
11017
10651
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -11051,7 +10685,7 @@ function printList(dir2) {
|
|
|
11051
10685
|
|
|
11052
10686
|
`);
|
|
11053
10687
|
for (const f of files.slice(0, 20)) {
|
|
11054
|
-
const { meta, outcome } = peek(
|
|
10688
|
+
const { meta, outcome } = peek(join17(dir2, f));
|
|
11055
10689
|
const when = fmtTime(rec(meta)?.ts);
|
|
11056
10690
|
const ws = str2(rec(meta)?.workspaceSlug);
|
|
11057
10691
|
const o = rec(outcome);
|
|
@@ -11072,7 +10706,7 @@ function peek(path) {
|
|
|
11072
10706
|
let meta;
|
|
11073
10707
|
let outcome;
|
|
11074
10708
|
try {
|
|
11075
|
-
for (const line of
|
|
10709
|
+
for (const line of readFileSync10(path, "utf8").split("\n")) {
|
|
11076
10710
|
if (!line.trim()) continue;
|
|
11077
10711
|
const o = safeParse(line);
|
|
11078
10712
|
const t = str2(rec(o)?.type);
|
|
@@ -11083,29 +10717,29 @@ function peek(path) {
|
|
|
11083
10717
|
}
|
|
11084
10718
|
return { meta, outcome };
|
|
11085
10719
|
}
|
|
11086
|
-
function resolveTarget(dir2,
|
|
11087
|
-
if (isAbsolute(
|
|
11088
|
-
if (
|
|
11089
|
-
throw new CompanionError(`no transcript at ${
|
|
10720
|
+
function resolveTarget(dir2, target) {
|
|
10721
|
+
if (isAbsolute(target) || target.includes("/")) {
|
|
10722
|
+
if (existsSync14(target)) return target;
|
|
10723
|
+
throw new CompanionError(`no transcript at ${target}.`);
|
|
11090
10724
|
}
|
|
11091
|
-
const exact =
|
|
11092
|
-
if (
|
|
11093
|
-
const matches = listFiles(dir2).filter((f) => f.includes(
|
|
11094
|
-
if (matches.length === 1) return
|
|
10725
|
+
const exact = join17(dir2, target);
|
|
10726
|
+
if (existsSync14(exact)) return exact;
|
|
10727
|
+
const matches = listFiles(dir2).filter((f) => f.includes(target));
|
|
10728
|
+
if (matches.length === 1) return join17(dir2, matches[0]);
|
|
11095
10729
|
if (matches.length === 0) {
|
|
11096
10730
|
throw new CompanionError(
|
|
11097
|
-
`no transcript matching "${
|
|
10731
|
+
`no transcript matching "${target}" in ${dir2}. Run \`cabane-companion transcript\` to list them.`
|
|
11098
10732
|
);
|
|
11099
10733
|
}
|
|
11100
10734
|
throw new CompanionError(
|
|
11101
|
-
`"${
|
|
10735
|
+
`"${target}" matches ${matches.length} transcripts \u2014 be more specific:
|
|
11102
10736
|
` + matches.slice(0, 10).map((m) => ` ${m}`).join("\n")
|
|
11103
10737
|
);
|
|
11104
10738
|
}
|
|
11105
10739
|
function renderFile(path) {
|
|
11106
10740
|
let content;
|
|
11107
10741
|
try {
|
|
11108
|
-
content =
|
|
10742
|
+
content = readFileSync10(path, "utf8");
|
|
11109
10743
|
} catch (err) {
|
|
11110
10744
|
throw new CompanionError(
|
|
11111
10745
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -11242,11 +10876,11 @@ program.command("pair").description(
|
|
|
11242
10876
|
});
|
|
11243
10877
|
});
|
|
11244
10878
|
program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
|
|
11245
|
-
writeCompletedPairing(
|
|
10879
|
+
writeCompletedPairing(readFileSync11(0, "utf8"));
|
|
11246
10880
|
});
|
|
11247
10881
|
program.command("start").description(
|
|
11248
10882
|
"pair this device if needed, connect a coding agent on it, and run in the background."
|
|
11249
|
-
).option("--foreground", "run attached in this terminal (
|
|
10883
|
+
).option("--foreground", "run attached in this terminal (for terminals and supervisors)").option("--daemon", "run in the background (the default; kept for compatibility)").option("--server <url>", "the cabane instance to pair with, if this device isn\u2019t paired yet").addOption(new Option("--open").hideHelp()).addOption(new Option("--no-open").hideHelp()).addOption(new Option("--port <port>").hideHelp()).action(async (opts) => {
|
|
11250
10884
|
await start({
|
|
11251
10885
|
...opts.foreground ? { foreground: true } : {},
|
|
11252
10886
|
...opts.server !== void 0 ? { server: opts.server } : {}
|
|
@@ -11261,13 +10895,6 @@ program.command("stop").description("stop a running companion (SIGTERM, then for
|
|
|
11261
10895
|
program.command("status").description("print the companion's local state (pairing, secrets, log path).").action(async () => {
|
|
11262
10896
|
await status();
|
|
11263
10897
|
});
|
|
11264
|
-
var service = program.command("service").description("inspect or remove the login service that starts the companion when you log in.");
|
|
11265
|
-
service.command("status").description("print the service manager, whether the unit is installed and running.").action(() => {
|
|
11266
|
-
serviceStatusCommand();
|
|
11267
|
-
});
|
|
11268
|
-
service.command("disable").description("stop the companion and remove the login service, leaving the machine clean.").action(() => {
|
|
11269
|
-
serviceDisableCommand();
|
|
11270
|
-
});
|
|
11271
10898
|
program.command("transcript").description("show the full agent transcript for a recent dispatch (the agent's whole turn).").argument("[file]", "a transcript filename or substring; omit to list recent transcripts").option("--last", "render the most recent transcript").option("-f, --follow", "watch for new turns and live-render them as they land (Ctrl-C to stop)").action(async (file, opts) => {
|
|
11272
10899
|
await transcript({
|
|
11273
10900
|
...file !== void 0 ? { target: file } : {},
|