@cabane/companion 0.6.156 → 0.6.158
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 +7 -109
- package/dist/bin.js +2 -1
- package/dist/boot-guard.js +112 -0
- package/dist/cli.js +1848 -357
- package/dist/launch.js +411 -0
- package/dist/pairing-config.js +9 -0
- package/dist/runtime.js +957 -196
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -692,16 +692,16 @@ function migrateConnectedHarnesses(cfg) {
|
|
|
692
692
|
}
|
|
693
693
|
function localAgentConfig(cfg, agent) {
|
|
694
694
|
const map = cfg.agents ?? {};
|
|
695
|
-
const
|
|
696
|
-
if (!cfg.prepareHook ||
|
|
697
|
-
return { ...
|
|
695
|
+
const row3 = map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
|
|
696
|
+
if (!cfg.prepareHook || row3.prepareHook) return row3;
|
|
697
|
+
return { ...row3, prepareHook: cfg.prepareHook };
|
|
698
698
|
}
|
|
699
699
|
function agentSandbox(cfg, agent) {
|
|
700
700
|
const sandbox = cfg.agents?.[agent.agentId]?.sandbox;
|
|
701
701
|
return sandbox ? { sandbox } : {};
|
|
702
702
|
}
|
|
703
|
-
function runnerSandboxFor(cfg,
|
|
704
|
-
const asked =
|
|
703
|
+
function runnerSandboxFor(cfg, row3, host) {
|
|
704
|
+
const asked = row3.sandbox ?? cfg.runners?.sandbox ?? "none";
|
|
705
705
|
if (asked !== "os" || cfg.runners?.provider === "docker") return "none";
|
|
706
706
|
return host.osSandboxUsable() ? "os" : "none";
|
|
707
707
|
}
|
|
@@ -715,7 +715,7 @@ function runnerSandboxReport(cfg, agents, host) {
|
|
|
715
715
|
}
|
|
716
716
|
function agentSandboxRows(cfg) {
|
|
717
717
|
return Object.entries(cfg.agents ?? {}).flatMap(
|
|
718
|
-
([key,
|
|
718
|
+
([key, row3]) => row3.sandbox ? [[key, row3.sandbox]] : []
|
|
719
719
|
);
|
|
720
720
|
}
|
|
721
721
|
function loadConfig() {
|
|
@@ -897,8 +897,8 @@ var init_config = __esm({
|
|
|
897
897
|
}).strict();
|
|
898
898
|
AGENT_ID_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
899
899
|
agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema).superRefine((agents, ctx2) => {
|
|
900
|
-
for (const [key,
|
|
901
|
-
if (
|
|
900
|
+
for (const [key, row3] of Object.entries(agents)) {
|
|
901
|
+
if (row3.sandbox === void 0) continue;
|
|
902
902
|
if (AGENT_ID_KEY.test(key)) continue;
|
|
903
903
|
ctx2.addIssue({
|
|
904
904
|
code: z2.ZodIssueCode.custom,
|
|
@@ -965,6 +965,15 @@ var init_config = __esm({
|
|
|
965
965
|
// absent `claudeCode` block means "never offered"; without it, the block's
|
|
966
966
|
// absence marks a config from before connectors were chosen.
|
|
967
967
|
harnessesChosen: z2.boolean().optional(),
|
|
968
|
+
// Whether this companion keeps itself current: installs the version Cabane names, between
|
|
969
|
+
// turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
|
|
970
|
+
// which `start` offers; off either way until someone says yes.
|
|
971
|
+
update: z2.object({
|
|
972
|
+
auto: z2.boolean()
|
|
973
|
+
}).strict().optional(),
|
|
974
|
+
// The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
|
|
975
|
+
// `cabane service install` still installs it.
|
|
976
|
+
serviceDeclined: z2.boolean().optional(),
|
|
968
977
|
// OpenCode's decline. Its block is an address, so a decline can't live there
|
|
969
978
|
// the way `claudeCode`/`codex` `enabled: false` does; without it a declined
|
|
970
979
|
// serve would be offered again on every `start`.
|
|
@@ -2682,8 +2691,8 @@ var init_policy = __esm({
|
|
|
2682
2691
|
// packages/agent-runtime/src/claude-code/model.ts
|
|
2683
2692
|
function parseClaudeCodeModel(model) {
|
|
2684
2693
|
if (!model) return null;
|
|
2685
|
-
const
|
|
2686
|
-
const bare =
|
|
2694
|
+
const sep6 = model.indexOf("/");
|
|
2695
|
+
const bare = sep6 === -1 || model.slice(0, sep6) !== "anthropic" ? model : model.slice(sep6 + 1);
|
|
2687
2696
|
return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
|
|
2688
2697
|
}
|
|
2689
2698
|
var CLAUDE_CODE_DEFAULT_MODEL_BARE;
|
|
@@ -2887,6 +2896,208 @@ var init_options = __esm({
|
|
|
2887
2896
|
}
|
|
2888
2897
|
});
|
|
2889
2898
|
|
|
2899
|
+
// packages/agent-runtime/src/claude-code/failure-evidence.ts
|
|
2900
|
+
function createFailureEvidence() {
|
|
2901
|
+
const signals = { sawRejectedLimit: false };
|
|
2902
|
+
return {
|
|
2903
|
+
observe(msg) {
|
|
2904
|
+
if (msg.type === "assistant" || msg.type === "auth_status") {
|
|
2905
|
+
const error = msg.error;
|
|
2906
|
+
if (typeof error === "string" && error.length > 0) {
|
|
2907
|
+
if (msg.type === "assistant") signals.lastAssistantError = error;
|
|
2908
|
+
else signals.authError = error;
|
|
2909
|
+
}
|
|
2910
|
+
} else if (msg.type === "rate_limit_event") {
|
|
2911
|
+
const info = msg.rate_limit_info;
|
|
2912
|
+
if (info?.status === "rejected") {
|
|
2913
|
+
signals.sawRejectedLimit = true;
|
|
2914
|
+
signals.rateLimitResetIso = resetsAtToIso(info.resetsAt) ?? signals.rateLimitResetIso;
|
|
2915
|
+
if (typeof info.rateLimitType === "string") signals.rateLimitType = info.rateLimitType;
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
},
|
|
2919
|
+
preInitReason(msg) {
|
|
2920
|
+
const failure = msg ? classifyPreInitFrame(msg, signals) : classifyFailure(signals, { text: "" });
|
|
2921
|
+
return failure ? encodeFailureReason(failure) : "session_start_failed";
|
|
2922
|
+
},
|
|
2923
|
+
failedResultReason(msg) {
|
|
2924
|
+
const failure = classifyResultFailure(msg, signals);
|
|
2925
|
+
const resultText = msg.result ?? "";
|
|
2926
|
+
const isError = msg.is_error === true;
|
|
2927
|
+
return failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
|
|
2928
|
+
},
|
|
2929
|
+
thrownFailure(message2, initReceived) {
|
|
2930
|
+
return initReceived ? classifyErrorText(message2) : classifyFailure(signals, { text: message2 });
|
|
2931
|
+
}
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
function classifyFailure(s, frame) {
|
|
2935
|
+
const rejectedIsCap = s.sawRejectedLimit && (s.rateLimitResetIso !== void 0 || isSubscriptionWindow(s.rateLimitType));
|
|
2936
|
+
if (rejectedIsCap || frame.terminalReason === "blocking_limit")
|
|
2937
|
+
return {
|
|
2938
|
+
kind: "usage_capped",
|
|
2939
|
+
...s.rateLimitResetIso ? { resetsAt: s.rateLimitResetIso } : {}
|
|
2940
|
+
};
|
|
2941
|
+
return classifyAssistantError(s.lastAssistantError) ?? classifyErrorText([s.authError, frame.text].filter(Boolean).join(" "));
|
|
2942
|
+
}
|
|
2943
|
+
function classifyResultFailure(msg, s) {
|
|
2944
|
+
const resultText = msg.result ?? "";
|
|
2945
|
+
const terminalReason = msg.terminal_reason;
|
|
2946
|
+
const errors = msg.errors;
|
|
2947
|
+
const text = [resultText, ...Array.isArray(errors) ? errors.map(String) : []].join(" ");
|
|
2948
|
+
return classifyFailure(s, { terminalReason, text });
|
|
2949
|
+
}
|
|
2950
|
+
function classifyPreInitFrame(msg, s) {
|
|
2951
|
+
if (msg.type === "result") {
|
|
2952
|
+
const isError = msg.is_error === true;
|
|
2953
|
+
if (msg.subtype === "success" && !isError) return classifyFailure(s, { text: "" });
|
|
2954
|
+
return classifyResultFailure(msg, s);
|
|
2955
|
+
}
|
|
2956
|
+
if (msg.type !== "assistant") return classifyFailure(s, { text: "" });
|
|
2957
|
+
const text = contentBlocks(msg).flatMap((b) => b.type === "text" && typeof b.text === "string" ? [b.text] : []).join(" ");
|
|
2958
|
+
return classifyFailure(s, { text });
|
|
2959
|
+
}
|
|
2960
|
+
function preInitToolActivity(msg) {
|
|
2961
|
+
const kind = msg.type === "assistant" ? "tool_use" : msg.type === "user" ? "tool_result" : null;
|
|
2962
|
+
return kind !== null && contentBlocks(msg).some((b) => b.type === kind);
|
|
2963
|
+
}
|
|
2964
|
+
function contentBlocks(msg) {
|
|
2965
|
+
const content = msg.message?.content;
|
|
2966
|
+
return Array.isArray(content) ? content : [];
|
|
2967
|
+
}
|
|
2968
|
+
function isSubscriptionWindow(value) {
|
|
2969
|
+
return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
|
|
2970
|
+
}
|
|
2971
|
+
var init_failure_evidence = __esm({
|
|
2972
|
+
"packages/agent-runtime/src/claude-code/failure-evidence.ts"() {
|
|
2973
|
+
"use strict";
|
|
2974
|
+
init_failure();
|
|
2975
|
+
}
|
|
2976
|
+
});
|
|
2977
|
+
|
|
2978
|
+
// packages/agent-runtime/src/claude-code/context-usage.ts
|
|
2979
|
+
function createContextUsage() {
|
|
2980
|
+
let resolvedModel;
|
|
2981
|
+
let lastRequestContextTokens;
|
|
2982
|
+
const compactions = [];
|
|
2983
|
+
return {
|
|
2984
|
+
get resolvedModel() {
|
|
2985
|
+
return resolvedModel;
|
|
2986
|
+
},
|
|
2987
|
+
recordInit(msg) {
|
|
2988
|
+
const model = msg.model;
|
|
2989
|
+
if (typeof model === "string" && model.length > 0) resolvedModel = model;
|
|
2990
|
+
},
|
|
2991
|
+
recordCompaction(msg) {
|
|
2992
|
+
const compaction = readCompaction(msg);
|
|
2993
|
+
if (compaction) compactions.push(compaction);
|
|
2994
|
+
},
|
|
2995
|
+
recordRequest(msg) {
|
|
2996
|
+
const tokens = readRequestContextTokens(msg);
|
|
2997
|
+
if (tokens !== void 0) lastRequestContextTokens = tokens;
|
|
2998
|
+
},
|
|
2999
|
+
async resultUsage(msg, readContextUsage) {
|
|
3000
|
+
const usage = readSdkUsage(msg.usage);
|
|
3001
|
+
if (usage) {
|
|
3002
|
+
if (lastRequestContextTokens !== void 0) usage.contextTokens = lastRequestContextTokens;
|
|
3003
|
+
const window = readContextWindow(msg, resolvedModel);
|
|
3004
|
+
if (window !== void 0) usage.contextWindow = window;
|
|
3005
|
+
if (compactions.length > 0) usage.contextCompactions = compactions;
|
|
3006
|
+
const reading = await readHarnessContext(readContextUsage);
|
|
3007
|
+
if (reading) Object.assign(usage, reading);
|
|
3008
|
+
}
|
|
3009
|
+
return usage;
|
|
3010
|
+
}
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
3013
|
+
function readSdkUsage(usage) {
|
|
3014
|
+
if (!usage || typeof usage !== "object") return void 0;
|
|
3015
|
+
const fields = usage;
|
|
3016
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
3017
|
+
const cacheReadTokens = num(fields.cache_read_input_tokens);
|
|
3018
|
+
const cacheCreationTokens = num(fields.cache_creation_input_tokens);
|
|
3019
|
+
const inputTokens = num(fields.input_tokens) + cacheCreationTokens + cacheReadTokens;
|
|
3020
|
+
const outputTokens = num(fields.output_tokens);
|
|
3021
|
+
return { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens };
|
|
3022
|
+
}
|
|
3023
|
+
function readRequestContextTokens(msg) {
|
|
3024
|
+
return readSdkUsage(msg.message?.usage)?.inputTokens;
|
|
3025
|
+
}
|
|
3026
|
+
async function readHarnessContext(read) {
|
|
3027
|
+
if (!read) return void 0;
|
|
3028
|
+
let timer;
|
|
3029
|
+
const timeout = new Promise((resolve2) => {
|
|
3030
|
+
timer = setTimeout(() => resolve2(void 0), CONTEXT_READING_TIMEOUT_MS);
|
|
3031
|
+
});
|
|
3032
|
+
try {
|
|
3033
|
+
return parseContextUsage(await Promise.race([read(), timeout]));
|
|
3034
|
+
} catch {
|
|
3035
|
+
return void 0;
|
|
3036
|
+
} finally {
|
|
3037
|
+
clearTimeout(timer);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
function parseContextUsage(response) {
|
|
3041
|
+
const r = response;
|
|
3042
|
+
if (!r || typeof r !== "object") return void 0;
|
|
3043
|
+
const total = readContextCount(r.totalTokens);
|
|
3044
|
+
const window = readContextCount(r.maxTokens);
|
|
3045
|
+
if (total === void 0 || window === void 0 || window === 0) return void 0;
|
|
3046
|
+
const categories = [];
|
|
3047
|
+
for (const c of Array.isArray(r.categories) ? r.categories : []) {
|
|
3048
|
+
const row3 = c;
|
|
3049
|
+
const tokens = readContextCount(row3?.tokens);
|
|
3050
|
+
if (typeof row3?.name === "string" && tokens !== void 0 && typeof row3.kind === "string" && CATEGORY_KINDS.has(row3.kind)) {
|
|
3051
|
+
categories.push({ name: row3.name, tokens, kind: row3.kind });
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
const buffer = categories.filter((c) => c.kind === "buffer").reduce((sum, c) => sum + c.tokens, 0);
|
|
3055
|
+
return {
|
|
3056
|
+
contextTokens: total,
|
|
3057
|
+
contextWindow: window,
|
|
3058
|
+
...buffer > 0 && buffer < window ? { contextCompactAt: window - buffer } : {},
|
|
3059
|
+
...categories.length > 0 ? { contextCategories: categories } : {}
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
function readCompaction(msg) {
|
|
3063
|
+
const meta = msg.compact_metadata;
|
|
3064
|
+
const preTokens = readContextCount(meta?.pre_tokens);
|
|
3065
|
+
if (!meta || preTokens === void 0) return void 0;
|
|
3066
|
+
if (meta.trigger !== "manual" && meta.trigger !== "auto") return void 0;
|
|
3067
|
+
const postTokens = readContextCount(meta.post_tokens);
|
|
3068
|
+
return {
|
|
3069
|
+
trigger: meta.trigger,
|
|
3070
|
+
preTokens,
|
|
3071
|
+
...postTokens !== void 0 ? { postTokens } : {}
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
function readContextWindow(msg, resolvedModel) {
|
|
3075
|
+
const modelUsage = msg.modelUsage;
|
|
3076
|
+
if (!modelUsage || typeof modelUsage !== "object") return void 0;
|
|
3077
|
+
const pos = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
|
|
3078
|
+
if (resolvedModel) {
|
|
3079
|
+
const direct = pos(modelUsage[resolvedModel]?.contextWindow);
|
|
3080
|
+
if (direct !== void 0) return direct;
|
|
3081
|
+
}
|
|
3082
|
+
let max;
|
|
3083
|
+
for (const entry of Object.values(modelUsage)) {
|
|
3084
|
+
const w = pos(entry?.contextWindow);
|
|
3085
|
+
if (w !== void 0 && (max === void 0 || w > max)) max = w;
|
|
3086
|
+
}
|
|
3087
|
+
return max;
|
|
3088
|
+
}
|
|
3089
|
+
function readContextCount(v) {
|
|
3090
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
|
|
3091
|
+
}
|
|
3092
|
+
var CONTEXT_READING_TIMEOUT_MS, CATEGORY_KINDS;
|
|
3093
|
+
var init_context_usage = __esm({
|
|
3094
|
+
"packages/agent-runtime/src/claude-code/context-usage.ts"() {
|
|
3095
|
+
"use strict";
|
|
3096
|
+
CONTEXT_READING_TIMEOUT_MS = 2e3;
|
|
3097
|
+
CATEGORY_KINDS = /* @__PURE__ */ new Set(["used", "free", "buffer", "deferred"]);
|
|
3098
|
+
}
|
|
3099
|
+
});
|
|
3100
|
+
|
|
2890
3101
|
// packages/agent-runtime/src/claude-code/loop.ts
|
|
2891
3102
|
async function* decodeSdkStream(iter, ctx2) {
|
|
2892
3103
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -2902,17 +3113,15 @@ async function* decodeSdkStream(iter, ctx2) {
|
|
|
2902
3113
|
let resultReason;
|
|
2903
3114
|
let sawResult = false;
|
|
2904
3115
|
let usage;
|
|
2905
|
-
const
|
|
2906
|
-
|
|
2907
|
-
let resolvedModel;
|
|
2908
|
-
const signals = { sawRejectedLimit: false };
|
|
3116
|
+
const contextUsage = createContextUsage();
|
|
3117
|
+
const failureEvidence = createFailureEvidence();
|
|
2909
3118
|
let preInitNotice = null;
|
|
2910
3119
|
try {
|
|
2911
3120
|
for await (const msg of iter) {
|
|
2912
3121
|
if (ctx2.signal.aborted) return;
|
|
3122
|
+
failureEvidence.observe(msg);
|
|
2913
3123
|
if (msg.type === "system" && msg.subtype === "init") {
|
|
2914
|
-
|
|
2915
|
-
if (typeof initModel === "string" && initModel.length > 0) resolvedModel = initModel;
|
|
3124
|
+
contextUsage.recordInit(msg);
|
|
2916
3125
|
mcpInventory = readMcpInventory(msg);
|
|
2917
3126
|
workspaceProven = provesWorkspaceTools(msg);
|
|
2918
3127
|
if (!workspaceProven) {
|
|
@@ -2929,63 +3138,35 @@ async function* decodeSdkStream(iter, ctx2) {
|
|
|
2929
3138
|
};
|
|
2930
3139
|
}
|
|
2931
3140
|
} else if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
2932
|
-
|
|
2933
|
-
if (compaction) compactions.push(compaction);
|
|
3141
|
+
contextUsage.recordCompaction(msg);
|
|
2934
3142
|
} else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
|
|
2935
3143
|
if (preInitToolActivity(msg) && ctx2.startEvidence) ctx2.startEvidence.toolActivity = true;
|
|
2936
|
-
|
|
2937
|
-
resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
|
|
3144
|
+
resultReason = failureEvidence.preInitReason(msg);
|
|
2938
3145
|
if (msg.type === "assistant" && isHarnessAuthored(msg))
|
|
2939
3146
|
preInitNotice = harnessNoticeText(msg);
|
|
2940
3147
|
break;
|
|
2941
3148
|
} else if (msg.type === "assistant") {
|
|
2942
|
-
const assistantErr = msg.error;
|
|
2943
|
-
if (typeof assistantErr === "string" && assistantErr.length > 0) {
|
|
2944
|
-
signals.lastAssistantError = assistantErr;
|
|
2945
|
-
}
|
|
2946
3149
|
if (isHarnessAuthored(msg)) {
|
|
2947
3150
|
await emitHarnessMessage(msg, emit, pending, buffer);
|
|
2948
3151
|
yield* drain(out);
|
|
2949
3152
|
continue;
|
|
2950
3153
|
}
|
|
2951
|
-
|
|
2952
|
-
if (reqContext !== void 0) lastRequestContextTokens = reqContext;
|
|
3154
|
+
contextUsage.recordRequest(msg);
|
|
2953
3155
|
await processAssistantMessage(msg, emit, pending, buffer);
|
|
2954
3156
|
yield* drain(out);
|
|
2955
3157
|
} else if (msg.type === "user") {
|
|
2956
3158
|
await emitToolResults(msg, emit, pending);
|
|
2957
3159
|
yield* drain(out);
|
|
2958
|
-
} else if (msg.type === "rate_limit_event") {
|
|
2959
|
-
const info = msg.rate_limit_info;
|
|
2960
|
-
if (info?.status === "rejected") {
|
|
2961
|
-
signals.sawRejectedLimit = true;
|
|
2962
|
-
signals.rateLimitResetIso = resetsAtToIso(info.resetsAt) ?? signals.rateLimitResetIso;
|
|
2963
|
-
if (typeof info.rateLimitType === "string") signals.rateLimitType = info.rateLimitType;
|
|
2964
|
-
}
|
|
2965
|
-
} else if (msg.type === "auth_status") {
|
|
2966
|
-
const err = msg.error;
|
|
2967
|
-
if (typeof err === "string" && err.length > 0) signals.authError = err;
|
|
2968
3160
|
} else if (msg.type === "result") {
|
|
2969
3161
|
const origin = msg.origin;
|
|
2970
3162
|
if (origin?.kind === "task-notification") continue;
|
|
2971
3163
|
sawResult = true;
|
|
2972
|
-
usage =
|
|
2973
|
-
if (usage) {
|
|
2974
|
-
if (lastRequestContextTokens !== void 0)
|
|
2975
|
-
usage.contextTokens = lastRequestContextTokens;
|
|
2976
|
-
const window = readContextWindow(msg, resolvedModel);
|
|
2977
|
-
if (window !== void 0) usage.contextWindow = window;
|
|
2978
|
-
if (compactions.length > 0) usage.contextCompactions = compactions;
|
|
2979
|
-
const reading = await readHarnessContext(ctx2.readContextUsage);
|
|
2980
|
-
if (reading) Object.assign(usage, reading);
|
|
2981
|
-
}
|
|
3164
|
+
usage = await contextUsage.resultUsage(msg, ctx2.readContextUsage);
|
|
2982
3165
|
const isError = msg.is_error === true;
|
|
2983
3166
|
if (msg.subtype === "success" && !isError) {
|
|
2984
3167
|
ok = true;
|
|
2985
3168
|
} else {
|
|
2986
|
-
|
|
2987
|
-
const failure = classifyResultFailure(msg, signals);
|
|
2988
|
-
resultReason = failure ? encodeFailureReason(failure) : isError ? `error:${resultText.slice(0, 200) || "unknown"}` : `result_error:${msg.subtype}`;
|
|
3169
|
+
resultReason = failureEvidence.failedResultReason(msg);
|
|
2989
3170
|
ok = false;
|
|
2990
3171
|
}
|
|
2991
3172
|
break;
|
|
@@ -2994,7 +3175,7 @@ async function* decodeSdkStream(iter, ctx2) {
|
|
|
2994
3175
|
} catch (err) {
|
|
2995
3176
|
if (ctx2.signal.aborted) throw err;
|
|
2996
3177
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
2997
|
-
const failure =
|
|
3178
|
+
const failure = failureEvidence.thrownFailure(message2, mcpInventory.initReceived);
|
|
2998
3179
|
if (!failure && (mcpInventory.initReceived || ctx2.resumedSessionId === null)) throw err;
|
|
2999
3180
|
ok = false;
|
|
3000
3181
|
if (failure) {
|
|
@@ -3009,8 +3190,7 @@ async function* decodeSdkStream(iter, ctx2) {
|
|
|
3009
3190
|
if (!workspaceProven) {
|
|
3010
3191
|
ok = false;
|
|
3011
3192
|
if (resultReason === void 0) {
|
|
3012
|
-
|
|
3013
|
-
resultReason = failure ? encodeFailureReason(failure) : "session_start_failed";
|
|
3193
|
+
resultReason = failureEvidence.preInitReason();
|
|
3014
3194
|
}
|
|
3015
3195
|
if (preInitNotice) yield { type: "runtime_notice", body: preInitNotice };
|
|
3016
3196
|
}
|
|
@@ -3028,7 +3208,7 @@ async function* decodeSdkStream(iter, ctx2) {
|
|
|
3028
3208
|
ok,
|
|
3029
3209
|
...resultReason ? { reason: resultReason } : {},
|
|
3030
3210
|
...usage ? { usage } : {},
|
|
3031
|
-
...resolvedModel ? { resolvedModel } : {},
|
|
3211
|
+
...contextUsage.resolvedModel ? { resolvedModel: contextUsage.resolvedModel } : {},
|
|
3032
3212
|
mcpInventory
|
|
3033
3213
|
};
|
|
3034
3214
|
}
|
|
@@ -3047,38 +3227,6 @@ function provesWorkspaceTools(msg) {
|
|
|
3047
3227
|
const tools = msg.tools;
|
|
3048
3228
|
return Array.isArray(tools) && tools.includes("mcp__cabane__query");
|
|
3049
3229
|
}
|
|
3050
|
-
function classifyFailure(s, frame) {
|
|
3051
|
-
const rejectedIsCap = s.sawRejectedLimit && (s.rateLimitResetIso !== void 0 || isSubscriptionWindow(s.rateLimitType));
|
|
3052
|
-
if (rejectedIsCap || frame.terminalReason === "blocking_limit")
|
|
3053
|
-
return {
|
|
3054
|
-
kind: "usage_capped",
|
|
3055
|
-
...s.rateLimitResetIso ? { resetsAt: s.rateLimitResetIso } : {}
|
|
3056
|
-
};
|
|
3057
|
-
return classifyAssistantError(s.lastAssistantError) ?? classifyErrorText([s.authError, frame.text].filter(Boolean).join(" "));
|
|
3058
|
-
}
|
|
3059
|
-
function classifyResultFailure(msg, s) {
|
|
3060
|
-
const resultText = msg.result ?? "";
|
|
3061
|
-
const terminalReason = msg.terminal_reason;
|
|
3062
|
-
const errors = msg.errors;
|
|
3063
|
-
const text = [resultText, ...Array.isArray(errors) ? errors.map(String) : []].join(" ");
|
|
3064
|
-
return classifyFailure(s, { terminalReason, text });
|
|
3065
|
-
}
|
|
3066
|
-
function classifyPreInitFrame(msg, s) {
|
|
3067
|
-
if (msg.type === "result") {
|
|
3068
|
-
const isError = msg.is_error === true;
|
|
3069
|
-
if (msg.subtype === "success" && !isError) return classifyFailure(s, { text: "" });
|
|
3070
|
-
return classifyResultFailure(msg, s);
|
|
3071
|
-
}
|
|
3072
|
-
if (msg.type !== "assistant") return classifyFailure(s, { text: "" });
|
|
3073
|
-
const error = msg.error;
|
|
3074
|
-
const signals = typeof error === "string" && error.length > 0 ? { ...s, lastAssistantError: error } : s;
|
|
3075
|
-
const text = contentBlocks(msg).flatMap((b) => b.type === "text" && typeof b.text === "string" ? [b.text] : []).join(" ");
|
|
3076
|
-
return classifyFailure(signals, { text });
|
|
3077
|
-
}
|
|
3078
|
-
function preInitToolActivity(msg) {
|
|
3079
|
-
const kind = msg.type === "assistant" ? "tool_use" : msg.type === "user" ? "tool_result" : null;
|
|
3080
|
-
return kind !== null && contentBlocks(msg).some((b) => b.type === kind);
|
|
3081
|
-
}
|
|
3082
3230
|
function isHarnessAuthored(msg) {
|
|
3083
3231
|
const error = msg.error;
|
|
3084
3232
|
if (typeof error === "string" && error.length > 0) return true;
|
|
@@ -3102,107 +3250,19 @@ async function emitHarnessMessage(msg, emit, pending, buffer) {
|
|
|
3102
3250
|
);
|
|
3103
3251
|
}
|
|
3104
3252
|
}
|
|
3105
|
-
function contentBlocks(msg) {
|
|
3106
|
-
const content = msg.message?.content;
|
|
3107
|
-
return Array.isArray(content) ? content : [];
|
|
3108
|
-
}
|
|
3109
|
-
function isSubscriptionWindow(value) {
|
|
3110
|
-
return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
|
|
3111
|
-
}
|
|
3112
3253
|
function* drain(out) {
|
|
3113
3254
|
while (out.length > 0) yield out.shift();
|
|
3114
3255
|
}
|
|
3115
|
-
|
|
3116
|
-
const usage = msg.usage;
|
|
3117
|
-
if (!usage || typeof usage !== "object") return void 0;
|
|
3118
|
-
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
3119
|
-
const cacheReadTokens = num(usage.cache_read_input_tokens);
|
|
3120
|
-
const cacheCreationTokens = num(usage.cache_creation_input_tokens);
|
|
3121
|
-
const inputTokens = num(usage.input_tokens) + cacheCreationTokens + cacheReadTokens;
|
|
3122
|
-
const outputTokens = num(usage.output_tokens);
|
|
3123
|
-
return { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens };
|
|
3124
|
-
}
|
|
3125
|
-
function readRequestContextTokens(msg) {
|
|
3126
|
-
const usage = msg.message?.usage;
|
|
3127
|
-
if (!usage || typeof usage !== "object") return void 0;
|
|
3128
|
-
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
3129
|
-
return num(usage.input_tokens) + num(usage.cache_read_input_tokens) + num(usage.cache_creation_input_tokens);
|
|
3130
|
-
}
|
|
3131
|
-
async function readHarnessContext(read) {
|
|
3132
|
-
if (!read) return void 0;
|
|
3133
|
-
let timer;
|
|
3134
|
-
const timeout = new Promise((resolve2) => {
|
|
3135
|
-
timer = setTimeout(() => resolve2(void 0), CONTEXT_READING_TIMEOUT_MS);
|
|
3136
|
-
});
|
|
3137
|
-
try {
|
|
3138
|
-
return parseContextUsage(await Promise.race([read(), timeout]));
|
|
3139
|
-
} catch {
|
|
3140
|
-
return void 0;
|
|
3141
|
-
} finally {
|
|
3142
|
-
clearTimeout(timer);
|
|
3143
|
-
}
|
|
3144
|
-
}
|
|
3145
|
-
function parseContextUsage(response) {
|
|
3146
|
-
const r = response;
|
|
3147
|
-
if (!r || typeof r !== "object") return void 0;
|
|
3148
|
-
const count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
|
|
3149
|
-
const total = count(r.totalTokens);
|
|
3150
|
-
const window = count(r.maxTokens);
|
|
3151
|
-
if (total === void 0 || window === void 0 || window === 0) return void 0;
|
|
3152
|
-
const categories = [];
|
|
3153
|
-
for (const c of Array.isArray(r.categories) ? r.categories : []) {
|
|
3154
|
-
const row2 = c;
|
|
3155
|
-
const tokens = count(row2?.tokens);
|
|
3156
|
-
if (typeof row2?.name === "string" && tokens !== void 0 && typeof row2.kind === "string" && CATEGORY_KINDS.has(row2.kind)) {
|
|
3157
|
-
categories.push({ name: row2.name, tokens, kind: row2.kind });
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
const buffer = categories.filter((c) => c.kind === "buffer").reduce((sum, c) => sum + c.tokens, 0);
|
|
3161
|
-
return {
|
|
3162
|
-
contextTokens: total,
|
|
3163
|
-
contextWindow: window,
|
|
3164
|
-
...buffer > 0 && buffer < window ? { contextCompactAt: window - buffer } : {},
|
|
3165
|
-
...categories.length > 0 ? { contextCategories: categories } : {}
|
|
3166
|
-
};
|
|
3167
|
-
}
|
|
3168
|
-
function readCompaction(msg) {
|
|
3169
|
-
const meta = msg.compact_metadata;
|
|
3170
|
-
const count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
|
|
3171
|
-
const preTokens = count(meta?.pre_tokens);
|
|
3172
|
-
if (!meta || preTokens === void 0) return void 0;
|
|
3173
|
-
if (meta.trigger !== "manual" && meta.trigger !== "auto") return void 0;
|
|
3174
|
-
const postTokens = count(meta.post_tokens);
|
|
3175
|
-
return {
|
|
3176
|
-
trigger: meta.trigger,
|
|
3177
|
-
preTokens,
|
|
3178
|
-
...postTokens !== void 0 ? { postTokens } : {}
|
|
3179
|
-
};
|
|
3180
|
-
}
|
|
3181
|
-
function readContextWindow(msg, resolvedModel) {
|
|
3182
|
-
const modelUsage = msg.modelUsage;
|
|
3183
|
-
if (!modelUsage || typeof modelUsage !== "object") return void 0;
|
|
3184
|
-
const pos = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
|
|
3185
|
-
if (resolvedModel) {
|
|
3186
|
-
const direct = pos(modelUsage[resolvedModel]?.contextWindow);
|
|
3187
|
-
if (direct !== void 0) return direct;
|
|
3188
|
-
}
|
|
3189
|
-
let max;
|
|
3190
|
-
for (const entry of Object.values(modelUsage)) {
|
|
3191
|
-
const w = pos(entry?.contextWindow);
|
|
3192
|
-
if (w !== void 0 && (max === void 0 || w > max)) max = w;
|
|
3193
|
-
}
|
|
3194
|
-
return max;
|
|
3195
|
-
}
|
|
3196
|
-
var HARNESS_PLACEHOLDERS, CONTEXT_READING_TIMEOUT_MS, CATEGORY_KINDS;
|
|
3256
|
+
var HARNESS_PLACEHOLDERS;
|
|
3197
3257
|
var init_loop = __esm({
|
|
3198
3258
|
"packages/agent-runtime/src/claude-code/loop.ts"() {
|
|
3199
3259
|
"use strict";
|
|
3200
3260
|
init_transcript();
|
|
3201
3261
|
init_failure();
|
|
3202
3262
|
init_session();
|
|
3263
|
+
init_failure_evidence();
|
|
3264
|
+
init_context_usage();
|
|
3203
3265
|
HARNESS_PLACEHOLDERS = /* @__PURE__ */ new Set(["No response requested.", "(no content)"]);
|
|
3204
|
-
CONTEXT_READING_TIMEOUT_MS = 2e3;
|
|
3205
|
-
CATEGORY_KINDS = /* @__PURE__ */ new Set(["used", "free", "buffer", "deferred"]);
|
|
3206
3266
|
}
|
|
3207
3267
|
});
|
|
3208
3268
|
|
|
@@ -4236,9 +4296,9 @@ var init_policy2 = __esm({
|
|
|
4236
4296
|
|
|
4237
4297
|
// packages/agent-runtime/src/opencode/model.ts
|
|
4238
4298
|
function parseOpencodeModel(model) {
|
|
4239
|
-
const
|
|
4240
|
-
if (
|
|
4241
|
-
return { providerID: model.slice(0,
|
|
4299
|
+
const sep6 = model.indexOf("/");
|
|
4300
|
+
if (sep6 === -1) return { providerID: "", modelID: model };
|
|
4301
|
+
return { providerID: model.slice(0, sep6), modelID: model.slice(sep6 + 1) };
|
|
4242
4302
|
}
|
|
4243
4303
|
var init_model2 = __esm({
|
|
4244
4304
|
"packages/agent-runtime/src/opencode/model.ts"() {
|
|
@@ -4539,10 +4599,10 @@ async function* parseSseStream(body, sessionId, signal) {
|
|
|
4539
4599
|
const { value, done } = await reader.read();
|
|
4540
4600
|
if (done) return;
|
|
4541
4601
|
buf += decoder.decode(value, { stream: true });
|
|
4542
|
-
let
|
|
4543
|
-
while ((
|
|
4544
|
-
const frame = buf.slice(0,
|
|
4545
|
-
buf = buf.slice(
|
|
4602
|
+
let sep6;
|
|
4603
|
+
while ((sep6 = buf.indexOf("\n\n")) !== -1) {
|
|
4604
|
+
const frame = buf.slice(0, sep6);
|
|
4605
|
+
buf = buf.slice(sep6 + 2);
|
|
4546
4606
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
|
|
4547
4607
|
if (!data) continue;
|
|
4548
4608
|
let ev;
|
|
@@ -5553,8 +5613,8 @@ var init_policy3 = __esm({
|
|
|
5553
5613
|
|
|
5554
5614
|
// packages/agent-runtime/src/codex/model.ts
|
|
5555
5615
|
function parseCodexModel(model) {
|
|
5556
|
-
const
|
|
5557
|
-
const bare =
|
|
5616
|
+
const sep6 = model.indexOf("/");
|
|
5617
|
+
const bare = sep6 === -1 || model.slice(0, sep6) !== "openai" ? model : model.slice(sep6 + 1);
|
|
5558
5618
|
return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
|
|
5559
5619
|
}
|
|
5560
5620
|
var CODEX_DEFAULT_MODEL_BARES;
|
|
@@ -10249,7 +10309,7 @@ var init_turn_execution = __esm({
|
|
|
10249
10309
|
|
|
10250
10310
|
// src/dispatcher.ts
|
|
10251
10311
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
10252
|
-
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as
|
|
10312
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
10253
10313
|
import { join as join17 } from "path";
|
|
10254
10314
|
function checkoutState(cwd) {
|
|
10255
10315
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
@@ -10267,7 +10327,7 @@ function checkoutState(cwd) {
|
|
|
10267
10327
|
if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
|
|
10268
10328
|
let stat;
|
|
10269
10329
|
try {
|
|
10270
|
-
stat =
|
|
10330
|
+
stat = statSync3(gitPath);
|
|
10271
10331
|
} catch (error) {
|
|
10272
10332
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
10273
10333
|
}
|
|
@@ -11079,7 +11139,7 @@ import {
|
|
|
11079
11139
|
mkdirSync as mkdirSync12,
|
|
11080
11140
|
readdirSync as readdirSync5,
|
|
11081
11141
|
readFileSync as readFileSync11,
|
|
11082
|
-
renameSync as
|
|
11142
|
+
renameSync as renameSync7,
|
|
11083
11143
|
rmSync as rmSync11,
|
|
11084
11144
|
writeFileSync as writeFileSync8
|
|
11085
11145
|
} from "fs";
|
|
@@ -11116,7 +11176,7 @@ var init_outbox = __esm({
|
|
|
11116
11176
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
11117
11177
|
try {
|
|
11118
11178
|
writeFileSync8(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
11119
|
-
|
|
11179
|
+
renameSync7(tmp, target);
|
|
11120
11180
|
} catch (err) {
|
|
11121
11181
|
try {
|
|
11122
11182
|
rmSync11(tmp, { force: true });
|
|
@@ -11285,8 +11345,8 @@ var init_image_manifest = __esm({
|
|
|
11285
11345
|
});
|
|
11286
11346
|
|
|
11287
11347
|
// src/runner/exec-shim.ts
|
|
11288
|
-
import { chmodSync as
|
|
11289
|
-
import { join as
|
|
11348
|
+
import { chmodSync as chmodSync8, mkdirSync as mkdirSync20, mkdtempSync as mkdtempSync2, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "fs";
|
|
11349
|
+
import { join as join31 } from "path";
|
|
11290
11350
|
function shellQuote2(value) {
|
|
11291
11351
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
11292
11352
|
}
|
|
@@ -11296,18 +11356,18 @@ function createShimDir() {
|
|
|
11296
11356
|
return {
|
|
11297
11357
|
write(argv) {
|
|
11298
11358
|
if (!dir2) {
|
|
11299
|
-
|
|
11300
|
-
dir2 = mkdtempSync2(
|
|
11359
|
+
mkdirSync20(join31(cabaneDir(), "runner"), { recursive: true });
|
|
11360
|
+
dir2 = mkdtempSync2(join31(cabaneDir(), "runner", "turn-"));
|
|
11301
11361
|
}
|
|
11302
|
-
const path =
|
|
11303
|
-
|
|
11362
|
+
const path = join31(dir2, `exec-${++count}`);
|
|
11363
|
+
writeFileSync13(path, `#!/bin/sh
|
|
11304
11364
|
exec ${argv.map(shellQuote2).join(" ")} "$@"
|
|
11305
11365
|
`);
|
|
11306
|
-
|
|
11366
|
+
chmodSync8(path, 448);
|
|
11307
11367
|
return path;
|
|
11308
11368
|
},
|
|
11309
11369
|
dispose() {
|
|
11310
|
-
if (dir2)
|
|
11370
|
+
if (dir2) rmSync17(dir2, { recursive: true, force: true });
|
|
11311
11371
|
dir2 = null;
|
|
11312
11372
|
}
|
|
11313
11373
|
};
|
|
@@ -12260,7 +12320,11 @@ async function serveConnection(socket, handlers) {
|
|
|
12260
12320
|
reply(socket, result);
|
|
12261
12321
|
return;
|
|
12262
12322
|
}
|
|
12263
|
-
if (req.cmd === "
|
|
12323
|
+
if (req.cmd === "update") {
|
|
12324
|
+
reply(socket, await handlers.update(req.auto));
|
|
12325
|
+
return;
|
|
12326
|
+
}
|
|
12327
|
+
if (req.cmd === "stop") {
|
|
12264
12328
|
reply(socket, { ok: true });
|
|
12265
12329
|
setTimeout(() => handlers.stop(), 50).unref?.();
|
|
12266
12330
|
return;
|
|
@@ -12885,13 +12949,19 @@ init_term();
|
|
|
12885
12949
|
|
|
12886
12950
|
// src/commands/start.ts
|
|
12887
12951
|
init_config();
|
|
12888
|
-
import { spawn as
|
|
12952
|
+
import { spawn as spawn10 } from "child_process";
|
|
12889
12953
|
import { hostname as hostname3 } from "os";
|
|
12890
12954
|
init_cabane_command();
|
|
12891
12955
|
|
|
12892
12956
|
// src/logger.ts
|
|
12893
12957
|
init_config();
|
|
12894
|
-
import {
|
|
12958
|
+
import {
|
|
12959
|
+
createWriteStream,
|
|
12960
|
+
mkdirSync as mkdirSync5,
|
|
12961
|
+
openSync as openSync2,
|
|
12962
|
+
renameSync as renameSync3,
|
|
12963
|
+
statSync as statSync2
|
|
12964
|
+
} from "fs";
|
|
12895
12965
|
import { dirname as dirname4, join as join8 } from "path";
|
|
12896
12966
|
import { stripVTControlCharacters } from "util";
|
|
12897
12967
|
import pino from "pino";
|
|
@@ -12946,6 +13016,53 @@ function formatHumanLine(record) {
|
|
|
12946
13016
|
}
|
|
12947
13017
|
return line + "\n";
|
|
12948
13018
|
}
|
|
13019
|
+
var LOG_ROTATE_BYTES = 50 * 1024 * 1024;
|
|
13020
|
+
var LOG_ROTATIONS_KEPT = 3;
|
|
13021
|
+
var RotatingLog = class {
|
|
13022
|
+
constructor(path, maxBytes = LOG_ROTATE_BYTES, keep = LOG_ROTATIONS_KEPT) {
|
|
13023
|
+
this.path = path;
|
|
13024
|
+
this.maxBytes = maxBytes;
|
|
13025
|
+
this.keep = keep;
|
|
13026
|
+
this.size = sizeOf(path);
|
|
13027
|
+
this.stream = openAppend(path);
|
|
13028
|
+
}
|
|
13029
|
+
path;
|
|
13030
|
+
maxBytes;
|
|
13031
|
+
keep;
|
|
13032
|
+
stream;
|
|
13033
|
+
size;
|
|
13034
|
+
write(chunk) {
|
|
13035
|
+
const ok = this.stream.write(chunk);
|
|
13036
|
+
this.size += Buffer.byteLength(chunk);
|
|
13037
|
+
if (this.size >= this.maxBytes) this.rotate();
|
|
13038
|
+
return ok;
|
|
13039
|
+
}
|
|
13040
|
+
rotate() {
|
|
13041
|
+
this.stream.end();
|
|
13042
|
+
try {
|
|
13043
|
+
for (let i = this.keep - 1; i >= 1; i--) {
|
|
13044
|
+
try {
|
|
13045
|
+
renameSync3(`${this.path}.${i}`, `${this.path}.${i + 1}`);
|
|
13046
|
+
} catch {
|
|
13047
|
+
}
|
|
13048
|
+
}
|
|
13049
|
+
renameSync3(this.path, `${this.path}.1`);
|
|
13050
|
+
} catch {
|
|
13051
|
+
}
|
|
13052
|
+
this.stream = openAppend(this.path);
|
|
13053
|
+
this.size = sizeOf(this.path);
|
|
13054
|
+
}
|
|
13055
|
+
};
|
|
13056
|
+
function openAppend(path) {
|
|
13057
|
+
return createWriteStream("", { fd: openSync2(path, "a") });
|
|
13058
|
+
}
|
|
13059
|
+
function sizeOf(path) {
|
|
13060
|
+
try {
|
|
13061
|
+
return statSync2(path).size;
|
|
13062
|
+
} catch {
|
|
13063
|
+
return 0;
|
|
13064
|
+
}
|
|
13065
|
+
}
|
|
12949
13066
|
var cached = null;
|
|
12950
13067
|
var consoleLogging = true;
|
|
12951
13068
|
var overrides = {};
|
|
@@ -12964,7 +13081,7 @@ function configureLogger(log, config) {
|
|
|
12964
13081
|
function createLogger(destinations = {}, options = {}) {
|
|
12965
13082
|
const path = companionLogPath();
|
|
12966
13083
|
if (!destinations.file) mkdirSync5(dirname4(path), { recursive: true });
|
|
12967
|
-
const file = destinations.file ??
|
|
13084
|
+
const file = destinations.file ?? new RotatingLog(path);
|
|
12968
13085
|
const state = { format: options.logFormat ?? "human" };
|
|
12969
13086
|
const streams = [];
|
|
12970
13087
|
const human = (chunk) => {
|
|
@@ -13022,7 +13139,7 @@ import { serve } from "@hono/node-server";
|
|
|
13022
13139
|
import { Hono } from "hono";
|
|
13023
13140
|
|
|
13024
13141
|
// src/dashboard/routes.ts
|
|
13025
|
-
import { openSync as
|
|
13142
|
+
import { openSync as openSync3, readSync, closeSync as closeSync2, fstatSync, existsSync as existsSync6 } from "fs";
|
|
13026
13143
|
import { readFile } from "fs/promises";
|
|
13027
13144
|
import { extname, join as join9, normalize } from "path";
|
|
13028
13145
|
import { streamSSE } from "hono/streaming";
|
|
@@ -13406,7 +13523,7 @@ function tailFile(path, lines2) {
|
|
|
13406
13523
|
const MAX_BYTES = 256 * 1024;
|
|
13407
13524
|
let fd;
|
|
13408
13525
|
try {
|
|
13409
|
-
fd =
|
|
13526
|
+
fd = openSync3(path, "r");
|
|
13410
13527
|
const size = fstatSync(fd).size;
|
|
13411
13528
|
const start2 = Math.max(0, size - MAX_BYTES);
|
|
13412
13529
|
const len = size - start2;
|
|
@@ -13657,7 +13774,7 @@ init_config();
|
|
|
13657
13774
|
|
|
13658
13775
|
// src/dispatch-dedupe.ts
|
|
13659
13776
|
init_config();
|
|
13660
|
-
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as
|
|
13777
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync7 } from "fs";
|
|
13661
13778
|
import { join as join11 } from "path";
|
|
13662
13779
|
var MAX_IDS = 256;
|
|
13663
13780
|
function dir(log) {
|
|
@@ -13775,7 +13892,7 @@ function writeTurnIds(workspaceId, turns) {
|
|
|
13775
13892
|
const path = turnPathFor(workspaceId);
|
|
13776
13893
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
13777
13894
|
writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
|
|
13778
|
-
|
|
13895
|
+
renameSync4(tmp, path);
|
|
13779
13896
|
return entries.length;
|
|
13780
13897
|
}
|
|
13781
13898
|
function rememberTurnId(workspaceId, eventId, turnId) {
|
|
@@ -13971,9 +14088,9 @@ import {
|
|
|
13971
14088
|
mkdirSync as mkdirSync10,
|
|
13972
14089
|
readdirSync as readdirSync4,
|
|
13973
14090
|
readFileSync as readFileSync9,
|
|
13974
|
-
renameSync as
|
|
14091
|
+
renameSync as renameSync6,
|
|
13975
14092
|
rmSync as rmSync9,
|
|
13976
|
-
statSync as
|
|
14093
|
+
statSync as statSync4,
|
|
13977
14094
|
writeFileSync as writeFileSync6
|
|
13978
14095
|
} from "fs";
|
|
13979
14096
|
import { join as join20 } from "path";
|
|
@@ -14046,7 +14163,7 @@ import {
|
|
|
14046
14163
|
mkdirSync as mkdirSync9,
|
|
14047
14164
|
readdirSync as readdirSync3,
|
|
14048
14165
|
readFileSync as readFileSync8,
|
|
14049
|
-
renameSync as
|
|
14166
|
+
renameSync as renameSync5,
|
|
14050
14167
|
rmSync as rmSync8
|
|
14051
14168
|
} from "fs";
|
|
14052
14169
|
import { join as join18 } from "path";
|
|
@@ -14082,11 +14199,11 @@ function admitCollectedEntry(raw, turn) {
|
|
|
14082
14199
|
}
|
|
14083
14200
|
var atomicMove = (from, to) => {
|
|
14084
14201
|
try {
|
|
14085
|
-
|
|
14202
|
+
renameSync5(from, to);
|
|
14086
14203
|
} catch {
|
|
14087
14204
|
const partial = `${to}.partial`;
|
|
14088
14205
|
copyFileSync2(from, partial);
|
|
14089
|
-
|
|
14206
|
+
renameSync5(partial, to);
|
|
14090
14207
|
rmSync8(from, { force: true });
|
|
14091
14208
|
}
|
|
14092
14209
|
};
|
|
@@ -14330,6 +14447,25 @@ function pruneOtherAttempts(deviceDir, pair2, keep) {
|
|
|
14330
14447
|
}
|
|
14331
14448
|
}
|
|
14332
14449
|
}
|
|
14450
|
+
var IDLE_SESSION_ARCHIVE_MS = 30 * 24 * 60 * 6e4;
|
|
14451
|
+
function pruneIdleSessionArchives(deviceDir, now = Date.now(), maxAgeMs = IDLE_SESSION_ARCHIVE_MS) {
|
|
14452
|
+
const root = join20(deviceDir, "attempts");
|
|
14453
|
+
if (!existsSync14(root)) return [];
|
|
14454
|
+
const removed = [];
|
|
14455
|
+
for (const id of readdirSync4(root)) {
|
|
14456
|
+
if (!UUID_RE.test(id)) continue;
|
|
14457
|
+
let kept;
|
|
14458
|
+
try {
|
|
14459
|
+
kept = statSync4(join20(root, id)).mtimeMs;
|
|
14460
|
+
} catch {
|
|
14461
|
+
continue;
|
|
14462
|
+
}
|
|
14463
|
+
if (now - kept < maxAgeMs) continue;
|
|
14464
|
+
rmSync9(join20(root, id), { recursive: true, force: true });
|
|
14465
|
+
removed.push(id);
|
|
14466
|
+
}
|
|
14467
|
+
return removed;
|
|
14468
|
+
}
|
|
14333
14469
|
var CappedFile = class extends Writable {
|
|
14334
14470
|
constructor(path, cap) {
|
|
14335
14471
|
super();
|
|
@@ -14707,7 +14843,7 @@ function createDockerProvider(opts) {
|
|
|
14707
14843
|
(resolve2) => file.writableFinished ? resolve2() : file.once("finish", resolve2)
|
|
14708
14844
|
);
|
|
14709
14845
|
if (r.code === 0 && !file.over) {
|
|
14710
|
-
|
|
14846
|
+
renameSync6(partial, archive);
|
|
14711
14847
|
writeFileSync6(join20(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
|
|
14712
14848
|
pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
|
|
14713
14849
|
await writeMarker(name, attemptId);
|
|
@@ -14844,7 +14980,7 @@ function createDockerProvider(opts) {
|
|
|
14844
14980
|
);
|
|
14845
14981
|
} else {
|
|
14846
14982
|
log.info(
|
|
14847
|
-
{ container: name, attemptId, bytes:
|
|
14983
|
+
{ container: name, attemptId, bytes: statSync4(archive).size },
|
|
14848
14984
|
"runners: session files restored"
|
|
14849
14985
|
);
|
|
14850
14986
|
}
|
|
@@ -15308,6 +15444,12 @@ var SseSubscriber = class {
|
|
|
15308
15444
|
this.aborted = true;
|
|
15309
15445
|
this.controller?.abort();
|
|
15310
15446
|
}
|
|
15447
|
+
// Connect again after a `stop()`, once its loop has exited.
|
|
15448
|
+
async resume() {
|
|
15449
|
+
await this.finished;
|
|
15450
|
+
this.aborted = false;
|
|
15451
|
+
this.start();
|
|
15452
|
+
}
|
|
15311
15453
|
setCursor(eventId) {
|
|
15312
15454
|
this.lastEventId = eventId;
|
|
15313
15455
|
}
|
|
@@ -15396,9 +15538,564 @@ function sleep4(ms) {
|
|
|
15396
15538
|
|
|
15397
15539
|
// src/supervisor.ts
|
|
15398
15540
|
init_transcript_writer();
|
|
15541
|
+
|
|
15542
|
+
// src/updater.ts
|
|
15543
|
+
import { spawn as spawn8 } from "child_process";
|
|
15544
|
+
import { copyFileSync as copyFileSync3, existsSync as existsSync17, mkdirSync as mkdirSync16, renameSync as renameSync11, rmSync as rmSync14 } from "fs";
|
|
15545
|
+
import { dirname as dirname12, join as join26 } from "path";
|
|
15546
|
+
|
|
15547
|
+
// src/boot-rule.ts
|
|
15548
|
+
import {
|
|
15549
|
+
mkdirSync as mkdirSync13,
|
|
15550
|
+
readFileSync as readFileSync12,
|
|
15551
|
+
realpathSync as realpathSync5,
|
|
15552
|
+
renameSync as renameSync8,
|
|
15553
|
+
rmSync as rmSync12,
|
|
15554
|
+
symlinkSync,
|
|
15555
|
+
writeFileSync as writeFileSync9
|
|
15556
|
+
} from "fs";
|
|
15557
|
+
import { dirname as dirname9, join as join23 } from "path";
|
|
15558
|
+
var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
|
|
15559
|
+
var MAX_STAGE_ATTEMPTS = 3;
|
|
15560
|
+
function superseded(pending, ...running) {
|
|
15561
|
+
return !!pending && !running.includes(pending.to);
|
|
15562
|
+
}
|
|
15563
|
+
function rolledBackState(state, pending, reason, now) {
|
|
15564
|
+
const { pending: _done, ...rest } = state;
|
|
15565
|
+
return {
|
|
15566
|
+
...rest,
|
|
15567
|
+
lastResult: {
|
|
15568
|
+
kind: "rolled_back",
|
|
15569
|
+
from: pending.from,
|
|
15570
|
+
to: pending.to,
|
|
15571
|
+
at: now.toISOString(),
|
|
15572
|
+
reason: reason.replace(/\s+/g, " ").trim().slice(0, 400)
|
|
15573
|
+
},
|
|
15574
|
+
failed: { version: pending.to, attempts: MAX_STAGE_ATTEMPTS, at: now.toISOString() }
|
|
15575
|
+
};
|
|
15576
|
+
}
|
|
15577
|
+
|
|
15578
|
+
// src/install-layout.ts
|
|
15579
|
+
init_config();
|
|
15580
|
+
import {
|
|
15581
|
+
existsSync as existsSync16,
|
|
15582
|
+
lstatSync,
|
|
15583
|
+
mkdirSync as mkdirSync14,
|
|
15584
|
+
readFileSync as readFileSync13,
|
|
15585
|
+
readdirSync as readdirSync6,
|
|
15586
|
+
realpathSync as realpathSync6,
|
|
15587
|
+
renameSync as renameSync9,
|
|
15588
|
+
rmSync as rmSync13,
|
|
15589
|
+
symlinkSync as symlinkSync2
|
|
15590
|
+
} from "fs";
|
|
15591
|
+
import { dirname as dirname10, join as join24 } from "path";
|
|
15592
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
15593
|
+
function versionsDir() {
|
|
15594
|
+
return join24(cabaneDir(), "versions");
|
|
15595
|
+
}
|
|
15596
|
+
function currentLink() {
|
|
15597
|
+
return join24(cabaneDir(), "current");
|
|
15598
|
+
}
|
|
15599
|
+
function bootGuardPath() {
|
|
15600
|
+
return join24(cabaneDir(), "boot-guard.mjs");
|
|
15601
|
+
}
|
|
15602
|
+
function pinnedNodeDir() {
|
|
15603
|
+
return join24(cabaneDir(), "node");
|
|
15604
|
+
}
|
|
15605
|
+
function pinnedNodePath() {
|
|
15606
|
+
return join24(pinnedNodeDir(), "bin", "node");
|
|
15607
|
+
}
|
|
15608
|
+
function versionPackageRoot(version) {
|
|
15609
|
+
return join24(versionsDir(), version, "node_modules", "@cabane", "companion");
|
|
15610
|
+
}
|
|
15611
|
+
function ownPackageRoot(moduleUrl = import.meta.url) {
|
|
15612
|
+
return dirname10(dirname10(fileURLToPath6(moduleUrl)));
|
|
15613
|
+
}
|
|
15614
|
+
function packageVersion(root) {
|
|
15615
|
+
try {
|
|
15616
|
+
const pkg2 = JSON.parse(readFileSync13(join24(root, "package.json"), "utf8"));
|
|
15617
|
+
return typeof pkg2.version === "string" ? pkg2.version : null;
|
|
15618
|
+
} catch {
|
|
15619
|
+
return null;
|
|
15620
|
+
}
|
|
15621
|
+
}
|
|
15622
|
+
function currentRoot() {
|
|
15623
|
+
try {
|
|
15624
|
+
const root = realpathSync6(currentLink());
|
|
15625
|
+
return existsSync16(join24(root, "dist", "cli.js")) ? root : null;
|
|
15626
|
+
} catch {
|
|
15627
|
+
return null;
|
|
15628
|
+
}
|
|
15629
|
+
}
|
|
15630
|
+
function pointCurrentAt(root) {
|
|
15631
|
+
const link = currentLink();
|
|
15632
|
+
mkdirSync14(dirname10(link), { recursive: true });
|
|
15633
|
+
const fresh = `${link}.${process.pid}.${Date.now()}`;
|
|
15634
|
+
symlinkSync2(root, fresh);
|
|
15635
|
+
try {
|
|
15636
|
+
renameSync9(fresh, link);
|
|
15637
|
+
} catch (err) {
|
|
15638
|
+
rmSync13(fresh, { force: true });
|
|
15639
|
+
throw err;
|
|
15640
|
+
}
|
|
15641
|
+
}
|
|
15642
|
+
function pruneVersions(keep) {
|
|
15643
|
+
const dir2 = versionsDir();
|
|
15644
|
+
if (!existsSync16(dir2)) return [];
|
|
15645
|
+
const removed = [];
|
|
15646
|
+
for (const name of readdirSync6(dir2)) {
|
|
15647
|
+
if (keep.includes(name)) continue;
|
|
15648
|
+
rmSync13(join24(dir2, name), { recursive: true, force: true });
|
|
15649
|
+
removed.push(name);
|
|
15650
|
+
}
|
|
15651
|
+
return removed;
|
|
15652
|
+
}
|
|
15653
|
+
function compareVersions(a, b) {
|
|
15654
|
+
const parts = (v) => /^\d+(\.\d+)*$/.test(v) ? v.split(".").map(Number) : null;
|
|
15655
|
+
const pa = parts(a);
|
|
15656
|
+
const pb = parts(b);
|
|
15657
|
+
if (!pa || !pb) return pa ? 1 : pb ? -1 : 0;
|
|
15658
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
15659
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
15660
|
+
if (d !== 0) return d;
|
|
15661
|
+
}
|
|
15662
|
+
return 0;
|
|
15663
|
+
}
|
|
15664
|
+
|
|
15665
|
+
// src/update-state.ts
|
|
15666
|
+
init_config();
|
|
15667
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync14, renameSync as renameSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
15668
|
+
import { dirname as dirname11, join as join25 } from "path";
|
|
15669
|
+
import { z as z18 } from "zod";
|
|
15670
|
+
var resultSchema = z18.object({
|
|
15671
|
+
kind: z18.enum(["updated", "rolled_back", "failed"]),
|
|
15672
|
+
from: z18.string(),
|
|
15673
|
+
to: z18.string(),
|
|
15674
|
+
at: z18.string(),
|
|
15675
|
+
reason: z18.string().optional()
|
|
15676
|
+
});
|
|
15677
|
+
var pendingSchema = z18.object({
|
|
15678
|
+
from: z18.string(),
|
|
15679
|
+
// The package root `current` pointed at before the swap, so a rollback can point it back.
|
|
15680
|
+
fromRoot: z18.string(),
|
|
15681
|
+
to: z18.string(),
|
|
15682
|
+
at: z18.string(),
|
|
15683
|
+
boots: z18.number().int().nonnegative()
|
|
15684
|
+
});
|
|
15685
|
+
var stateSchema = z18.object({
|
|
15686
|
+
server: z18.object({ latest: z18.string(), minimum: z18.string(), at: z18.string() }).optional(),
|
|
15687
|
+
pending: pendingSchema.optional(),
|
|
15688
|
+
lastResult: resultSchema.optional(),
|
|
15689
|
+
failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
|
|
15690
|
+
});
|
|
15691
|
+
function updateStatePath(dir2 = cabaneDir()) {
|
|
15692
|
+
return join25(dir2, "update.json");
|
|
15693
|
+
}
|
|
15694
|
+
function readUpdateState(path = updateStatePath()) {
|
|
15695
|
+
try {
|
|
15696
|
+
const parsed = stateSchema.safeParse(JSON.parse(readFileSync14(path, "utf8")));
|
|
15697
|
+
return parsed.success ? parsed.data : {};
|
|
15698
|
+
} catch {
|
|
15699
|
+
return {};
|
|
15700
|
+
}
|
|
15701
|
+
}
|
|
15702
|
+
function writeUpdateState(state, path = updateStatePath()) {
|
|
15703
|
+
mkdirSync15(dirname11(path), { recursive: true });
|
|
15704
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
15705
|
+
writeFileSync10(tmp, `${JSON.stringify(state, null, 2)}
|
|
15706
|
+
`, { mode: 384 });
|
|
15707
|
+
renameSync10(tmp, path);
|
|
15708
|
+
}
|
|
15709
|
+
function updateUpdateState(change, path = updateStatePath()) {
|
|
15710
|
+
const next = change(readUpdateState(path));
|
|
15711
|
+
writeUpdateState(next, path);
|
|
15712
|
+
return next;
|
|
15713
|
+
}
|
|
15714
|
+
|
|
15715
|
+
// src/updater.ts
|
|
15716
|
+
function updateMode(env = process.env) {
|
|
15717
|
+
return env.CABANE_COMPANION_SUPERVISED === "1" ? "supervised" : "attached";
|
|
15718
|
+
}
|
|
15719
|
+
var STAGE_RETRY_MS = 60 * 6e4;
|
|
15720
|
+
var SWAP_DRAIN_MS = 1e4;
|
|
15721
|
+
var SOURCE_BUILD_VERSION = "0.0.0";
|
|
15722
|
+
function versionToStage(input) {
|
|
15723
|
+
const { own, state, now, force } = input;
|
|
15724
|
+
if (!input.auto && !force) return null;
|
|
15725
|
+
if (own === SOURCE_BUILD_VERSION) return null;
|
|
15726
|
+
const latest = state.server?.latest;
|
|
15727
|
+
if (!latest || compareVersions(latest, own) <= 0) return null;
|
|
15728
|
+
if (state.pending) return null;
|
|
15729
|
+
const failed = state.failed;
|
|
15730
|
+
if (!force && failed?.version === latest) {
|
|
15731
|
+
if (failed.attempts >= MAX_STAGE_ATTEMPTS) return null;
|
|
15732
|
+
if (now - Date.parse(failed.at) < STAGE_RETRY_MS) return null;
|
|
15733
|
+
}
|
|
15734
|
+
return latest;
|
|
15735
|
+
}
|
|
15736
|
+
function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
|
|
15737
|
+
if (existsSync17(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
|
|
15738
|
+
return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
|
|
15739
|
+
}
|
|
15740
|
+
function writeBootGuard(root = ownPackageRoot()) {
|
|
15741
|
+
const built = join26(root, "dist", "boot-guard.js");
|
|
15742
|
+
if (!existsSync17(built)) return false;
|
|
15743
|
+
const target = bootGuardPath();
|
|
15744
|
+
mkdirSync16(dirname12(target), { recursive: true });
|
|
15745
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
15746
|
+
copyFileSync3(built, tmp);
|
|
15747
|
+
renameSync11(tmp, target);
|
|
15748
|
+
return true;
|
|
15749
|
+
}
|
|
15750
|
+
function oneLine(text) {
|
|
15751
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 400);
|
|
15752
|
+
}
|
|
15753
|
+
var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
|
|
15754
|
+
let stdout = "";
|
|
15755
|
+
let stderr = "";
|
|
15756
|
+
let child;
|
|
15757
|
+
try {
|
|
15758
|
+
child = spawn8(command, [...args], {
|
|
15759
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
15760
|
+
env: opts.env ?? process.env
|
|
15761
|
+
});
|
|
15762
|
+
} catch (err) {
|
|
15763
|
+
resolve2({ code: null, stdout, stderr: err instanceof Error ? err.message : String(err) });
|
|
15764
|
+
return;
|
|
15765
|
+
}
|
|
15766
|
+
const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : null;
|
|
15767
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
15768
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
15769
|
+
child.on("error", (err) => {
|
|
15770
|
+
if (timer) clearTimeout(timer);
|
|
15771
|
+
resolve2({ code: null, stdout, stderr: stderr || err.message });
|
|
15772
|
+
});
|
|
15773
|
+
child.on("close", (code) => {
|
|
15774
|
+
if (timer) clearTimeout(timer);
|
|
15775
|
+
resolve2({ code, stdout, stderr });
|
|
15776
|
+
});
|
|
15777
|
+
});
|
|
15778
|
+
async function stageVersion(version, harnesses, deps = {}) {
|
|
15779
|
+
const run = deps.run ?? runProcess;
|
|
15780
|
+
const node = deps.execPath ?? process.execPath;
|
|
15781
|
+
const target = join26(versionsDir(), version);
|
|
15782
|
+
const placed = versionPackageRoot(version);
|
|
15783
|
+
if (existsSync17(placed)) {
|
|
15784
|
+
if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
|
|
15785
|
+
return { ok: true, root: placed };
|
|
15786
|
+
}
|
|
15787
|
+
const npm = join26(dirname12(node), "npm");
|
|
15788
|
+
if (!existsSync17(npm))
|
|
15789
|
+
return { ok: false, reason: `there is no npm beside Node at ${dirname12(node)}` };
|
|
15790
|
+
mkdirSync16(versionsDir(), { recursive: true });
|
|
15791
|
+
const staging = join26(versionsDir(), `.${version}.staging-${process.pid}`);
|
|
15792
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
15793
|
+
const installed = await run(
|
|
15794
|
+
npm,
|
|
15795
|
+
[
|
|
15796
|
+
"install",
|
|
15797
|
+
"--prefix",
|
|
15798
|
+
staging,
|
|
15799
|
+
"--include=optional",
|
|
15800
|
+
"--no-audit",
|
|
15801
|
+
"--no-fund",
|
|
15802
|
+
"--no-save",
|
|
15803
|
+
"--loglevel=error",
|
|
15804
|
+
`@cabane/companion@${version}`
|
|
15805
|
+
],
|
|
15806
|
+
{
|
|
15807
|
+
// npm's install scripts run on this Node, not whichever `node` is first on PATH.
|
|
15808
|
+
env: { ...process.env, PATH: `${dirname12(node)}:${process.env.PATH ?? ""}` },
|
|
15809
|
+
timeoutMs: 15 * 6e4
|
|
15810
|
+
}
|
|
15811
|
+
);
|
|
15812
|
+
if (installed.code !== 0) {
|
|
15813
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
15814
|
+
if (/\bE404\b|\bETARGET\b|No matching version found|is not in this registry/i.test(
|
|
15815
|
+
installed.stderr
|
|
15816
|
+
))
|
|
15817
|
+
return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
|
|
15818
|
+
return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
|
|
15819
|
+
}
|
|
15820
|
+
const root = join26(staging, "node_modules", "@cabane", "companion");
|
|
15821
|
+
const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
|
|
15822
|
+
if (verdict) {
|
|
15823
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
15824
|
+
return { ok: false, reason: verdict };
|
|
15825
|
+
}
|
|
15826
|
+
rmSync14(target, { recursive: true, force: true });
|
|
15827
|
+
renameSync11(staging, target);
|
|
15828
|
+
return { ok: true, root: versionPackageRoot(version) };
|
|
15829
|
+
}
|
|
15830
|
+
async function verifyInstall(root, version, harnesses, deps) {
|
|
15831
|
+
if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
|
|
15832
|
+
const answered = await deps.run(deps.execPath, [join26(root, "dist", "bin.js"), "--version"], {
|
|
15833
|
+
timeoutMs: 6e4,
|
|
15834
|
+
env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
|
|
15835
|
+
});
|
|
15836
|
+
if (answered.code !== 0 || answered.stdout.trim() !== version) {
|
|
15837
|
+
return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
|
|
15838
|
+
}
|
|
15839
|
+
if (harnesses.length === 0) return null;
|
|
15840
|
+
const probed = await deps.run(deps.execPath, [join26(root, "dist", "postinstall.js"), "--json"], {
|
|
15841
|
+
timeoutMs: 6e4
|
|
15842
|
+
});
|
|
15843
|
+
let resolutions = {};
|
|
15844
|
+
try {
|
|
15845
|
+
resolutions = JSON.parse(probed.stdout);
|
|
15846
|
+
} catch {
|
|
15847
|
+
return "it couldn't say which bundled agents it has";
|
|
15848
|
+
}
|
|
15849
|
+
const missing = harnesses.filter((h) => resolutions[h]?.status !== "present");
|
|
15850
|
+
if (missing.length > 0) {
|
|
15851
|
+
return `its bundled ${missing.map(harnessName).join(" and ")} ${missing.length > 1 ? "are" : "is"} missing`;
|
|
15852
|
+
}
|
|
15853
|
+
return null;
|
|
15854
|
+
}
|
|
15855
|
+
function harnessName(h) {
|
|
15856
|
+
return h === "claude-code" ? "Claude Code" : "Codex";
|
|
15857
|
+
}
|
|
15858
|
+
function lastLine(text) {
|
|
15859
|
+
const lines2 = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
15860
|
+
return lines2.at(-1)?.slice(0, 300) ?? "";
|
|
15861
|
+
}
|
|
15862
|
+
var CompanionUpdater = class {
|
|
15863
|
+
constructor(deps) {
|
|
15864
|
+
this.deps = deps;
|
|
15865
|
+
this.now = deps.now ?? Date.now;
|
|
15866
|
+
}
|
|
15867
|
+
deps;
|
|
15868
|
+
staging = null;
|
|
15869
|
+
ready = null;
|
|
15870
|
+
swapping = false;
|
|
15871
|
+
watchdog = null;
|
|
15872
|
+
now;
|
|
15873
|
+
// At start: a recorded swap to a version that will never start again (someone installed or
|
|
15874
|
+
// chose another since) is dropped, or nothing would ever confirm it and no later update would
|
|
15875
|
+
// be installed.
|
|
15876
|
+
start() {
|
|
15877
|
+
const pending = readUpdateState().pending;
|
|
15878
|
+
const current = currentRoot();
|
|
15879
|
+
const currentVersion = current ? packageVersion(current) : null;
|
|
15880
|
+
if (superseded(pending, this.deps.own, ...currentVersion ? [currentVersion] : [])) {
|
|
15881
|
+
updateUpdateState(({ pending: _gone, ...rest }) => rest);
|
|
15882
|
+
this.deps.log.info(
|
|
15883
|
+
{ to: pending.to, own: this.deps.own },
|
|
15884
|
+
`Dropped the update to ${pending.to}: Cabane ${this.deps.own} was installed since`
|
|
15885
|
+
);
|
|
15886
|
+
}
|
|
15887
|
+
}
|
|
15888
|
+
// The deadline for an update this process is the result of: ten minutes from the first time
|
|
15889
|
+
// Cabane answers it without its stream connecting. Until Cabane answers, there's no verdict on
|
|
15890
|
+
// time: a server or network that is down says nothing about the version.
|
|
15891
|
+
armDeadline() {
|
|
15892
|
+
if (this.watchdog) return;
|
|
15893
|
+
this.watchdog = setTimeout(() => void this.giveUp(), UNCONFIRMED_WINDOW_MS);
|
|
15894
|
+
this.watchdog.unref?.();
|
|
15895
|
+
}
|
|
15896
|
+
stop() {
|
|
15897
|
+
if (this.watchdog) clearTimeout(this.watchdog);
|
|
15898
|
+
this.watchdog = null;
|
|
15899
|
+
}
|
|
15900
|
+
async giveUp() {
|
|
15901
|
+
const pending = readUpdateState().pending;
|
|
15902
|
+
if (!pending || pending.to !== this.deps.own) return;
|
|
15903
|
+
this.deps.log.warn(
|
|
15904
|
+
{ from: pending.from, to: pending.to },
|
|
15905
|
+
`Cabane ${pending.to} didn't connect within ${UNCONFIRMED_WINDOW_MS / 6e4} minutes of Cabane answering; going back to ${pending.from}`
|
|
15906
|
+
);
|
|
15907
|
+
rollBack(
|
|
15908
|
+
pending,
|
|
15909
|
+
`it didn't connect to Cabane within ${UNCONFIRMED_WINDOW_MS / 6e4} minutes`
|
|
15910
|
+
);
|
|
15911
|
+
await this.deps.restart();
|
|
15912
|
+
}
|
|
15913
|
+
// Auto-update is on and something will restart this companion into the new version.
|
|
15914
|
+
updatesItself() {
|
|
15915
|
+
return this.deps.mode === "supervised" && this.deps.autoUpdate();
|
|
15916
|
+
}
|
|
15917
|
+
// What the heartbeat reports.
|
|
15918
|
+
report() {
|
|
15919
|
+
const last = readUpdateState().lastResult;
|
|
15920
|
+
return { auto: this.updatesItself(), ...last ? { lastResult: last } : {} };
|
|
15921
|
+
}
|
|
15922
|
+
// A heartbeat landed. Record what the server named; confirm an update this process is the
|
|
15923
|
+
// result of; then, when there is something newer, install it and swap when idle.
|
|
15924
|
+
async onHeartbeat(server, streamConnected) {
|
|
15925
|
+
const state = updateUpdateState((s) => ({
|
|
15926
|
+
...s,
|
|
15927
|
+
...server ? { server: { ...server, at: new Date(this.now()).toISOString() } } : {}
|
|
15928
|
+
}));
|
|
15929
|
+
if (state.pending?.to === this.deps.own) {
|
|
15930
|
+
if (streamConnected) this.confirm(state.pending);
|
|
15931
|
+
else this.armDeadline();
|
|
15932
|
+
}
|
|
15933
|
+
if (this.ready) {
|
|
15934
|
+
await this.trySwap();
|
|
15935
|
+
return;
|
|
15936
|
+
}
|
|
15937
|
+
this.maybeStage(false);
|
|
15938
|
+
}
|
|
15939
|
+
confirm(pending) {
|
|
15940
|
+
this.stop();
|
|
15941
|
+
updateUpdateState((s) => {
|
|
15942
|
+
const { pending: _done, failed: _failed, ...rest } = s;
|
|
15943
|
+
return {
|
|
15944
|
+
...rest,
|
|
15945
|
+
lastResult: {
|
|
15946
|
+
kind: "updated",
|
|
15947
|
+
from: pending.from,
|
|
15948
|
+
to: pending.to,
|
|
15949
|
+
at: new Date(this.now()).toISOString()
|
|
15950
|
+
}
|
|
15951
|
+
};
|
|
15952
|
+
});
|
|
15953
|
+
const removed = pruneVersions([pending.to, pending.from]);
|
|
15954
|
+
this.deps.log.info(
|
|
15955
|
+
{ from: pending.from, to: pending.to, removed },
|
|
15956
|
+
`Updated Cabane from ${pending.from} to ${pending.to}`
|
|
15957
|
+
);
|
|
15958
|
+
}
|
|
15959
|
+
// `cabane update`, from the person: install the latest now, whatever the setting.
|
|
15960
|
+
async updateNow() {
|
|
15961
|
+
const state = readUpdateState();
|
|
15962
|
+
const latest = state.server?.latest;
|
|
15963
|
+
if (!latest) return { ok: false, message: "Cabane hasn't said which version is latest yet." };
|
|
15964
|
+
if (compareVersions(latest, this.deps.own) <= 0)
|
|
15965
|
+
return { ok: true, message: `Cabane ${this.deps.own} is up to date.` };
|
|
15966
|
+
if (state.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
|
|
15967
|
+
if (this.ready === latest) {
|
|
15968
|
+
void this.trySwap();
|
|
15969
|
+
return { ok: true, message: this.waitingMessage(latest) };
|
|
15970
|
+
}
|
|
15971
|
+
this.maybeStage(true);
|
|
15972
|
+
if (this.staging) await this.staging;
|
|
15973
|
+
if (this.ready !== latest) {
|
|
15974
|
+
const after = readUpdateState();
|
|
15975
|
+
if (after.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
|
|
15976
|
+
if (after.failed?.version === latest && after.failed.attempts === 0)
|
|
15977
|
+
return { ok: false, message: `Cabane ${latest} isn't published yet. Try again later.` };
|
|
15978
|
+
const failed = after.lastResult;
|
|
15979
|
+
return {
|
|
15980
|
+
ok: false,
|
|
15981
|
+
message: failed?.kind === "failed" && failed.to === latest ? `Couldn't update to ${latest}: ${failed.reason ?? "see cabane logs"}.` : `Couldn't update to ${latest}. cabane logs says why.`
|
|
15982
|
+
};
|
|
15983
|
+
}
|
|
15984
|
+
void this.trySwap();
|
|
15985
|
+
return { ok: true, message: this.waitingMessage(latest) };
|
|
15986
|
+
}
|
|
15987
|
+
waitingMessage(version) {
|
|
15988
|
+
if (this.deps.mode === "attached")
|
|
15989
|
+
return `Cabane ${version} is installed. It runs from the next cabane start.`;
|
|
15990
|
+
return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version} once the turns running now finish.`;
|
|
15991
|
+
}
|
|
15992
|
+
maybeStage(force) {
|
|
15993
|
+
if (this.staging) return;
|
|
15994
|
+
const version = versionToStage({
|
|
15995
|
+
auto: this.updatesItself(),
|
|
15996
|
+
own: this.deps.own,
|
|
15997
|
+
state: readUpdateState(),
|
|
15998
|
+
now: this.now(),
|
|
15999
|
+
force
|
|
16000
|
+
});
|
|
16001
|
+
if (!version) return;
|
|
16002
|
+
const stage = this.deps.stage ?? stageVersion;
|
|
16003
|
+
this.staging = (async () => {
|
|
16004
|
+
this.deps.log.info({ version }, `Installing Cabane ${version} beside this one`);
|
|
16005
|
+
const outcome = await stage(version, this.deps.harnesses());
|
|
16006
|
+
if (outcome.ok) {
|
|
16007
|
+
this.ready = version;
|
|
16008
|
+
updateUpdateState((s) => {
|
|
16009
|
+
const { failed: _failed, ...rest } = s;
|
|
16010
|
+
return rest;
|
|
16011
|
+
});
|
|
16012
|
+
return;
|
|
16013
|
+
}
|
|
16014
|
+
if (outcome.unpublished) {
|
|
16015
|
+
this.deps.log.info({ version }, `Cabane ${version} isn't published yet; trying later`);
|
|
16016
|
+
updateUpdateState((s) => ({
|
|
16017
|
+
...s,
|
|
16018
|
+
failed: { version, attempts: 0, at: new Date(this.now()).toISOString() }
|
|
16019
|
+
}));
|
|
16020
|
+
return;
|
|
16021
|
+
}
|
|
16022
|
+
this.deps.log.warn({ version, reason: outcome.reason }, `Couldn't install Cabane ${version}`);
|
|
16023
|
+
updateUpdateState((s) => {
|
|
16024
|
+
const attempts = s.failed?.version === version ? s.failed.attempts + 1 : 1;
|
|
16025
|
+
const at = new Date(this.now()).toISOString();
|
|
16026
|
+
return {
|
|
16027
|
+
...s,
|
|
16028
|
+
failed: { version, attempts, at },
|
|
16029
|
+
// Reported once it has failed for good, so a registry blip isn't an email.
|
|
16030
|
+
...attempts >= MAX_STAGE_ATTEMPTS || force ? {
|
|
16031
|
+
lastResult: {
|
|
16032
|
+
kind: "failed",
|
|
16033
|
+
from: this.deps.own,
|
|
16034
|
+
to: version,
|
|
16035
|
+
at,
|
|
16036
|
+
reason: oneLine(outcome.reason)
|
|
16037
|
+
}
|
|
16038
|
+
} : {}
|
|
16039
|
+
};
|
|
16040
|
+
});
|
|
16041
|
+
})().finally(() => {
|
|
16042
|
+
this.staging = null;
|
|
16043
|
+
});
|
|
16044
|
+
void this.staging.then(() => this.trySwap());
|
|
16045
|
+
}
|
|
16046
|
+
// Swap into the ready version once nothing is running. Called on each heartbeat until it can.
|
|
16047
|
+
async trySwap() {
|
|
16048
|
+
const to = this.ready;
|
|
16049
|
+
if (!to || this.swapping) return;
|
|
16050
|
+
if (readUpdateState().pending) return;
|
|
16051
|
+
if (this.deps.mode === "attached") {
|
|
16052
|
+
this.select(to);
|
|
16053
|
+
this.deps.log.info({ version: to }, `Cabane ${to} is installed; it runs from the next start`);
|
|
16054
|
+
return;
|
|
16055
|
+
}
|
|
16056
|
+
if (!this.deps.isIdle()) return;
|
|
16057
|
+
this.swapping = true;
|
|
16058
|
+
try {
|
|
16059
|
+
await this.deps.pauseStream();
|
|
16060
|
+
const sleep5 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
16061
|
+
const deadline = this.now() + SWAP_DRAIN_MS;
|
|
16062
|
+
await sleep5(250);
|
|
16063
|
+
while (!this.deps.isIdle() && this.now() < deadline) await sleep5(250);
|
|
16064
|
+
if (!this.deps.isIdle()) {
|
|
16065
|
+
this.deps.resumeStream();
|
|
16066
|
+
return;
|
|
16067
|
+
}
|
|
16068
|
+
this.select(to);
|
|
16069
|
+
this.deps.log.info({ from: this.deps.own, to }, `Restarting into Cabane ${to}`);
|
|
16070
|
+
await this.deps.restart();
|
|
16071
|
+
} finally {
|
|
16072
|
+
this.swapping = false;
|
|
16073
|
+
}
|
|
16074
|
+
}
|
|
16075
|
+
// Record the swap and point `current` at `to`, leaving the guard that counts its starts.
|
|
16076
|
+
select(to) {
|
|
16077
|
+
const fromRoot = currentRoot() ?? ownPackageRoot();
|
|
16078
|
+
writeBootGuard();
|
|
16079
|
+
updateUpdateState((s) => ({
|
|
16080
|
+
...s,
|
|
16081
|
+
pending: {
|
|
16082
|
+
from: this.deps.own,
|
|
16083
|
+
fromRoot,
|
|
16084
|
+
to,
|
|
16085
|
+
at: new Date(this.now()).toISOString(),
|
|
16086
|
+
boots: 0
|
|
16087
|
+
}
|
|
16088
|
+
}));
|
|
16089
|
+
pointCurrentAt(versionPackageRoot(to));
|
|
16090
|
+
this.ready = null;
|
|
16091
|
+
}
|
|
16092
|
+
};
|
|
16093
|
+
|
|
16094
|
+
// src/supervisor.ts
|
|
15399
16095
|
init_version2();
|
|
15400
16096
|
init_src();
|
|
15401
16097
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
16098
|
+
var ARCHIVE_PRUNE_MS = 24 * 60 * 6e4;
|
|
15402
16099
|
var SHUTDOWN_HEARTBEAT_WAIT_MS = 1e3;
|
|
15403
16100
|
function positiveIntEnv(name) {
|
|
15404
16101
|
const raw = process.env[name];
|
|
@@ -15480,6 +16177,10 @@ var CompanionSupervisor = class {
|
|
|
15480
16177
|
// Which dispatches this process is running, so a redelivered frame doesn't
|
|
15481
16178
|
// run the turn a second time.
|
|
15482
16179
|
claims = new DispatchClaims();
|
|
16180
|
+
archivePruneTimer = null;
|
|
16181
|
+
// Keeps this companion current when its person said so; null until paired.
|
|
16182
|
+
updater = null;
|
|
16183
|
+
restartForUpdate;
|
|
15483
16184
|
constructor(opts) {
|
|
15484
16185
|
this.config = opts.config;
|
|
15485
16186
|
this.log = opts.log;
|
|
@@ -15489,6 +16190,10 @@ var CompanionSupervisor = class {
|
|
|
15489
16190
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
15490
16191
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
15491
16192
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
16193
|
+
this.restartForUpdate = opts.restartForUpdate ?? (async () => {
|
|
16194
|
+
await this.shutdown();
|
|
16195
|
+
this.exitFn(0);
|
|
16196
|
+
});
|
|
15492
16197
|
this.dispatcherFactory = opts.dispatcherFactory;
|
|
15493
16198
|
if (opts.runnerCommand) this.runnerCommand = opts.runnerCommand;
|
|
15494
16199
|
if (opts.runnerProvider) this.providerForRunners = opts.runnerProvider;
|
|
@@ -15539,6 +16244,9 @@ var CompanionSupervisor = class {
|
|
|
15539
16244
|
if (!this.runnerImage) await invalidateCompanionVolume(docker, image, source);
|
|
15540
16245
|
}
|
|
15541
16246
|
this.latchBootBundledState();
|
|
16247
|
+
this.pruneSessionArchives();
|
|
16248
|
+
this.archivePruneTimer = setInterval(() => this.pruneSessionArchives(), ARCHIVE_PRUNE_MS);
|
|
16249
|
+
this.archivePruneTimer.unref?.();
|
|
15542
16250
|
}
|
|
15543
16251
|
const sandboxRows = agentSandboxRows(this.config);
|
|
15544
16252
|
if (sandboxRows.length > 0 && this.dockerRunners()) {
|
|
@@ -15548,7 +16256,7 @@ var CompanionSupervisor = class {
|
|
|
15548
16256
|
);
|
|
15549
16257
|
}
|
|
15550
16258
|
const anySandboxed = [{}, ...sandboxRows.map(([, sandbox]) => ({ sandbox }))].some(
|
|
15551
|
-
(
|
|
16259
|
+
(row3) => this.runnerSandboxFor(row3) === "os"
|
|
15552
16260
|
);
|
|
15553
16261
|
if (anySandboxed) {
|
|
15554
16262
|
const policy = this.sandboxPolicy();
|
|
@@ -15602,6 +16310,23 @@ var CompanionSupervisor = class {
|
|
|
15602
16310
|
);
|
|
15603
16311
|
}
|
|
15604
16312
|
});
|
|
16313
|
+
this.updater = new CompanionUpdater({
|
|
16314
|
+
own: COMPANION_VERSION,
|
|
16315
|
+
mode: updateMode(),
|
|
16316
|
+
autoUpdate: () => this.config.update?.auto === true,
|
|
16317
|
+
harnesses: () => this.connectedBundledHarnesses(),
|
|
16318
|
+
isIdle: () => this.idle(),
|
|
16319
|
+
pauseStream: async () => {
|
|
16320
|
+
const sub = this.deviceSub;
|
|
16321
|
+
if (!sub) return;
|
|
16322
|
+
sub.stop();
|
|
16323
|
+
await sub.finished;
|
|
16324
|
+
},
|
|
16325
|
+
resumeStream: () => void this.deviceSub?.resume(),
|
|
16326
|
+
restart: () => this.restartForUpdate(),
|
|
16327
|
+
log: this.log
|
|
16328
|
+
});
|
|
16329
|
+
this.updater.start();
|
|
15605
16330
|
await this.refreshAssignments();
|
|
15606
16331
|
this.deviceSub.start();
|
|
15607
16332
|
const firstHeartbeat = startupHarnessRefresh.then(async () => {
|
|
@@ -15662,13 +16387,20 @@ var CompanionSupervisor = class {
|
|
|
15662
16387
|
// Omitted until the first observed failure or heal, so the server's
|
|
15663
16388
|
// status-less manifest synthesis still runs for a fresh companion.
|
|
15664
16389
|
...connectorReports.length > 0 ? { connectors: connectorReports } : {},
|
|
15665
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
16390
|
+
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
16391
|
+
...this.updater ? { update: this.updater.report() } : {}
|
|
15666
16392
|
});
|
|
15667
16393
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
15668
16394
|
this.deviceId = res.deviceId;
|
|
15669
16395
|
this.checkVersionSkew(res.serverVersion);
|
|
15670
16396
|
this.heartbeated = true;
|
|
15671
16397
|
void this.recoverRuns();
|
|
16398
|
+
void this.updater?.onHeartbeat(res.companion, this.deviceConnected).catch(
|
|
16399
|
+
(err) => this.log.warn(
|
|
16400
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
16401
|
+
"Couldn't check for a Cabane update; trying again on the next check-in."
|
|
16402
|
+
)
|
|
16403
|
+
);
|
|
15672
16404
|
} catch (err) {
|
|
15673
16405
|
this.log.warn(
|
|
15674
16406
|
{ ...apiErrorLogFields(err) },
|
|
@@ -15991,8 +16723,8 @@ var CompanionSupervisor = class {
|
|
|
15991
16723
|
// The sandbox one agent's turns run under: its `agents` row, then the
|
|
15992
16724
|
// device's. Resolved from the current config on every ask, so a reload
|
|
15993
16725
|
// changes it; only the host probe is kept.
|
|
15994
|
-
runnerSandboxFor(
|
|
15995
|
-
return runnerSandboxFor(this.config,
|
|
16726
|
+
runnerSandboxFor(row3) {
|
|
16727
|
+
return runnerSandboxFor(this.config, row3, this.sandboxHost);
|
|
15996
16728
|
}
|
|
15997
16729
|
// Resolved against the agents this device runs right now.
|
|
15998
16730
|
runnerManifest() {
|
|
@@ -16699,9 +17431,54 @@ var CompanionSupervisor = class {
|
|
|
16699
17431
|
}
|
|
16700
17432
|
}
|
|
16701
17433
|
}
|
|
17434
|
+
// A long-lived device keeps a session archive per conversation and agent it ever ran; those
|
|
17435
|
+
// nobody has used in a month go.
|
|
17436
|
+
pruneSessionArchives() {
|
|
17437
|
+
try {
|
|
17438
|
+
const removed = pruneIdleSessionArchives(cabaneDir());
|
|
17439
|
+
if (removed.length > 0)
|
|
17440
|
+
this.log.info({ removed: removed.length }, "runners: removed idle session archives");
|
|
17441
|
+
} catch (err) {
|
|
17442
|
+
this.log.warn(
|
|
17443
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
17444
|
+
"runners: couldn't remove idle session archives"
|
|
17445
|
+
);
|
|
17446
|
+
}
|
|
17447
|
+
}
|
|
17448
|
+
// Nothing running and nothing about to: no dispatch claimed, no turn chained, no recovery
|
|
17449
|
+
// in progress. What a self-update swap waits for.
|
|
17450
|
+
idle() {
|
|
17451
|
+
if (this.claims.size > 0 || this.recovering) return false;
|
|
17452
|
+
for (const wr of this.workspaces.values()) if (wr.chains.size > 0) return false;
|
|
17453
|
+
return true;
|
|
17454
|
+
}
|
|
17455
|
+
// The bundled harnesses this device has connected, which a new version must still carry.
|
|
17456
|
+
connectedBundledHarnesses() {
|
|
17457
|
+
return [
|
|
17458
|
+
...isClaudeCodeConnected(this.config) ? ["claude-code"] : [],
|
|
17459
|
+
...isCodexEnabled(this.config) ? ["codex"] : []
|
|
17460
|
+
];
|
|
17461
|
+
}
|
|
17462
|
+
// `cabane update`, over the control socket: set whether this companion keeps itself current
|
|
17463
|
+
// (`auto`), or install the latest now.
|
|
17464
|
+
async update(auto) {
|
|
17465
|
+
if (auto !== void 0) {
|
|
17466
|
+
this.config = { ...this.config, update: { auto } };
|
|
17467
|
+
saveConfig(this.config);
|
|
17468
|
+
if (auto) this.kickHeartbeat();
|
|
17469
|
+
return {
|
|
17470
|
+
ok: true,
|
|
17471
|
+
message: auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
|
|
17472
|
+
};
|
|
17473
|
+
}
|
|
17474
|
+
if (!this.updater) return { ok: false, message: "This computer isn't paired yet." };
|
|
17475
|
+
return this.updater.updateNow();
|
|
17476
|
+
}
|
|
16702
17477
|
// ---- control actions ----
|
|
16703
17478
|
async shutdown() {
|
|
16704
17479
|
this.stopped = true;
|
|
17480
|
+
this.updater?.stop();
|
|
17481
|
+
if (this.archivePruneTimer) clearInterval(this.archivePruneTimer);
|
|
16705
17482
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
16706
17483
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
16707
17484
|
const heartbeat = this.inFlightHeartbeat;
|
|
@@ -16736,9 +17513,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
16736
17513
|
}
|
|
16737
17514
|
function defaultReexec() {
|
|
16738
17515
|
clearRuntimeState();
|
|
16739
|
-
void import("child_process").then(({ spawn:
|
|
17516
|
+
void import("child_process").then(({ spawn: spawn11 }) => {
|
|
16740
17517
|
try {
|
|
16741
|
-
const child =
|
|
17518
|
+
const child = spawn11(process.execPath, process.argv.slice(1), {
|
|
16742
17519
|
stdio: "inherit",
|
|
16743
17520
|
detached: false
|
|
16744
17521
|
});
|
|
@@ -16812,22 +17589,22 @@ function handleUncaught(log, err, origin) {
|
|
|
16812
17589
|
|
|
16813
17590
|
// src/crash-marker.ts
|
|
16814
17591
|
init_config();
|
|
16815
|
-
import { existsSync as
|
|
16816
|
-
import { join as
|
|
17592
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync15, rmSync as rmSync15, writeFileSync as writeFileSync11 } from "fs";
|
|
17593
|
+
import { join as join27 } from "path";
|
|
16817
17594
|
function crashMarkerPath() {
|
|
16818
|
-
return
|
|
17595
|
+
return join27(cabaneDir(), "last-error.json");
|
|
16819
17596
|
}
|
|
16820
17597
|
function recordCrash(rec2) {
|
|
16821
17598
|
try {
|
|
16822
|
-
|
|
16823
|
-
|
|
17599
|
+
mkdirSync17(cabaneDir(), { recursive: true });
|
|
17600
|
+
writeFileSync11(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
|
|
16824
17601
|
} catch {
|
|
16825
17602
|
}
|
|
16826
17603
|
}
|
|
16827
17604
|
function clearCrash() {
|
|
16828
17605
|
try {
|
|
16829
17606
|
const path = crashMarkerPath();
|
|
16830
|
-
if (
|
|
17607
|
+
if (existsSync18(path)) rmSync15(path, { force: true });
|
|
16831
17608
|
} catch {
|
|
16832
17609
|
}
|
|
16833
17610
|
}
|
|
@@ -16882,11 +17659,14 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
16882
17659
|
// process behind the marker pid is this companion (not a recycled pid).
|
|
16883
17660
|
instanceId
|
|
16884
17661
|
});
|
|
17662
|
+
let restartForUpdate = async () => {
|
|
17663
|
+
};
|
|
16885
17664
|
const supervisor = new CompanionSupervisor({
|
|
16886
17665
|
config: cfg,
|
|
16887
17666
|
log,
|
|
16888
17667
|
hub,
|
|
16889
|
-
...opts.checkHarness ? { checkHarness: opts.checkHarness } : {}
|
|
17668
|
+
...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
|
|
17669
|
+
restartForUpdate: () => restartForUpdate()
|
|
16890
17670
|
});
|
|
16891
17671
|
await supervisor.start();
|
|
16892
17672
|
const connectHarness = async (runtime, serverUrl) => {
|
|
@@ -16911,6 +17691,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
16911
17691
|
control = await startControlServer({
|
|
16912
17692
|
status: () => hub.statusJson(),
|
|
16913
17693
|
connect: (runtime, serverUrl) => connectHarness(runtime, serverUrl),
|
|
17694
|
+
update: (auto) => supervisor.update(auto),
|
|
16914
17695
|
stop: () => void supervisor.requestStop()
|
|
16915
17696
|
});
|
|
16916
17697
|
if (opts.dashboard) {
|
|
@@ -16963,6 +17744,10 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
16963
17744
|
} catch {
|
|
16964
17745
|
}
|
|
16965
17746
|
};
|
|
17747
|
+
restartForUpdate = async () => {
|
|
17748
|
+
await stop2();
|
|
17749
|
+
process.exit(0);
|
|
17750
|
+
};
|
|
16966
17751
|
return {
|
|
16967
17752
|
ok: true,
|
|
16968
17753
|
runtime: {
|
|
@@ -16984,42 +17769,438 @@ async function closeSurfaces(control, dashboard) {
|
|
|
16984
17769
|
// src/commands/start.ts
|
|
16985
17770
|
init_term();
|
|
16986
17771
|
|
|
16987
|
-
// src/
|
|
16988
|
-
init_cabane_command();
|
|
16989
|
-
init_cli_entry();
|
|
17772
|
+
// src/service.ts
|
|
16990
17773
|
init_config();
|
|
16991
|
-
import {
|
|
16992
|
-
import {
|
|
16993
|
-
|
|
16994
|
-
|
|
16995
|
-
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
17000
|
-
|
|
17001
|
-
|
|
17002
|
-
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
|
|
17006
|
-
|
|
17007
|
-
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
17011
|
-
|
|
17012
|
-
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17774
|
+
import { createHash as createHash6 } from "crypto";
|
|
17775
|
+
import {
|
|
17776
|
+
chmodSync as chmodSync7,
|
|
17777
|
+
cpSync,
|
|
17778
|
+
existsSync as existsSync19,
|
|
17779
|
+
mkdirSync as mkdirSync18,
|
|
17780
|
+
readFileSync as readFileSync16,
|
|
17781
|
+
renameSync as renameSync12,
|
|
17782
|
+
rmSync as rmSync16,
|
|
17783
|
+
writeFileSync as writeFileSync12
|
|
17784
|
+
} from "fs";
|
|
17785
|
+
import { homedir as homedir7, userInfo as userInfo3 } from "os";
|
|
17786
|
+
import { dirname as dirname13, join as join28, sep as sep5 } from "path";
|
|
17787
|
+
function servicePlatform(platform = process.platform) {
|
|
17788
|
+
if (platform === "linux") return "systemd";
|
|
17789
|
+
if (platform === "darwin") return "launchd";
|
|
17790
|
+
return null;
|
|
17791
|
+
}
|
|
17792
|
+
function serviceName(env = process.env) {
|
|
17793
|
+
const home = env.CABANE_COMPANION_HOME;
|
|
17794
|
+
if (!home) return "cabane-companion";
|
|
17795
|
+
return `cabane-companion-${createHash6("sha256").update(home).digest("hex").slice(0, 8)}`;
|
|
17796
|
+
}
|
|
17797
|
+
function launchdLabel(env = process.env) {
|
|
17798
|
+
return serviceName(env).replace(/^cabane-companion/, "ai.cabane.companion").replace("-", ".");
|
|
17799
|
+
}
|
|
17800
|
+
function serviceNode(execPath = process.execPath) {
|
|
17801
|
+
return existsSync19(pinnedNodePath()) ? pinnedNodePath() : execPath;
|
|
17802
|
+
}
|
|
17803
|
+
function servicePaths(platform, env = process.env, home = homedir7(), node = serviceNode()) {
|
|
17804
|
+
const unitFile = platform === "systemd" ? join28(
|
|
17805
|
+
env.XDG_CONFIG_HOME || join28(home, ".config"),
|
|
17806
|
+
"systemd",
|
|
17807
|
+
"user",
|
|
17808
|
+
`${serviceName(env)}.service`
|
|
17809
|
+
) : join28(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
|
|
17810
|
+
return {
|
|
17811
|
+
unitFile,
|
|
17812
|
+
envFile: join28(cabaneDir(), "service.env"),
|
|
17813
|
+
node,
|
|
17814
|
+
entry: bootGuardPath()
|
|
17815
|
+
};
|
|
17816
|
+
}
|
|
17817
|
+
function unitEnvironment(env) {
|
|
17818
|
+
return {
|
|
17819
|
+
CABANE_COMPANION_SUPERVISED: "1",
|
|
17820
|
+
...env.CABANE_COMPANION_HOME ? { CABANE_COMPANION_HOME: env.CABANE_COMPANION_HOME } : {}
|
|
17821
|
+
};
|
|
17822
|
+
}
|
|
17823
|
+
function renderSystemdUnit(paths, env = process.env) {
|
|
17824
|
+
const environment = Object.entries(unitEnvironment(env)).map(
|
|
17825
|
+
([key, value]) => `Environment=${systemdQuote(`${key}=${value}`)}`
|
|
17826
|
+
);
|
|
17827
|
+
return [
|
|
17828
|
+
"# Written by `cabane service install`. Remove with `cabane service uninstall`.",
|
|
17829
|
+
"[Unit]",
|
|
17830
|
+
"Description=Cabane companion",
|
|
17831
|
+
"Wants=network-online.target",
|
|
17832
|
+
"After=network-online.target",
|
|
17833
|
+
// Never stop restarting: a machine nobody watches has nobody to reset a failed unit.
|
|
17834
|
+
"StartLimitIntervalSec=0",
|
|
17835
|
+
"",
|
|
17836
|
+
"[Service]",
|
|
17837
|
+
"Type=simple",
|
|
17838
|
+
`ExecStart=${systemdQuote(paths.node)} ${systemdQuote(paths.entry)} start --foreground`,
|
|
17839
|
+
"Restart=always",
|
|
17840
|
+
"RestartSec=5",
|
|
17841
|
+
...environment,
|
|
17842
|
+
`EnvironmentFile=-${paths.envFile}`,
|
|
17843
|
+
"",
|
|
17844
|
+
"[Install]",
|
|
17845
|
+
"WantedBy=default.target",
|
|
17846
|
+
""
|
|
17847
|
+
].join("\n");
|
|
17848
|
+
}
|
|
17849
|
+
function systemdQuote(value) {
|
|
17850
|
+
return /[\s"\\]/.test(value) ? `"${value.replace(/(["\\])/g, "\\$1")}"` : value;
|
|
17851
|
+
}
|
|
17852
|
+
function renderLaunchdPlist(paths, env = process.env, servicePath) {
|
|
17853
|
+
const xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
17854
|
+
const vars = { ...unitEnvironment(env), PATH: servicePath };
|
|
17855
|
+
return [
|
|
17856
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
17857
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
17858
|
+
"<!-- Written by `cabane service install`. Remove with `cabane service uninstall`. -->",
|
|
17859
|
+
'<plist version="1.0">',
|
|
17860
|
+
"<dict>",
|
|
17861
|
+
` <key>Label</key><string>${xml(launchdLabel(env))}</string>`,
|
|
17862
|
+
" <key>ProgramArguments</key>",
|
|
17863
|
+
" <array>",
|
|
17864
|
+
...[paths.node, paths.entry, "start", "--foreground"].map(
|
|
17865
|
+
(arg) => ` <string>${xml(arg)}</string>`
|
|
17866
|
+
),
|
|
17867
|
+
" </array>",
|
|
17868
|
+
" <key>EnvironmentVariables</key>",
|
|
17869
|
+
" <dict>",
|
|
17870
|
+
...Object.entries(vars).map(
|
|
17871
|
+
([key, value]) => ` <key>${xml(key)}</key><string>${xml(value)}</string>`
|
|
17872
|
+
),
|
|
17873
|
+
" </dict>",
|
|
17874
|
+
" <key>RunAtLoad</key><true/>",
|
|
17875
|
+
" <key>KeepAlive</key><true/>",
|
|
17876
|
+
" <key>ThrottleInterval</key><integer>5</integer>",
|
|
17877
|
+
// The companion writes its own log; the console copy would only grow a second one.
|
|
17878
|
+
" <key>StandardOutPath</key><string>/dev/null</string>",
|
|
17879
|
+
` <key>StandardErrorPath</key><string>${xml(join28(cabaneDir(), "service-error.log"))}</string>`,
|
|
17880
|
+
"</dict>",
|
|
17881
|
+
"</plist>",
|
|
17882
|
+
""
|
|
17883
|
+
].join("\n");
|
|
17884
|
+
}
|
|
17885
|
+
function withServicePath(existing, path) {
|
|
17886
|
+
const kept = existing.split("\n").filter((line) => line.trim() && !/^\s*PATH\s*=/.test(line));
|
|
17887
|
+
return [
|
|
17888
|
+
"# The Cabane service reads this. PATH is your login shell's, written by `cabane service install`.",
|
|
17889
|
+
"# Add a headless sign-in here if this machine has no keychain, e.g. CLAUDE_CODE_OAUTH_TOKEN=\u2026",
|
|
17890
|
+
`PATH=${path}`,
|
|
17891
|
+
...kept.filter(
|
|
17892
|
+
(line) => !line.startsWith("# The Cabane service reads this") && !line.startsWith("# Add a headless sign-in here")
|
|
17893
|
+
),
|
|
17894
|
+
""
|
|
17895
|
+
].join("\n");
|
|
17896
|
+
}
|
|
17897
|
+
async function loginShellPath(run = runProcess, env = process.env) {
|
|
17898
|
+
const shell = env.SHELL;
|
|
17899
|
+
if (shell) {
|
|
17900
|
+
const r = await run(shell, ["-l", "-c", 'printf "%s" "$PATH"'], { timeoutMs: 1e4 });
|
|
17901
|
+
const path = r.stdout.trim().split("\n").at(-1)?.trim();
|
|
17902
|
+
if (r.code === 0 && path && path.includes("/")) return path;
|
|
17903
|
+
}
|
|
17904
|
+
return env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
|
17905
|
+
}
|
|
17906
|
+
async function installService(deps = {}) {
|
|
17907
|
+
const run = deps.run ?? runProcess;
|
|
17908
|
+
const env = deps.env ?? process.env;
|
|
17909
|
+
const platform = servicePlatform(deps.platform);
|
|
17910
|
+
if (!platform)
|
|
17911
|
+
return {
|
|
17912
|
+
ok: false,
|
|
17913
|
+
lines: [
|
|
17914
|
+
"Running Cabane as a service works on macOS and Linux. Start it with cabane start here."
|
|
17915
|
+
]
|
|
17916
|
+
};
|
|
17917
|
+
const node = serviceNode(deps.execPath);
|
|
17918
|
+
const seeded = await seedManagedInstall({
|
|
17919
|
+
run,
|
|
17920
|
+
execPath: node,
|
|
17921
|
+
...deps.own ? { own: deps.own } : {}
|
|
17016
17922
|
});
|
|
17017
|
-
|
|
17018
|
-
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
|
|
17022
|
-
|
|
17923
|
+
if (!seeded.ok) return { ok: false, lines: [seeded.reason] };
|
|
17924
|
+
if (!writeBootGuard(seeded.root))
|
|
17925
|
+
return {
|
|
17926
|
+
ok: false,
|
|
17927
|
+
lines: [
|
|
17928
|
+
"This install has no boot guard to run the service with. Reinstall Cabane, then run cabane service install again."
|
|
17929
|
+
]
|
|
17930
|
+
};
|
|
17931
|
+
rewriteScriptWrappers();
|
|
17932
|
+
const paths = servicePaths(platform, env, deps.home, node);
|
|
17933
|
+
const servicePath = await loginShellPath(run, env);
|
|
17934
|
+
mkdirSync18(dirname13(paths.envFile), { recursive: true });
|
|
17935
|
+
const existing = existsSync19(paths.envFile) ? readFileSync16(paths.envFile, "utf8") : "";
|
|
17936
|
+
writeFileSync12(paths.envFile, withServicePath(existing, servicePath), { mode: 384 });
|
|
17937
|
+
chmodSync7(paths.envFile, 384);
|
|
17938
|
+
mkdirSync18(dirname13(paths.unitFile), { recursive: true });
|
|
17939
|
+
const outcome = await writeAndStartUnit(platform, paths, env, servicePath, run);
|
|
17940
|
+
if (!outcome.ok) await removeUnit(platform, paths, env, run);
|
|
17941
|
+
return outcome;
|
|
17942
|
+
}
|
|
17943
|
+
async function writeAndStartUnit(platform, paths, env, servicePath, run) {
|
|
17944
|
+
const lines2 = [];
|
|
17945
|
+
if (platform === "systemd") {
|
|
17946
|
+
writeAtomic(paths.unitFile, renderSystemdUnit(paths, env));
|
|
17947
|
+
const unit = `${serviceName(env)}.service`;
|
|
17948
|
+
for (const args of [
|
|
17949
|
+
["--user", "daemon-reload"],
|
|
17950
|
+
["--user", "enable", unit],
|
|
17951
|
+
["--user", "restart", unit]
|
|
17952
|
+
]) {
|
|
17953
|
+
const r = await run("systemctl", args, { timeoutMs: 3e4 });
|
|
17954
|
+
if (r.code !== 0)
|
|
17955
|
+
return {
|
|
17956
|
+
ok: false,
|
|
17957
|
+
lines: [
|
|
17958
|
+
`systemctl ${args.join(" ")} failed: ${r.stderr.trim() || `exit ${r.code}`}`,
|
|
17959
|
+
"This needs a systemd user session. Start Cabane with cabane start instead."
|
|
17960
|
+
]
|
|
17961
|
+
};
|
|
17962
|
+
}
|
|
17963
|
+
const linger = await run("loginctl", ["show-user", userName(), "--property=Linger"], {
|
|
17964
|
+
timeoutMs: 1e4
|
|
17965
|
+
});
|
|
17966
|
+
if (linger.code === 0 && /Linger=no/.test(linger.stdout)) {
|
|
17967
|
+
lines2.push(
|
|
17968
|
+
"! It stops when you log out of this computer. To keep it running after logout, run once:",
|
|
17969
|
+
` sudo loginctl enable-linger ${userName()}`
|
|
17970
|
+
);
|
|
17971
|
+
}
|
|
17972
|
+
} else {
|
|
17973
|
+
writeAtomic(paths.unitFile, renderLaunchdPlist(paths, env, servicePath));
|
|
17974
|
+
const domain = `gui/${process.getuid?.() ?? userInfo3().uid}`;
|
|
17975
|
+
await run("launchctl", ["bootout", `${domain}/${launchdLabel(env)}`], { timeoutMs: 3e4 });
|
|
17976
|
+
const r = await run("launchctl", ["bootstrap", domain, paths.unitFile], { timeoutMs: 3e4 });
|
|
17977
|
+
if (r.code !== 0)
|
|
17978
|
+
return {
|
|
17979
|
+
ok: false,
|
|
17980
|
+
lines: [`launchctl bootstrap failed: ${r.stderr.trim() || `exit ${r.code}`}`]
|
|
17981
|
+
};
|
|
17982
|
+
}
|
|
17983
|
+
return { ok: true, lines: lines2 };
|
|
17984
|
+
}
|
|
17985
|
+
async function uninstallService(deps = {}) {
|
|
17986
|
+
const run = deps.run ?? runProcess;
|
|
17987
|
+
const env = deps.env ?? process.env;
|
|
17988
|
+
const platform = servicePlatform(deps.platform);
|
|
17989
|
+
if (!platform) return { ok: true, lines: [] };
|
|
17990
|
+
const paths = servicePaths(platform, env, deps.home);
|
|
17991
|
+
if (!existsSync19(paths.unitFile)) return { ok: true, lines: ["No Cabane service is installed."] };
|
|
17992
|
+
await removeUnit(platform, paths, env, run);
|
|
17993
|
+
return { ok: true, lines: [] };
|
|
17994
|
+
}
|
|
17995
|
+
async function removeUnit(platform, paths, env, run) {
|
|
17996
|
+
if (platform === "systemd") {
|
|
17997
|
+
await run("systemctl", ["--user", "disable", "--now", `${serviceName(env)}.service`], {
|
|
17998
|
+
timeoutMs: 3e4
|
|
17999
|
+
});
|
|
18000
|
+
rmSync16(paths.unitFile, { force: true });
|
|
18001
|
+
await run("systemctl", ["--user", "daemon-reload"], { timeoutMs: 3e4 });
|
|
18002
|
+
} else {
|
|
18003
|
+
const domain = `gui/${process.getuid?.() ?? userInfo3().uid}`;
|
|
18004
|
+
await run("launchctl", ["bootout", `${domain}/${launchdLabel(env)}`], { timeoutMs: 3e4 });
|
|
18005
|
+
rmSync16(paths.unitFile, { force: true });
|
|
18006
|
+
}
|
|
18007
|
+
}
|
|
18008
|
+
async function seedManagedInstall(deps) {
|
|
18009
|
+
const current = currentRoot();
|
|
18010
|
+
if (current && current.startsWith(versionsDir() + sep5)) return { ok: true, root: current };
|
|
18011
|
+
const own = deps.own ?? ownPackageRoot();
|
|
18012
|
+
const version = packageVersion(own);
|
|
18013
|
+
if (!version || version === "0.0.0") {
|
|
18014
|
+
pointCurrentAt(own);
|
|
18015
|
+
return { ok: true, root: own };
|
|
18016
|
+
}
|
|
18017
|
+
const target = versionPackageRoot(version);
|
|
18018
|
+
if (!existsSync19(target)) {
|
|
18019
|
+
const staging = join28(versionsDir(), `.${version}.seed-${process.pid}`);
|
|
18020
|
+
rmSync16(staging, { recursive: true, force: true });
|
|
18021
|
+
try {
|
|
18022
|
+
cpSync(own, join28(staging, "node_modules", "@cabane", "companion"), {
|
|
18023
|
+
recursive: true,
|
|
18024
|
+
verbatimSymlinks: true
|
|
18025
|
+
});
|
|
18026
|
+
rmSync16(join28(versionsDir(), version), { recursive: true, force: true });
|
|
18027
|
+
renameSync12(staging, join28(versionsDir(), version));
|
|
18028
|
+
} catch (err) {
|
|
18029
|
+
rmSync16(staging, { recursive: true, force: true });
|
|
18030
|
+
return {
|
|
18031
|
+
ok: false,
|
|
18032
|
+
reason: `Couldn't copy Cabane into ${tildeDir()}/versions for the service: ${err instanceof Error ? err.message : String(err)}`
|
|
18033
|
+
};
|
|
18034
|
+
}
|
|
18035
|
+
}
|
|
18036
|
+
const unrunnable = await verifyInstall(target, version, [], deps);
|
|
18037
|
+
if (unrunnable) {
|
|
18038
|
+
rmSync16(join28(versionsDir(), version), { recursive: true, force: true });
|
|
18039
|
+
return { ok: false, reason: `The copy of Cabane for the service can't run: ${unrunnable}.` };
|
|
18040
|
+
}
|
|
18041
|
+
pointCurrentAt(target);
|
|
18042
|
+
return { ok: true, root: target };
|
|
18043
|
+
}
|
|
18044
|
+
function tildeDir() {
|
|
18045
|
+
return cabaneDir().replace(homedir7(), "~");
|
|
18046
|
+
}
|
|
18047
|
+
function unitNode(platform, unitFile) {
|
|
18048
|
+
let text;
|
|
18049
|
+
try {
|
|
18050
|
+
text = readFileSync16(unitFile, "utf8");
|
|
18051
|
+
} catch {
|
|
18052
|
+
return null;
|
|
18053
|
+
}
|
|
18054
|
+
const found = platform === "systemd" ? /^ExecStart=("(?:[^"\\]|\\.)*"|\S+)/m.exec(text)?.[1] : /<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]*)<\/string>/.exec(text)?.[1];
|
|
18055
|
+
if (!found) return null;
|
|
18056
|
+
return found.startsWith('"') ? found.slice(1, -1).replace(/\\(.)/g, "$1") : found;
|
|
18057
|
+
}
|
|
18058
|
+
async function serviceState(deps = {}) {
|
|
18059
|
+
const run = deps.run ?? runProcess;
|
|
18060
|
+
const env = deps.env ?? process.env;
|
|
18061
|
+
const platform = servicePlatform(deps.platform);
|
|
18062
|
+
const none = {
|
|
18063
|
+
installed: false,
|
|
18064
|
+
platform,
|
|
18065
|
+
state: "not installed",
|
|
18066
|
+
pid: null,
|
|
18067
|
+
node: null,
|
|
18068
|
+
nodeOk: true
|
|
18069
|
+
};
|
|
18070
|
+
if (!platform) return none;
|
|
18071
|
+
const paths = servicePaths(platform, env, deps.home);
|
|
18072
|
+
if (!existsSync19(paths.unitFile)) return none;
|
|
18073
|
+
const node = unitNode(platform, paths.unitFile);
|
|
18074
|
+
const nodeOk = node === null || existsSync19(node);
|
|
18075
|
+
if (platform === "systemd") {
|
|
18076
|
+
const r2 = await run(
|
|
18077
|
+
"systemctl",
|
|
18078
|
+
["--user", "show", `${serviceName(env)}.service`, "--property=ActiveState,SubState,MainPID"],
|
|
18079
|
+
{ timeoutMs: 1e4 }
|
|
18080
|
+
);
|
|
18081
|
+
const prop = (name) => new RegExp(`^${name}=(.*)$`, "m").exec(r2.stdout)?.[1] ?? "";
|
|
18082
|
+
const active = prop("ActiveState");
|
|
18083
|
+
const pid2 = Number(prop("MainPID")) || null;
|
|
18084
|
+
return {
|
|
18085
|
+
installed: true,
|
|
18086
|
+
platform,
|
|
18087
|
+
state: active === "active" ? "running" : prop("SubState") || active || "unknown",
|
|
18088
|
+
pid: pid2,
|
|
18089
|
+
node,
|
|
18090
|
+
nodeOk
|
|
18091
|
+
};
|
|
18092
|
+
}
|
|
18093
|
+
const domain = `gui/${process.getuid?.() ?? userInfo3().uid}`;
|
|
18094
|
+
const r = await run("launchctl", ["print", `${domain}/${launchdLabel(env)}`], {
|
|
18095
|
+
timeoutMs: 1e4
|
|
18096
|
+
});
|
|
18097
|
+
if (r.code !== 0)
|
|
18098
|
+
return { installed: true, platform, state: "not loaded", pid: null, node, nodeOk };
|
|
18099
|
+
const state = /^\s*state = (.+)$/m.exec(r.stdout)?.[1]?.trim() ?? "unknown";
|
|
18100
|
+
const pid = Number(/^\s*pid = (\d+)$/m.exec(r.stdout)?.[1]) || null;
|
|
18101
|
+
return { installed: true, platform, state, pid, node, nodeOk };
|
|
18102
|
+
}
|
|
18103
|
+
function serviceInstalled(deps = {}) {
|
|
18104
|
+
const platform = servicePlatform(deps.platform);
|
|
18105
|
+
if (!platform) return false;
|
|
18106
|
+
return existsSync19(servicePaths(platform, deps.env ?? process.env, deps.home).unitFile);
|
|
18107
|
+
}
|
|
18108
|
+
function writeAtomic(path, content) {
|
|
18109
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
18110
|
+
writeFileSync12(tmp, content);
|
|
18111
|
+
renameSync12(tmp, path);
|
|
18112
|
+
}
|
|
18113
|
+
function userName() {
|
|
18114
|
+
try {
|
|
18115
|
+
return userInfo3().username;
|
|
18116
|
+
} catch {
|
|
18117
|
+
return process.env.USER ?? "";
|
|
18118
|
+
}
|
|
18119
|
+
}
|
|
18120
|
+
function rewriteScriptWrappers(dir2 = join28(cabaneDir(), "bin")) {
|
|
18121
|
+
for (const name of ["cabane", "cabane-companion"]) {
|
|
18122
|
+
const path = join28(dir2, name);
|
|
18123
|
+
let text;
|
|
18124
|
+
try {
|
|
18125
|
+
text = readFileSync16(path, "utf8");
|
|
18126
|
+
} catch {
|
|
18127
|
+
continue;
|
|
18128
|
+
}
|
|
18129
|
+
if (!text.includes("Written by https://cabane.ai/install.sh")) continue;
|
|
18130
|
+
if (!text.includes("/dist/cli.js")) continue;
|
|
18131
|
+
writeAtomic(path, text.replace("/dist/cli.js", "/dist/bin.js"));
|
|
18132
|
+
chmodSync7(path, 493);
|
|
18133
|
+
}
|
|
18134
|
+
}
|
|
18135
|
+
async function startService(deps = {}) {
|
|
18136
|
+
return controlService("start", deps);
|
|
18137
|
+
}
|
|
18138
|
+
async function stopService(deps = {}) {
|
|
18139
|
+
return controlService("stop", deps);
|
|
18140
|
+
}
|
|
18141
|
+
async function controlService(action, deps) {
|
|
18142
|
+
const run = deps.run ?? runProcess;
|
|
18143
|
+
const env = deps.env ?? process.env;
|
|
18144
|
+
const platform = servicePlatform(deps.platform);
|
|
18145
|
+
if (!platform) return { ok: false, lines: [] };
|
|
18146
|
+
const paths = servicePaths(platform, env, deps.home);
|
|
18147
|
+
const r = platform === "systemd" ? await run("systemctl", ["--user", action, `${serviceName(env)}.service`], {
|
|
18148
|
+
timeoutMs: 3e4
|
|
18149
|
+
}) : action === "start" ? await run(
|
|
18150
|
+
"launchctl",
|
|
18151
|
+
["bootstrap", `gui/${process.getuid?.() ?? userInfo3().uid}`, paths.unitFile],
|
|
18152
|
+
{
|
|
18153
|
+
timeoutMs: 3e4
|
|
18154
|
+
}
|
|
18155
|
+
) : await run(
|
|
18156
|
+
"launchctl",
|
|
18157
|
+
["bootout", `gui/${process.getuid?.() ?? userInfo3().uid}/${launchdLabel(env)}`],
|
|
18158
|
+
{
|
|
18159
|
+
timeoutMs: 3e4
|
|
18160
|
+
}
|
|
18161
|
+
);
|
|
18162
|
+
return r.code === 0 ? { ok: true, lines: [] } : {
|
|
18163
|
+
ok: false,
|
|
18164
|
+
lines: [`Couldn't ${action} the Cabane service: ${r.stderr.trim() || `exit ${r.code}`}`]
|
|
18165
|
+
};
|
|
18166
|
+
}
|
|
18167
|
+
|
|
18168
|
+
// src/commands/daemon.ts
|
|
18169
|
+
init_cabane_command();
|
|
18170
|
+
init_cli_entry();
|
|
18171
|
+
init_config();
|
|
18172
|
+
import { spawn as spawn9 } from "child_process";
|
|
18173
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync19, openSync as openSync4 } from "fs";
|
|
18174
|
+
var STARTUP_TIMEOUT_MS = 8e3;
|
|
18175
|
+
var POLL_INTERVAL_MS = 150;
|
|
18176
|
+
async function startDaemon(opts = {}, deps = {}) {
|
|
18177
|
+
const ensureRuntime = deps.ensureRuntime ?? warnAboutHarnessReadiness;
|
|
18178
|
+
const readState = deps.readState ?? readLiveRuntimeState;
|
|
18179
|
+
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
18180
|
+
const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
|
|
18181
|
+
const sleep5 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
18182
|
+
const now = deps.now ?? (() => Date.now());
|
|
18183
|
+
const quiet = opts.report === "failures";
|
|
18184
|
+
const cfg = requireStartConfig({
|
|
18185
|
+
// Unset → `requireStartConfig`'s own `requireConfig`, the real one.
|
|
18186
|
+
...deps.requireCfg ? { requireCfg: deps.requireCfg } : {},
|
|
18187
|
+
...deps.save ? { save: deps.save } : {},
|
|
18188
|
+
// The child won't log it (the block is written by then), and this process owns
|
|
18189
|
+
// the user's terminal — so the one-time record is one line, here.
|
|
18190
|
+
onMigrated: () => {
|
|
18191
|
+
if (!quiet) {
|
|
18192
|
+
process.stdout.write(
|
|
18193
|
+
"Carried Claude Code over as a connected harness on this device \u2014 connectors are chosen now, not detected.\n"
|
|
18194
|
+
);
|
|
18195
|
+
}
|
|
18196
|
+
}
|
|
18197
|
+
});
|
|
18198
|
+
ensureRuntime(cfg, {
|
|
18199
|
+
// The onboarding script already said what's connectable, in its own
|
|
18200
|
+
// words. Saying it again here, on stderr, in the middle of the closing block
|
|
18201
|
+
// would be the same news twice.
|
|
18202
|
+
...quiet ? { warn: () => {
|
|
18203
|
+
} } : {}
|
|
17023
18204
|
});
|
|
17024
18205
|
const existing = readState();
|
|
17025
18206
|
if (existing) {
|
|
@@ -17079,10 +18260,10 @@ function alreadyRunningLines(pid) {
|
|
|
17079
18260
|
}
|
|
17080
18261
|
function defaultSpawnDetached(args) {
|
|
17081
18262
|
const cliPath = companionCliEntry();
|
|
17082
|
-
|
|
17083
|
-
const logFd =
|
|
18263
|
+
mkdirSync19(cabaneDir(), { recursive: true });
|
|
18264
|
+
const logFd = openSync4(companionLogPath(), "a");
|
|
17084
18265
|
try {
|
|
17085
|
-
return
|
|
18266
|
+
return spawn9(process.execPath, [cliPath, ...args], {
|
|
17086
18267
|
detached: true,
|
|
17087
18268
|
stdio: ["ignore", logFd, logFd],
|
|
17088
18269
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -17092,6 +18273,209 @@ function defaultSpawnDetached(args) {
|
|
|
17092
18273
|
}
|
|
17093
18274
|
}
|
|
17094
18275
|
|
|
18276
|
+
// src/commands/service.ts
|
|
18277
|
+
init_cabane_command();
|
|
18278
|
+
init_config();
|
|
18279
|
+
init_term();
|
|
18280
|
+
init_version2();
|
|
18281
|
+
|
|
18282
|
+
// src/commands/stop.ts
|
|
18283
|
+
var TERM_GRACE_MS = 6e3;
|
|
18284
|
+
var POLL_INTERVAL_MS2 = 150;
|
|
18285
|
+
async function stop(deps = {}) {
|
|
18286
|
+
if ((deps.serviceInstalled ?? serviceInstalled)()) {
|
|
18287
|
+
const outcome = await (deps.stopService ?? stopService)();
|
|
18288
|
+
for (const line of outcome.lines) process.stdout.write(`${line}
|
|
18289
|
+
`);
|
|
18290
|
+
if (!outcome.ok) {
|
|
18291
|
+
process.exitCode = 1;
|
|
18292
|
+
return;
|
|
18293
|
+
}
|
|
18294
|
+
process.stdout.write(
|
|
18295
|
+
"companion: stopped the service. It starts again at boot, or with `cabane start`.\n"
|
|
18296
|
+
);
|
|
18297
|
+
return;
|
|
18298
|
+
}
|
|
18299
|
+
const readState = deps.readState ?? readLiveRuntimeState;
|
|
18300
|
+
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
18301
|
+
const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
|
|
18302
|
+
const sleep5 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
18303
|
+
const now = deps.now ?? (() => Date.now());
|
|
18304
|
+
const state = readState();
|
|
18305
|
+
if (!state) {
|
|
18306
|
+
process.stdout.write("companion: not running (nothing to stop).\n");
|
|
18307
|
+
return;
|
|
18308
|
+
}
|
|
18309
|
+
if (await verify(state) === "stale") {
|
|
18310
|
+
clearRuntimeState();
|
|
18311
|
+
process.stdout.write("companion: not running (stale marker swept).\n");
|
|
18312
|
+
return;
|
|
18313
|
+
}
|
|
18314
|
+
const { pid } = state;
|
|
18315
|
+
process.stdout.write(`companion: stopping (pid ${pid})\u2026
|
|
18316
|
+
`);
|
|
18317
|
+
try {
|
|
18318
|
+
kill(pid, "SIGTERM");
|
|
18319
|
+
} catch {
|
|
18320
|
+
clearRuntimeState();
|
|
18321
|
+
process.stdout.write("companion: already stopped.\n");
|
|
18322
|
+
return;
|
|
18323
|
+
}
|
|
18324
|
+
const deadline = now() + TERM_GRACE_MS;
|
|
18325
|
+
while (now() < deadline) {
|
|
18326
|
+
if (!isAlive(kill, pid)) {
|
|
18327
|
+
process.stdout.write("companion: stopped.\n");
|
|
18328
|
+
return;
|
|
18329
|
+
}
|
|
18330
|
+
await sleep5(POLL_INTERVAL_MS2);
|
|
18331
|
+
}
|
|
18332
|
+
process.stdout.write(
|
|
18333
|
+
`companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
|
|
18334
|
+
`
|
|
18335
|
+
);
|
|
18336
|
+
try {
|
|
18337
|
+
kill(pid, "SIGKILL");
|
|
18338
|
+
} catch {
|
|
18339
|
+
}
|
|
18340
|
+
clearRuntimeState();
|
|
18341
|
+
process.stdout.write("companion: force-stopped.\n");
|
|
18342
|
+
}
|
|
18343
|
+
function isAlive(kill, pid) {
|
|
18344
|
+
try {
|
|
18345
|
+
kill(pid, 0);
|
|
18346
|
+
return true;
|
|
18347
|
+
} catch {
|
|
18348
|
+
return false;
|
|
18349
|
+
}
|
|
18350
|
+
}
|
|
18351
|
+
|
|
18352
|
+
// src/commands/service.ts
|
|
18353
|
+
var STARTUP_WAIT_MS = 2e4;
|
|
18354
|
+
async function installAndStartService(deps = {}) {
|
|
18355
|
+
const readState = deps.readState ?? readLiveRuntimeState;
|
|
18356
|
+
const sleep5 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
18357
|
+
const now = deps.now ?? Date.now;
|
|
18358
|
+
const before = await serviceState(deps);
|
|
18359
|
+
const running = readState();
|
|
18360
|
+
if (running && running.pid !== before.pid) await (deps.stopRunning ?? (() => stop()))();
|
|
18361
|
+
setRunsAsService(true);
|
|
18362
|
+
const outcome = await installService(deps);
|
|
18363
|
+
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
18364
|
+
if (!outcome.ok) {
|
|
18365
|
+
setRunsAsService(false);
|
|
18366
|
+
return false;
|
|
18367
|
+
}
|
|
18368
|
+
const deadline = now() + STARTUP_WAIT_MS;
|
|
18369
|
+
while (now() < deadline) {
|
|
18370
|
+
const state = readState();
|
|
18371
|
+
if (state?.socket) return true;
|
|
18372
|
+
await sleep5(250);
|
|
18373
|
+
}
|
|
18374
|
+
write(
|
|
18375
|
+
`${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
|
|
18376
|
+
);
|
|
18377
|
+
return false;
|
|
18378
|
+
}
|
|
18379
|
+
async function serviceInstall(deps = {}) {
|
|
18380
|
+
if (!await installAndStartService(deps)) {
|
|
18381
|
+
process.exitCode = 1;
|
|
18382
|
+
return;
|
|
18383
|
+
}
|
|
18384
|
+
write(
|
|
18385
|
+
INDENT + tick(
|
|
18386
|
+
"Cabane runs as a service. It starts at boot, restarts if it stops, and updates itself between turns."
|
|
18387
|
+
)
|
|
18388
|
+
);
|
|
18389
|
+
write(`${INDENT} Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
|
|
18390
|
+
}
|
|
18391
|
+
async function serviceUninstall(deps = {}) {
|
|
18392
|
+
const outcome = await uninstallService(deps);
|
|
18393
|
+
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
18394
|
+
if (outcome.lines.length > 0) return;
|
|
18395
|
+
setRunsAsService(false);
|
|
18396
|
+
write(
|
|
18397
|
+
INDENT + tick("Removed the service. Cabane won't start at boot, and it isn't running now.")
|
|
18398
|
+
);
|
|
18399
|
+
write(`${INDENT} Start it again with ${cabane("start")}`);
|
|
18400
|
+
}
|
|
18401
|
+
async function serviceStatus(deps = {}) {
|
|
18402
|
+
const state = await serviceState(deps);
|
|
18403
|
+
for (const line of serviceRows(state)) write(line);
|
|
18404
|
+
for (const line of updateRows(readUpdateState(), safeConfig()?.update?.auto, {
|
|
18405
|
+
service: state.installed
|
|
18406
|
+
}))
|
|
18407
|
+
write(line);
|
|
18408
|
+
if (state.installed && state.state !== "running") process.exitCode = 1;
|
|
18409
|
+
}
|
|
18410
|
+
var LABEL_WIDTH = 13;
|
|
18411
|
+
var row = (label, value) => ` ${label.padEnd(LABEL_WIDTH)}${value}`;
|
|
18412
|
+
function serviceRows(state) {
|
|
18413
|
+
if (!state.installed) {
|
|
18414
|
+
return [
|
|
18415
|
+
row(
|
|
18416
|
+
"Service",
|
|
18417
|
+
state.platform ? `not installed \u2014 ${cabane("service install")} starts Cabane at boot` : "not available on this system"
|
|
18418
|
+
)
|
|
18419
|
+
];
|
|
18420
|
+
}
|
|
18421
|
+
const via = state.platform === "systemd" ? "systemd" : "launchd";
|
|
18422
|
+
const rows = [
|
|
18423
|
+
row(
|
|
18424
|
+
"Service",
|
|
18425
|
+
state.state === "running" ? `running (${via}${state.pid ? `, pid ${state.pid}` : ""})` : `installed, not running (${via}: ${state.state})`
|
|
18426
|
+
)
|
|
18427
|
+
];
|
|
18428
|
+
if (!state.nodeOk) {
|
|
18429
|
+
rows.push(
|
|
18430
|
+
` ! The service's Node at ${tildePath(state.node ?? "")} is gone. Run ${cabane("service install")} again.`
|
|
18431
|
+
);
|
|
18432
|
+
}
|
|
18433
|
+
return rows;
|
|
18434
|
+
}
|
|
18435
|
+
function updateRows(state, auto, opts = {}) {
|
|
18436
|
+
const rows = [];
|
|
18437
|
+
if (opts.version !== false) rows.push(row("Version", COMPANION_VERSION));
|
|
18438
|
+
if (state.server) {
|
|
18439
|
+
rows.push(
|
|
18440
|
+
row(
|
|
18441
|
+
"Cabane wants",
|
|
18442
|
+
`${state.server.latest} (the latest), ${state.server.minimum} or newer to run turns`
|
|
18443
|
+
)
|
|
18444
|
+
);
|
|
18445
|
+
}
|
|
18446
|
+
rows.push(
|
|
18447
|
+
row(
|
|
18448
|
+
"Auto-update",
|
|
18449
|
+
!opts.service ? `off \u2014 runs with the service: ${cabane("service install")}` : auto === true ? "on \u2014 updates install between turns and roll back if they fail" : `off \u2014 ${cabane("update --auto on")} turns it on`
|
|
18450
|
+
)
|
|
18451
|
+
);
|
|
18452
|
+
const last = state.lastResult;
|
|
18453
|
+
if (last) {
|
|
18454
|
+
const when = new Date(last.at).toLocaleDateString("en-GB", { day: "numeric", month: "short" });
|
|
18455
|
+
const why = last.reason ? `: ${last.reason}` : "";
|
|
18456
|
+
rows.push(
|
|
18457
|
+
row(
|
|
18458
|
+
"Last update",
|
|
18459
|
+
last.kind === "updated" ? `${last.from} \u2192 ${last.to} on ${when}` : last.kind === "rolled_back" ? `${last.to} rolled back to ${last.from} on ${when}${why}` : `${last.to} failed on ${when}${why}`
|
|
18460
|
+
)
|
|
18461
|
+
);
|
|
18462
|
+
}
|
|
18463
|
+
return rows;
|
|
18464
|
+
}
|
|
18465
|
+
function safeConfig() {
|
|
18466
|
+
try {
|
|
18467
|
+
return loadConfig();
|
|
18468
|
+
} catch {
|
|
18469
|
+
return null;
|
|
18470
|
+
}
|
|
18471
|
+
}
|
|
18472
|
+
function setRunsAsService(on) {
|
|
18473
|
+
const cfg = safeConfig();
|
|
18474
|
+
if (!cfg) return;
|
|
18475
|
+
const { serviceDeclined: _declined, ...rest } = cfg;
|
|
18476
|
+
saveConfig({ ...rest, update: { auto: on } });
|
|
18477
|
+
}
|
|
18478
|
+
|
|
17095
18479
|
// src/commands/start.ts
|
|
17096
18480
|
var FORCE_EXIT_MS = 4e3;
|
|
17097
18481
|
var MAX_CODES = 3;
|
|
@@ -17116,6 +18500,10 @@ async function startScript(opts, interactive) {
|
|
|
17116
18500
|
await startSupervised(opts, interactive, daemon);
|
|
17117
18501
|
return;
|
|
17118
18502
|
}
|
|
18503
|
+
if (serviceInstalled()) {
|
|
18504
|
+
await startTheService();
|
|
18505
|
+
return;
|
|
18506
|
+
}
|
|
17119
18507
|
await startForPerson(opts, interactive);
|
|
17120
18508
|
}
|
|
17121
18509
|
async function surveyAgents(cfg, deps = {}) {
|
|
@@ -17499,7 +18887,7 @@ function openLink(link, spinner) {
|
|
|
17499
18887
|
`${INDENT}Couldn't open a browser here. Open this link on any device: ${link}`
|
|
17500
18888
|
);
|
|
17501
18889
|
try {
|
|
17502
|
-
const child =
|
|
18890
|
+
const child = spawn10(opener, [link], { stdio: "ignore", detached: true });
|
|
17503
18891
|
child.once("error", failed);
|
|
17504
18892
|
child.once("exit", (code) => {
|
|
17505
18893
|
if (code !== 0) failed();
|
|
@@ -17510,32 +18898,67 @@ function openLink(link, spinner) {
|
|
|
17510
18898
|
}
|
|
17511
18899
|
}
|
|
17512
18900
|
async function handOffToBackground(runtime, ctx2) {
|
|
18901
|
+
const { asService } = ctx2.justPaired ? await askHowToRun() : { asService: false };
|
|
17513
18902
|
await runtime.stop();
|
|
17514
|
-
|
|
17515
|
-
|
|
17516
|
-
|
|
18903
|
+
if (asService) {
|
|
18904
|
+
if (!await installAndStartService()) {
|
|
18905
|
+
write(`${INDENT}Running it in the background instead.`);
|
|
18906
|
+
if (!(await startDaemon({ report: "failures", ...ctx2 })).started) return;
|
|
18907
|
+
}
|
|
18908
|
+
} else {
|
|
18909
|
+
const outcome = await startDaemon({ report: "failures", ...ctx2 });
|
|
18910
|
+
if (!outcome.started) {
|
|
18911
|
+
return;
|
|
18912
|
+
}
|
|
17517
18913
|
}
|
|
18914
|
+
const supervised = asService && serviceInstalled();
|
|
17518
18915
|
for (const label of ctx2.connected) write(INDENT + tick(`${label} connected.`));
|
|
17519
18916
|
for (const runtime2 of ctx2.signedOut) {
|
|
17520
18917
|
write(
|
|
17521
18918
|
`${INDENT}! ${HARNESS_LABELS[runtime2]} is connected here but signed out. Run ${loginCommand(runtime2)} to use it again.`
|
|
17522
18919
|
);
|
|
17523
18920
|
}
|
|
18921
|
+
const where = supervised ? "as a service, and starts at boot" : "in the background";
|
|
17524
18922
|
write(
|
|
17525
18923
|
INDENT + tick(
|
|
17526
|
-
ctx2.justPaired ?
|
|
18924
|
+
ctx2.justPaired ? `Cabane is running ${where}. You can close this window.` : `Cabane is running ${where}.`
|
|
17527
18925
|
)
|
|
17528
18926
|
);
|
|
17529
18927
|
write(` Stop: ${cabane("stop")} Logs: ${cabane("logs")}`);
|
|
17530
18928
|
if (ctx2.justPaired) {
|
|
17531
|
-
|
|
17532
|
-
|
|
17533
|
-
|
|
17534
|
-
|
|
18929
|
+
if (!supervised) {
|
|
18930
|
+
blank();
|
|
18931
|
+
write(
|
|
18932
|
+
`${INDENT}! It won't restart on its own after a reboot \u2014 run ${cabane("start")} again, or ${cabane("service install")} to start it at boot.`
|
|
18933
|
+
);
|
|
18934
|
+
}
|
|
17535
18935
|
blank();
|
|
17536
18936
|
write(`${INDENT}All set on this side. Finish up in Cabane.`);
|
|
17537
18937
|
}
|
|
17538
18938
|
}
|
|
18939
|
+
async function askHowToRun(deps = {}) {
|
|
18940
|
+
const ask = deps.confirm ?? confirm;
|
|
18941
|
+
const cfg = safeLoadConfig();
|
|
18942
|
+
if (!cfg || !servicePlatform() || cfg.serviceDeclined || serviceInstalled())
|
|
18943
|
+
return { asService: false };
|
|
18944
|
+
blank();
|
|
18945
|
+
const asService = await ask(
|
|
18946
|
+
"Keep Cabane running and up to date on this computer? It starts at boot, restarts if it stops, and updates itself between turns."
|
|
18947
|
+
);
|
|
18948
|
+
if (!asService) saveConfig({ ...cfg, serviceDeclined: true });
|
|
18949
|
+
blank();
|
|
18950
|
+
return { asService };
|
|
18951
|
+
}
|
|
18952
|
+
async function startTheService() {
|
|
18953
|
+
const outcome = await startService();
|
|
18954
|
+
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
18955
|
+
if (!outcome.ok) {
|
|
18956
|
+
process.exitCode = 1;
|
|
18957
|
+
return;
|
|
18958
|
+
}
|
|
18959
|
+
write(INDENT + tick("Cabane runs as a service here, and is starting."));
|
|
18960
|
+
write(` Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
|
|
18961
|
+
}
|
|
17539
18962
|
async function runAttached(runtime) {
|
|
17540
18963
|
await new Promise((resolve2) => {
|
|
17541
18964
|
let shuttingDown = false;
|
|
@@ -17607,7 +19030,7 @@ async function pair(opts = {}, deps = {}) {
|
|
|
17607
19030
|
}
|
|
17608
19031
|
|
|
17609
19032
|
// src/cli.ts
|
|
17610
|
-
import { readFileSync as
|
|
19033
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
17611
19034
|
|
|
17612
19035
|
// src/commands/logs.ts
|
|
17613
19036
|
init_cabane_command();
|
|
@@ -17691,18 +19114,18 @@ async function logs(opts = {}) {
|
|
|
17691
19114
|
// src/commands/status.ts
|
|
17692
19115
|
init_bundled_binaries();
|
|
17693
19116
|
init_cabane_command();
|
|
17694
|
-
import { readdirSync as
|
|
17695
|
-
import { homedir as
|
|
17696
|
-
import { dirname as
|
|
19117
|
+
import { readdirSync as readdirSync7, realpathSync as realpathSync7, existsSync as existsSync20 } from "fs";
|
|
19118
|
+
import { homedir as homedir8 } from "os";
|
|
19119
|
+
import { dirname as dirname14, join as join29 } from "path";
|
|
17697
19120
|
init_config();
|
|
17698
19121
|
init_sandbox_policy();
|
|
17699
19122
|
init_secrets();
|
|
17700
19123
|
init_term();
|
|
17701
19124
|
init_transcript_writer();
|
|
17702
19125
|
init_version2();
|
|
17703
|
-
var
|
|
17704
|
-
function
|
|
17705
|
-
return ` ${label.padEnd(
|
|
19126
|
+
var LABEL_WIDTH2 = 13;
|
|
19127
|
+
function row2(label, value) {
|
|
19128
|
+
return ` ${label.padEnd(LABEL_WIDTH2)}${value}`;
|
|
17706
19129
|
}
|
|
17707
19130
|
async function status() {
|
|
17708
19131
|
const out = (line) => void process.stdout.write(`${line}
|
|
@@ -17720,29 +19143,36 @@ async function status() {
|
|
|
17720
19143
|
process.exitCode = 1;
|
|
17721
19144
|
return;
|
|
17722
19145
|
}
|
|
17723
|
-
out(
|
|
19146
|
+
out(row2("Paired with", `${cfg.baseUrl}${cfg.deviceLabel ? ` as ${cfg.deviceLabel}` : ""}`));
|
|
17724
19147
|
const running = await liveState();
|
|
17725
19148
|
let harnesses = null;
|
|
17726
19149
|
if (running) {
|
|
17727
19150
|
const mode = running.daemon ? "in the background" : "in a terminal";
|
|
17728
19151
|
const uptime = formatUptime(running.startedAt);
|
|
17729
|
-
out(
|
|
19152
|
+
out(row2("Running", `${mode} (pid ${running.pid}${uptime ? `, up ${uptime}` : ""})`));
|
|
17730
19153
|
harnesses = await runningHarnesses(running);
|
|
17731
19154
|
} else {
|
|
17732
|
-
out(
|
|
17733
|
-
}
|
|
19155
|
+
out(row2("Running", `no \u2014 start it with ${cabane("start")}`));
|
|
19156
|
+
}
|
|
19157
|
+
const service2 = await serviceState();
|
|
19158
|
+
for (const line of serviceRows(service2)) out(line);
|
|
19159
|
+
for (const line of updateRows(readUpdateState(), cfg.update?.auto, {
|
|
19160
|
+
version: false,
|
|
19161
|
+
service: service2.installed
|
|
19162
|
+
}))
|
|
19163
|
+
out(line);
|
|
17734
19164
|
for (const line of await agentLines(cfg, harnesses)) out(line);
|
|
17735
|
-
out(
|
|
17736
|
-
out(
|
|
19165
|
+
out(row2("Logs", `${cabane("logs")} (${tildePath(companionLogPath())})`));
|
|
19166
|
+
out(row2("Transcripts", transcriptsLine()));
|
|
17737
19167
|
const names = loadSecretStoreTolerant().names();
|
|
17738
19168
|
out(
|
|
17739
|
-
names.length > 0 ?
|
|
19169
|
+
names.length > 0 ? row2("Secrets", `${names.length} declared (${names.join(", ")})`) : row2(
|
|
17740
19170
|
"Secrets",
|
|
17741
19171
|
"none declared \u2014 add `${VAR}` values to ~/.cabane/secrets.json for MCP servers that need them"
|
|
17742
19172
|
)
|
|
17743
19173
|
);
|
|
17744
19174
|
if (cfg.prepareHook) {
|
|
17745
|
-
out(
|
|
19175
|
+
out(row2("Prepare hook", `${cfg.prepareHook.command} (device-level, all agents)`));
|
|
17746
19176
|
}
|
|
17747
19177
|
for (const line of sandboxStatusLines(cfg)) out(line);
|
|
17748
19178
|
const overrides2 = Object.keys(cfg.agents ?? {});
|
|
@@ -17752,10 +19182,10 @@ function versionLine() {
|
|
|
17752
19182
|
const install = companionInstall();
|
|
17753
19183
|
const own = installedCommandPath({ install });
|
|
17754
19184
|
if (install === "script") {
|
|
17755
|
-
return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(
|
|
19185
|
+
return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join29(homedir8(), ".cabane"))})`;
|
|
17756
19186
|
}
|
|
17757
19187
|
if (own)
|
|
17758
|
-
return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(
|
|
19188
|
+
return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname14(dirname14(own)))})`;
|
|
17759
19189
|
return `Cabane ${COMPANION_VERSION}`;
|
|
17760
19190
|
}
|
|
17761
19191
|
function otherInstallLines(running) {
|
|
@@ -17772,14 +19202,14 @@ function otherInstallLines(running) {
|
|
|
17772
19202
|
function otherInstall(install, own) {
|
|
17773
19203
|
const real = (p) => {
|
|
17774
19204
|
try {
|
|
17775
|
-
return
|
|
19205
|
+
return realpathSync7(p);
|
|
17776
19206
|
} catch {
|
|
17777
19207
|
return null;
|
|
17778
19208
|
}
|
|
17779
19209
|
};
|
|
17780
19210
|
if (install === "npm") {
|
|
17781
|
-
const script =
|
|
17782
|
-
|
|
19211
|
+
const script = join29(
|
|
19212
|
+
homedir8(),
|
|
17783
19213
|
".cabane",
|
|
17784
19214
|
"node",
|
|
17785
19215
|
"lib",
|
|
@@ -17787,12 +19217,12 @@ function otherInstall(install, own) {
|
|
|
17787
19217
|
"@cabane",
|
|
17788
19218
|
"companion"
|
|
17789
19219
|
);
|
|
17790
|
-
return
|
|
19220
|
+
return existsSync20(script) ? join29(homedir8(), ".cabane") : null;
|
|
17791
19221
|
}
|
|
17792
19222
|
const found = firstOnPath("cabane-companion", process.env.PATH);
|
|
17793
19223
|
if (!found) return null;
|
|
17794
|
-
const ownAlias = own ? real(
|
|
17795
|
-
return real(found) === ownAlias ? null :
|
|
19224
|
+
const ownAlias = own ? real(join29(dirname14(own), "cabane-companion")) : null;
|
|
19225
|
+
return real(found) === ownAlias ? null : dirname14(found);
|
|
17796
19226
|
}
|
|
17797
19227
|
async function liveState() {
|
|
17798
19228
|
const running = readLiveRuntimeState();
|
|
@@ -17819,18 +19249,18 @@ async function agentLines(cfg, running) {
|
|
|
17819
19249
|
const label = HARNESS_LABELS[runtime];
|
|
17820
19250
|
const connected = runtime === "codex" ? !!cfg.codex && cfg.codex.enabled !== false : !!cfg.claudeCode && cfg.claudeCode.enabled !== false;
|
|
17821
19251
|
if (!connected) {
|
|
17822
|
-
lines2.push(
|
|
19252
|
+
lines2.push(row2(label, "not connected"));
|
|
17823
19253
|
continue;
|
|
17824
19254
|
}
|
|
17825
19255
|
const fromRunning = running?.find((h) => h.runtime === runtime);
|
|
17826
19256
|
const signIn = fromRunning ? { status: fromRunning.signIn, version: fromRunning.version } : await localSignIn(runtime);
|
|
17827
|
-
lines2.push(
|
|
19257
|
+
lines2.push(row2(label, signInText(runtime, signIn.status, signIn.version)));
|
|
17828
19258
|
}
|
|
17829
19259
|
if (cfg.opencode?.serverUrl) {
|
|
17830
19260
|
const fromRunning = running?.find((h) => h.runtime === "opencode");
|
|
17831
19261
|
const answering = fromRunning ? fromRunning.state === "exposed" : null;
|
|
17832
19262
|
lines2.push(
|
|
17833
|
-
|
|
19263
|
+
row2(
|
|
17834
19264
|
HARNESS_LABELS.opencode,
|
|
17835
19265
|
`${cfg.opencode.serverUrl}${answering === false ? " \u2014 not answering" : ""}`
|
|
17836
19266
|
)
|
|
@@ -17886,7 +19316,7 @@ function transcriptsLine() {
|
|
|
17886
19316
|
let count = 0;
|
|
17887
19317
|
let newest;
|
|
17888
19318
|
try {
|
|
17889
|
-
const files =
|
|
19319
|
+
const files = readdirSync7(dir2).filter((f) => f.endsWith(".jsonl")).sort();
|
|
17890
19320
|
count = files.length;
|
|
17891
19321
|
newest = files[files.length - 1];
|
|
17892
19322
|
} catch {
|
|
@@ -17906,61 +19336,107 @@ function formatUptime(startedAt) {
|
|
|
17906
19336
|
return remMin ? `${hr}h${remMin}m` : `${hr}h`;
|
|
17907
19337
|
}
|
|
17908
19338
|
|
|
17909
|
-
// src/commands/
|
|
17910
|
-
|
|
17911
|
-
|
|
17912
|
-
|
|
17913
|
-
|
|
17914
|
-
|
|
17915
|
-
|
|
17916
|
-
const
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
|
|
17920
|
-
|
|
19339
|
+
// src/commands/update.ts
|
|
19340
|
+
init_config();
|
|
19341
|
+
init_term();
|
|
19342
|
+
init_version2();
|
|
19343
|
+
init_cabane_command();
|
|
19344
|
+
var UPDATE_TIMEOUT_MS = 20 * 6e4;
|
|
19345
|
+
async function update(opts = {}) {
|
|
19346
|
+
const running = readLiveRuntimeState();
|
|
19347
|
+
if (opts.auto) {
|
|
19348
|
+
const auto = opts.auto === "on";
|
|
19349
|
+
if (auto && !serviceInstalled()) {
|
|
19350
|
+
write(
|
|
19351
|
+
`${INDENT}Cabane updates itself only when it runs as a service. Run ${cabane("service install")} to run it as one, with updates on.`
|
|
19352
|
+
);
|
|
19353
|
+
process.exitCode = 1;
|
|
19354
|
+
return;
|
|
19355
|
+
}
|
|
19356
|
+
if (running?.socket) {
|
|
19357
|
+
const r = await controlRequest(running.socket, { cmd: "update", auto });
|
|
19358
|
+
write(`${INDENT}${r.ok ? tick(r.message) : r.message}`);
|
|
19359
|
+
return;
|
|
19360
|
+
}
|
|
19361
|
+
const cfg2 = loadConfig();
|
|
19362
|
+
if (!cfg2) {
|
|
19363
|
+
write(`${INDENT}This computer isn't set up yet. Run ${cabane("start")} first.`);
|
|
19364
|
+
process.exitCode = 1;
|
|
19365
|
+
return;
|
|
19366
|
+
}
|
|
19367
|
+
saveConfig({ ...cfg2, update: { auto } });
|
|
19368
|
+
write(
|
|
19369
|
+
INDENT + tick(
|
|
19370
|
+
auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself."
|
|
19371
|
+
)
|
|
19372
|
+
);
|
|
17921
19373
|
return;
|
|
17922
19374
|
}
|
|
17923
|
-
if (
|
|
17924
|
-
|
|
17925
|
-
|
|
19375
|
+
if (running?.socket) {
|
|
19376
|
+
const r = await controlRequest(
|
|
19377
|
+
running.socket,
|
|
19378
|
+
{ cmd: "update" },
|
|
19379
|
+
UPDATE_TIMEOUT_MS
|
|
19380
|
+
);
|
|
19381
|
+
write(`${INDENT}${r.ok ? tick(r.message) : r.message}`);
|
|
19382
|
+
if (!r.ok) process.exitCode = 1;
|
|
17926
19383
|
return;
|
|
17927
19384
|
}
|
|
17928
|
-
const
|
|
17929
|
-
|
|
17930
|
-
|
|
17931
|
-
|
|
17932
|
-
|
|
17933
|
-
|
|
17934
|
-
clearRuntimeState();
|
|
17935
|
-
process.stdout.write("companion: already stopped.\n");
|
|
19385
|
+
const latest = readUpdateState().server?.latest;
|
|
19386
|
+
if (!latest) {
|
|
19387
|
+
write(
|
|
19388
|
+
`${INDENT}Cabane hasn't said which version is latest yet. Run ${cabane("start")}, and it updates from there.`
|
|
19389
|
+
);
|
|
19390
|
+
process.exitCode = 1;
|
|
17936
19391
|
return;
|
|
17937
19392
|
}
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
process.stdout.write("companion: stopped.\n");
|
|
17942
|
-
return;
|
|
17943
|
-
}
|
|
17944
|
-
await sleep5(POLL_INTERVAL_MS2);
|
|
19393
|
+
if (compareVersions(latest, COMPANION_VERSION) <= 0) {
|
|
19394
|
+
write(INDENT + tick(`Cabane ${COMPANION_VERSION} is up to date.`));
|
|
19395
|
+
return;
|
|
17945
19396
|
}
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
|
|
19397
|
+
const cfg = loadConfig();
|
|
19398
|
+
const harnesses = cfg ? [
|
|
19399
|
+
...isClaudeCodeConnected(cfg) ? ["claude-code"] : [],
|
|
19400
|
+
...isCodexEnabled(cfg) ? ["codex"] : []
|
|
19401
|
+
] : [];
|
|
19402
|
+
write(
|
|
19403
|
+
`${INDENT}Installing Cabane ${latest}. It brings its own copies of the agents, so this can take a few minutes.`
|
|
17949
19404
|
);
|
|
17950
|
-
|
|
17951
|
-
|
|
17952
|
-
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
}
|
|
17957
|
-
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
19405
|
+
const outcome = await stageVersion(latest, harnesses);
|
|
19406
|
+
if (!outcome.ok) {
|
|
19407
|
+
if (outcome.unpublished) {
|
|
19408
|
+
write(`${INDENT}Cabane ${latest} isn't published yet. Try again later.`);
|
|
19409
|
+
process.exitCode = 1;
|
|
19410
|
+
return;
|
|
19411
|
+
}
|
|
19412
|
+
updateUpdateState((s) => ({
|
|
19413
|
+
...s,
|
|
19414
|
+
lastResult: {
|
|
19415
|
+
kind: "failed",
|
|
19416
|
+
from: COMPANION_VERSION,
|
|
19417
|
+
to: latest,
|
|
19418
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19419
|
+
reason: outcome.reason
|
|
19420
|
+
}
|
|
19421
|
+
}));
|
|
19422
|
+
write(`${INDENT}Couldn't update to ${latest}: ${outcome.reason}.`);
|
|
19423
|
+
process.exitCode = 1;
|
|
19424
|
+
return;
|
|
17963
19425
|
}
|
|
19426
|
+
const fromRoot = currentRoot() ?? ownPackageRoot();
|
|
19427
|
+
updateUpdateState((s) => ({
|
|
19428
|
+
...s,
|
|
19429
|
+
pending: {
|
|
19430
|
+
from: COMPANION_VERSION,
|
|
19431
|
+
fromRoot,
|
|
19432
|
+
to: latest,
|
|
19433
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19434
|
+
boots: 0
|
|
19435
|
+
}
|
|
19436
|
+
}));
|
|
19437
|
+
writeBootGuard();
|
|
19438
|
+
pointCurrentAt(outcome.root);
|
|
19439
|
+
write(INDENT + tick(`Updated to ${latest}. Start it with ${cabane("start")}`));
|
|
17964
19440
|
}
|
|
17965
19441
|
|
|
17966
19442
|
// src/commands/transcript.ts
|
|
@@ -17968,8 +19444,8 @@ init_cabane_command();
|
|
|
17968
19444
|
init_errors();
|
|
17969
19445
|
init_src();
|
|
17970
19446
|
init_transcript_writer();
|
|
17971
|
-
import { existsSync as
|
|
17972
|
-
import { isAbsolute, join as
|
|
19447
|
+
import { existsSync as existsSync21, readFileSync as readFileSync17, readdirSync as readdirSync8 } from "fs";
|
|
19448
|
+
import { isAbsolute, join as join30 } from "path";
|
|
17973
19449
|
async function transcript(opts = {}) {
|
|
17974
19450
|
const dir2 = transcriptsDir();
|
|
17975
19451
|
if (opts.follow) {
|
|
@@ -17986,7 +19462,7 @@ async function transcript(opts = {}) {
|
|
|
17986
19462
|
process.stdout.write(emptyMessage(dir2));
|
|
17987
19463
|
return;
|
|
17988
19464
|
}
|
|
17989
|
-
process.stdout.write(renderFile(
|
|
19465
|
+
process.stdout.write(renderFile(join30(dir2, newest)) + "\n");
|
|
17990
19466
|
return;
|
|
17991
19467
|
}
|
|
17992
19468
|
printList(dir2);
|
|
@@ -18069,7 +19545,7 @@ function isComplete(content) {
|
|
|
18069
19545
|
async function followTranscripts(dir2) {
|
|
18070
19546
|
const follower = new TranscriptFollower({
|
|
18071
19547
|
listFiles: () => listFiles(dir2),
|
|
18072
|
-
read: (f) =>
|
|
19548
|
+
read: (f) => readFileSync17(join30(dir2, f), "utf8"),
|
|
18073
19549
|
write: (s) => process.stdout.write(s),
|
|
18074
19550
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
18075
19551
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -18094,7 +19570,7 @@ async function followTranscripts(dir2) {
|
|
|
18094
19570
|
}
|
|
18095
19571
|
function listFiles(dir2) {
|
|
18096
19572
|
try {
|
|
18097
|
-
return
|
|
19573
|
+
return readdirSync8(dir2).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
18098
19574
|
} catch {
|
|
18099
19575
|
return [];
|
|
18100
19576
|
}
|
|
@@ -18109,7 +19585,7 @@ function printList(dir2) {
|
|
|
18109
19585
|
|
|
18110
19586
|
`);
|
|
18111
19587
|
for (const f of files.slice(0, 20)) {
|
|
18112
|
-
const { meta, outcome } = peek(
|
|
19588
|
+
const { meta, outcome } = peek(join30(dir2, f));
|
|
18113
19589
|
const when = fmtTime(rec(meta)?.ts);
|
|
18114
19590
|
const ws = str2(rec(meta)?.workspaceSlug);
|
|
18115
19591
|
const o = rec(outcome);
|
|
@@ -18131,7 +19607,7 @@ function peek(path) {
|
|
|
18131
19607
|
let meta;
|
|
18132
19608
|
let outcome;
|
|
18133
19609
|
try {
|
|
18134
|
-
for (const line of
|
|
19610
|
+
for (const line of readFileSync17(path, "utf8").split("\n")) {
|
|
18135
19611
|
if (!line.trim()) continue;
|
|
18136
19612
|
const o = safeParse(line);
|
|
18137
19613
|
const t = str2(rec(o)?.type);
|
|
@@ -18144,13 +19620,13 @@ function peek(path) {
|
|
|
18144
19620
|
}
|
|
18145
19621
|
function resolveTarget(dir2, target) {
|
|
18146
19622
|
if (isAbsolute(target) || target.includes("/")) {
|
|
18147
|
-
if (
|
|
19623
|
+
if (existsSync21(target)) return target;
|
|
18148
19624
|
throw new CompanionError(`no transcript at ${target}.`);
|
|
18149
19625
|
}
|
|
18150
|
-
const exact =
|
|
18151
|
-
if (
|
|
19626
|
+
const exact = join30(dir2, target);
|
|
19627
|
+
if (existsSync21(exact)) return exact;
|
|
18152
19628
|
const matches = listFiles(dir2).filter((f) => f.includes(target));
|
|
18153
|
-
if (matches.length === 1) return
|
|
19629
|
+
if (matches.length === 1) return join30(dir2, matches[0]);
|
|
18154
19630
|
if (matches.length === 0) {
|
|
18155
19631
|
throw new CompanionError(
|
|
18156
19632
|
`no transcript matching "${target}" in ${dir2}. Run \`${cabane("transcript")}\` to list them.`
|
|
@@ -18164,7 +19640,7 @@ function resolveTarget(dir2, target) {
|
|
|
18164
19640
|
function renderFile(path) {
|
|
18165
19641
|
let content;
|
|
18166
19642
|
try {
|
|
18167
|
-
content =
|
|
19643
|
+
content = readFileSync17(path, "utf8");
|
|
18168
19644
|
} catch (err) {
|
|
18169
19645
|
throw new CompanionError(
|
|
18170
19646
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -18324,7 +19800,7 @@ program.command("pair").description(
|
|
|
18324
19800
|
});
|
|
18325
19801
|
});
|
|
18326
19802
|
program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
|
|
18327
|
-
writeCompletedPairing(
|
|
19803
|
+
writeCompletedPairing(readFileSync18(0, "utf8"));
|
|
18328
19804
|
});
|
|
18329
19805
|
program.command("start").description("pair this device if needed, connect an agent on it, and run in the background.").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(
|
|
18330
19806
|
new Option("--log-level <level>", "info (default), warn, or debug").choices([
|
|
@@ -18348,6 +19824,21 @@ program.command("start").description("pair this device if needed, connect an age
|
|
|
18348
19824
|
program.command("connect").description("connect an agent on this machine to Cabane (claude-code, codex, opencode).").argument("<harness>", "claude-code, codex, or opencode").option("--url <url>", "the `opencode serve` URL (opencode only)").action(async (harness, opts) => {
|
|
18349
19825
|
await connect2(harness, { ...opts.url !== void 0 ? { serverUrl: opts.url } : {} });
|
|
18350
19826
|
});
|
|
19827
|
+
program.command("update").description(
|
|
19828
|
+
"update to the latest version now, or set whether Cabane keeps itself up to date (--auto on|off)."
|
|
19829
|
+
).addOption(new Option("--auto <setting>", "on or off").choices(["on", "off"])).action(async (opts) => {
|
|
19830
|
+
await update(opts.auto ? { auto: opts.auto } : {});
|
|
19831
|
+
});
|
|
19832
|
+
var service = program.command("service").description("run Cabane as a service that starts at boot and restarts if it stops.");
|
|
19833
|
+
service.command("install").description("install the service and start it (systemd on Linux, launchd on macOS).").action(async () => {
|
|
19834
|
+
await serviceInstall();
|
|
19835
|
+
});
|
|
19836
|
+
service.command("uninstall").description("stop the service and remove it.").action(async () => {
|
|
19837
|
+
await serviceUninstall();
|
|
19838
|
+
});
|
|
19839
|
+
service.command("status").description("show the service's state, this version, and how updates are going.").action(async () => {
|
|
19840
|
+
await serviceStatus();
|
|
19841
|
+
});
|
|
18351
19842
|
program.command("stop").description("stop a running companion (SIGTERM, then force-kill after a timeout).").action(async () => {
|
|
18352
19843
|
await stop();
|
|
18353
19844
|
});
|