@cabane/companion 0.6.188 → 0.6.190
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +354 -188
- package/dist/launch.js +9 -1
- package/dist/pairing-config.js +9 -1
- package/dist/runtime.js +260 -176
- package/package.json +1 -1
package/dist/launch.js
CHANGED
|
@@ -196,7 +196,15 @@ var companionConfigSchema = z2.object({
|
|
|
196
196
|
// and an agent with no folder of its own and no prepare hook works there
|
|
197
197
|
// (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
|
|
198
198
|
// laptop creates no new visible folder; set it to make the work visible.
|
|
199
|
-
root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional()
|
|
199
|
+
root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
|
|
200
|
+
// An argv prefix the operator puts in front of every turn's runner: the companion starts
|
|
201
|
+
// `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
|
|
202
|
+
// is the operator's (a box of their own making, ENG225); the companion knows nothing of it
|
|
203
|
+
// beyond handing the runner over, with the turn's inputs in its environment and the
|
|
204
|
+
// credential and secrets on its stdin (`runner/launch.ts`, `boxed`).
|
|
205
|
+
runner: z2.object({
|
|
206
|
+
command: z2.array(z2.string().min(1)).min(1)
|
|
207
|
+
}).strict().optional()
|
|
200
208
|
});
|
|
201
209
|
var legacyOfferSchema = z2.object({
|
|
202
210
|
claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
package/dist/pairing-config.js
CHANGED
|
@@ -221,7 +221,15 @@ var companionConfigSchema = z2.object({
|
|
|
221
221
|
// and an agent with no folder of its own and no prepare hook works there
|
|
222
222
|
// (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
|
|
223
223
|
// laptop creates no new visible folder; set it to make the work visible.
|
|
224
|
-
root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional()
|
|
224
|
+
root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
|
|
225
|
+
// An argv prefix the operator puts in front of every turn's runner: the companion starts
|
|
226
|
+
// `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
|
|
227
|
+
// is the operator's (a box of their own making, ENG225); the companion knows nothing of it
|
|
228
|
+
// beyond handing the runner over, with the turn's inputs in its environment and the
|
|
229
|
+
// credential and secrets on its stdin (`runner/launch.ts`, `boxed`).
|
|
230
|
+
runner: z2.object({
|
|
231
|
+
command: z2.array(z2.string().min(1)).min(1)
|
|
232
|
+
}).strict().optional()
|
|
225
233
|
});
|
|
226
234
|
function retiredConfigKeys(raw) {
|
|
227
235
|
if (!raw || typeof raw !== "object") return [];
|
package/dist/runtime.js
CHANGED
|
@@ -295,7 +295,15 @@ var companionConfigSchema = z2.object({
|
|
|
295
295
|
// and an agent with no folder of its own and no prepare hook works there
|
|
296
296
|
// (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
|
|
297
297
|
// laptop creates no new visible folder; set it to make the work visible.
|
|
298
|
-
root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional()
|
|
298
|
+
root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
|
|
299
|
+
// An argv prefix the operator puts in front of every turn's runner: the companion starts
|
|
300
|
+
// `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
|
|
301
|
+
// is the operator's (a box of their own making, ENG225); the companion knows nothing of it
|
|
302
|
+
// beyond handing the runner over, with the turn's inputs in its environment and the
|
|
303
|
+
// credential and secrets on its stdin (`runner/launch.ts`, `boxed`).
|
|
304
|
+
runner: z2.object({
|
|
305
|
+
command: z2.array(z2.string().min(1)).min(1)
|
|
306
|
+
}).strict().optional()
|
|
299
307
|
});
|
|
300
308
|
function workRoot(cfg) {
|
|
301
309
|
const root = cfg.root;
|
|
@@ -6150,6 +6158,10 @@ function createSdkCodexTransport(opts = {}) {
|
|
|
6150
6158
|
}
|
|
6151
6159
|
|
|
6152
6160
|
// packages/agent-runtime/src/codex/index.ts
|
|
6161
|
+
function isMissingRollout(err) {
|
|
6162
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6163
|
+
return /no rollout found for thread id/i.test(message);
|
|
6164
|
+
}
|
|
6153
6165
|
function createCodexAdapter(deps = {}) {
|
|
6154
6166
|
const transport = deps.transport ?? (deps.enabled ? createSdkCodexTransport({ ...deps.apiKey ? { apiKey: deps.apiKey } : {} }) : null);
|
|
6155
6167
|
return {
|
|
@@ -6168,8 +6180,8 @@ function createCodexAdapter(deps = {}) {
|
|
|
6168
6180
|
}
|
|
6169
6181
|
const directory = req.local.cwd ?? "";
|
|
6170
6182
|
const decision = decideResume3(req.session, directory);
|
|
6171
|
-
|
|
6172
|
-
|
|
6183
|
+
let resumeThreadId = "resume" in decision ? decision.resume : null;
|
|
6184
|
+
let threadUsageBaseline = "resume" in decision ? decision.usageBaseline : null;
|
|
6173
6185
|
if ("fresh" in decision && decision.reason === "cwd_mismatch") {
|
|
6174
6186
|
deps.onWarn?.(
|
|
6175
6187
|
"codex adapter: stored session directory no longer matches the current environment \u2014 starting a fresh thread (SJ527-analogue guard)",
|
|
@@ -6234,38 +6246,69 @@ function createCodexAdapter(deps = {}) {
|
|
|
6234
6246
|
}
|
|
6235
6247
|
if (signal.aborted) return;
|
|
6236
6248
|
}
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
model
|
|
6241
|
-
|
|
6249
|
+
for (; ; ) {
|
|
6250
|
+
const spec = buildRunSpec2(req, resumeThreadId, threadStanding, images?.paths ?? []);
|
|
6251
|
+
if (spec.model === null) {
|
|
6252
|
+
deps.onWarn?.("Codex is using its default model because no model was resolved", {
|
|
6253
|
+
model: req.config.model
|
|
6254
|
+
});
|
|
6255
|
+
}
|
|
6256
|
+
let emitted = false;
|
|
6257
|
+
try {
|
|
6258
|
+
const result = await transport.run(spec, signal);
|
|
6259
|
+
if (signal.aborted) return;
|
|
6260
|
+
for await (const event of decodeCodexStream(result.events, {
|
|
6261
|
+
signal,
|
|
6262
|
+
cwd: req.local.cwd,
|
|
6263
|
+
resumedThreadId: resumeThreadId,
|
|
6264
|
+
degraded,
|
|
6265
|
+
// Which segments the thread carries once this turn lands. A segment sent is that one —
|
|
6266
|
+
// the later copy is what the model reads. One withheld is what the thread had. Read off
|
|
6267
|
+
// the spec, so the record can't disagree with what was sent.
|
|
6268
|
+
promptFingerprint: spec.preamble.core ? spec.fingerprints.core : threadStanding?.core ?? null,
|
|
6269
|
+
conversationFingerprint: spec.preamble.conversation ? spec.fingerprints.conversation : threadStanding?.conversation ?? null,
|
|
6270
|
+
compactions,
|
|
6271
|
+
// What the thread held before this turn, for the write at `thread.started`.
|
|
6272
|
+
heldStanding: {
|
|
6273
|
+
promptFingerprint: threadStanding?.core ?? null,
|
|
6274
|
+
conversationFingerprint: threadStanding?.conversation ?? null,
|
|
6275
|
+
compactions: threadStanding ? compactions : null
|
|
6276
|
+
},
|
|
6277
|
+
storedState: req.session,
|
|
6278
|
+
usageBaseline: threadUsageBaseline,
|
|
6279
|
+
// Codex reports no model in-stream, so record what the thread was started with (null on the
|
|
6280
|
+
// default token). Same for the reasoning effort.
|
|
6281
|
+
resolvedModel: spec.model,
|
|
6282
|
+
resolvedReasoningEffort: spec.modelReasoningEffort ?? null
|
|
6283
|
+
})) {
|
|
6284
|
+
emitted = true;
|
|
6285
|
+
yield event;
|
|
6286
|
+
}
|
|
6287
|
+
return;
|
|
6288
|
+
} catch (err) {
|
|
6289
|
+
if (resumeThreadId === null || emitted || signal.aborted || !isMissingRollout(err)) {
|
|
6290
|
+
throw err;
|
|
6291
|
+
}
|
|
6292
|
+
deps.onWarn?.(
|
|
6293
|
+
"codex adapter: the stored thread has no rollout on this host \u2014 starting a fresh thread",
|
|
6294
|
+
{ threadId: resumeThreadId }
|
|
6295
|
+
);
|
|
6296
|
+
resumeThreadId = null;
|
|
6297
|
+
threadUsageBaseline = null;
|
|
6298
|
+
threadStanding = null;
|
|
6299
|
+
compactions = 0;
|
|
6300
|
+
req = { ...req, session: null };
|
|
6301
|
+
degraded = true;
|
|
6302
|
+
if (hooks?.fresh) {
|
|
6303
|
+
const recomposed = await hooks.fresh("session_start_failed");
|
|
6304
|
+
if (signal.aborted) return;
|
|
6305
|
+
if (recomposed) {
|
|
6306
|
+
req = { ...req, ...recomposed, session: null };
|
|
6307
|
+
degraded = false;
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6242
6311
|
}
|
|
6243
|
-
const result = await transport.run(spec, signal);
|
|
6244
|
-
if (signal.aborted) return;
|
|
6245
|
-
yield* decodeCodexStream(result.events, {
|
|
6246
|
-
signal,
|
|
6247
|
-
cwd: req.local.cwd,
|
|
6248
|
-
resumedThreadId: resumeThreadId,
|
|
6249
|
-
degraded,
|
|
6250
|
-
// Which segments the thread carries once this turn lands. A segment sent is that one —
|
|
6251
|
-
// the later copy is what the model reads. One withheld is what the thread had. Read off
|
|
6252
|
-
// the spec, so the record can't disagree with what was sent.
|
|
6253
|
-
promptFingerprint: spec.preamble.core ? spec.fingerprints.core : threadStanding?.core ?? null,
|
|
6254
|
-
conversationFingerprint: spec.preamble.conversation ? spec.fingerprints.conversation : threadStanding?.conversation ?? null,
|
|
6255
|
-
compactions,
|
|
6256
|
-
// What the thread held before this turn, for the write at `thread.started`.
|
|
6257
|
-
heldStanding: {
|
|
6258
|
-
promptFingerprint: threadStanding?.core ?? null,
|
|
6259
|
-
conversationFingerprint: threadStanding?.conversation ?? null,
|
|
6260
|
-
compactions: threadStanding ? compactions : null
|
|
6261
|
-
},
|
|
6262
|
-
storedState: req.session,
|
|
6263
|
-
usageBaseline: threadUsageBaseline,
|
|
6264
|
-
// Codex reports no model in-stream, so record what the thread was started with (null on the
|
|
6265
|
-
// default token). Same for the reasoning effort.
|
|
6266
|
-
resolvedModel: spec.model,
|
|
6267
|
-
resolvedReasoningEffort: spec.modelReasoningEffort ?? null
|
|
6268
|
-
});
|
|
6269
6312
|
} finally {
|
|
6270
6313
|
if (images) {
|
|
6271
6314
|
try {
|
|
@@ -7445,7 +7488,8 @@ var dispatchSchema = z14.object({
|
|
|
7445
7488
|
episode: z14.number().int().optional()
|
|
7446
7489
|
});
|
|
7447
7490
|
var credentialsSchema = z14.object({
|
|
7448
|
-
credential: z14.string().startsWith("cabrn_")
|
|
7491
|
+
credential: z14.string().startsWith("cabrn_"),
|
|
7492
|
+
secrets: z14.record(z14.string(), z14.string()).optional()
|
|
7449
7493
|
});
|
|
7450
7494
|
function runnerEnv(input) {
|
|
7451
7495
|
return {
|
|
@@ -7457,7 +7501,10 @@ function runnerEnv(input) {
|
|
|
7457
7501
|
};
|
|
7458
7502
|
}
|
|
7459
7503
|
function runnerStdin(input) {
|
|
7460
|
-
return `${JSON.stringify({
|
|
7504
|
+
return `${JSON.stringify({
|
|
7505
|
+
credential: input.credential,
|
|
7506
|
+
...input.secrets ? { secrets: input.secrets } : {}
|
|
7507
|
+
})}
|
|
7461
7508
|
`;
|
|
7462
7509
|
}
|
|
7463
7510
|
function encodeLauncherMessage(message) {
|
|
@@ -7482,6 +7529,29 @@ function selfRunnerCommand() {
|
|
|
7482
7529
|
if (!script) throw new Error("runner: cannot tell which script started this companion");
|
|
7483
7530
|
return [process.execPath, ...process.execArgv, script, "run-turn"];
|
|
7484
7531
|
}
|
|
7532
|
+
function wrappedRunnerCommand(prefix, own) {
|
|
7533
|
+
return [...prefix ?? [], ...own];
|
|
7534
|
+
}
|
|
7535
|
+
var BOXED_ENV_DROPPED = [
|
|
7536
|
+
"ANTHROPIC_API_KEY",
|
|
7537
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
7538
|
+
"CODEX_API_KEY",
|
|
7539
|
+
"GH_TOKEN",
|
|
7540
|
+
"GITHUB_TOKEN",
|
|
7541
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
7542
|
+
"XDG_RUNTIME_DIR"
|
|
7543
|
+
];
|
|
7544
|
+
var BOXED_ENV_DROPPED_PREFIX = "CABANE_GATE_";
|
|
7545
|
+
function runnerExecEnv(base, boxed) {
|
|
7546
|
+
const env = {};
|
|
7547
|
+
for (const [k, v] of Object.entries(base)) {
|
|
7548
|
+
if (v === void 0) continue;
|
|
7549
|
+
if (boxed && (BOXED_ENV_DROPPED.includes(k) || k.startsWith(BOXED_ENV_DROPPED_PREFIX)))
|
|
7550
|
+
continue;
|
|
7551
|
+
env[k] = v;
|
|
7552
|
+
}
|
|
7553
|
+
return env;
|
|
7554
|
+
}
|
|
7485
7555
|
function healthSnapshot(store) {
|
|
7486
7556
|
const out = {};
|
|
7487
7557
|
for (const r of store?.reports() ?? []) {
|
|
@@ -7686,13 +7756,12 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7686
7756
|
return { ok: false, durationMs: durationMs2, reason: reason2 };
|
|
7687
7757
|
}
|
|
7688
7758
|
try {
|
|
7689
|
-
const
|
|
7690
|
-
|
|
7691
|
-
if (v !== void 0) env[k] = v;
|
|
7692
|
-
}
|
|
7759
|
+
const boxed = launch.boxed === true;
|
|
7760
|
+
const env = runnerExecEnv(launch.env ?? process.env, boxed);
|
|
7693
7761
|
const input = {
|
|
7694
7762
|
baseUrl: opts.baseUrl,
|
|
7695
7763
|
credential: minted.credential,
|
|
7764
|
+
...boxed ? { secrets: launch.secrets?.() ?? {} } : {},
|
|
7696
7765
|
dispatch: payload,
|
|
7697
7766
|
turnId,
|
|
7698
7767
|
resumed: handleOpts.resumed,
|
|
@@ -7764,7 +7833,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7764
7833
|
locks?.release(lockOwner(event.id));
|
|
7765
7834
|
return;
|
|
7766
7835
|
case "process_group":
|
|
7767
|
-
groups.push(event.pgid);
|
|
7836
|
+
if (!boxed) groups.push(event.pgid);
|
|
7768
7837
|
return;
|
|
7769
7838
|
case "remote_session":
|
|
7770
7839
|
remote.push({ serverUrl: event.serverUrl, sessionId: event.sessionId });
|
|
@@ -7785,7 +7854,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7785
7854
|
}
|
|
7786
7855
|
if (result) return result;
|
|
7787
7856
|
const durationMs = Date.now() - startedAt;
|
|
7788
|
-
const
|
|
7857
|
+
const stopped = controller.signal.aborted && thrown === void 0;
|
|
7858
|
+
const reason = stopped ? "cancelled" : thrown !== void 0 ? `runner_failed: ${thrown}` : `runner_exited: ${code}`;
|
|
7789
7859
|
await child?.reap(groups);
|
|
7790
7860
|
const reaped = await (launch.reapTurn ?? reapTurnScopes)(turnId).catch((err) => ({
|
|
7791
7861
|
failures: [err instanceof Error ? err.message : String(err)]
|
|
@@ -7800,8 +7870,7 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7800
7870
|
await opts.api.setActiveRun(opts.workspaceId, payload.conversationId, payload.agentId, {
|
|
7801
7871
|
activeRunStartedAt: null,
|
|
7802
7872
|
turnId,
|
|
7803
|
-
outcome: "failed",
|
|
7804
|
-
errorReason: reason.slice(0, ERROR_REASON_MAX),
|
|
7873
|
+
...stopped ? { outcome: "interrupted" } : { outcome: "failed", errorReason: reason.slice(0, ERROR_REASON_MAX) },
|
|
7805
7874
|
readThrough: payload.messageId
|
|
7806
7875
|
});
|
|
7807
7876
|
} catch (err) {
|
|
@@ -7819,7 +7888,8 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
7819
7888
|
reason: "exited"
|
|
7820
7889
|
});
|
|
7821
7890
|
if (thrown !== void 0) throw new Error(thrown);
|
|
7822
|
-
log.
|
|
7891
|
+
if (stopped) log.info({ turnId, code }, "runner: stopped before it could report");
|
|
7892
|
+
else log.warn({ turnId, code }, "runner: exited without reporting a result");
|
|
7823
7893
|
duties.notifyEnd({ id: payload.messageId, ok: false, durationMs, reason });
|
|
7824
7894
|
return { ok: false, durationMs, reason };
|
|
7825
7895
|
}
|
|
@@ -7881,9 +7951,9 @@ function secretsPath() {
|
|
|
7881
7951
|
return join18(cabaneDir(), "secrets.json");
|
|
7882
7952
|
}
|
|
7883
7953
|
var secretStoreSchema = z16.record(z16.string(), z16.string());
|
|
7884
|
-
function
|
|
7954
|
+
function loadSecretValues() {
|
|
7885
7955
|
const path = secretsPath();
|
|
7886
|
-
if (!existsSync10(path)) return
|
|
7956
|
+
if (!existsSync10(path)) return {};
|
|
7887
7957
|
let raw;
|
|
7888
7958
|
try {
|
|
7889
7959
|
raw = readFileSync8(path, "utf8");
|
|
@@ -7892,7 +7962,7 @@ function loadSecretStore() {
|
|
|
7892
7962
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
7893
7963
|
);
|
|
7894
7964
|
}
|
|
7895
|
-
if (raw.trim().length === 0) return
|
|
7965
|
+
if (raw.trim().length === 0) return {};
|
|
7896
7966
|
let parsed;
|
|
7897
7967
|
try {
|
|
7898
7968
|
parsed = JSON.parse(raw);
|
|
@@ -7907,14 +7977,17 @@ function loadSecretStore() {
|
|
|
7907
7977
|
`${path} must be a flat object mapping secret names to string values (e.g. { "GITHUB_TOKEN": "ghp_\u2026" }).`
|
|
7908
7978
|
);
|
|
7909
7979
|
}
|
|
7910
|
-
return
|
|
7980
|
+
return result.data;
|
|
7911
7981
|
}
|
|
7912
7982
|
function loadSecretStoreTolerant(onWarn) {
|
|
7983
|
+
return makeStore(loadSecretValuesTolerant(onWarn));
|
|
7984
|
+
}
|
|
7985
|
+
function loadSecretValuesTolerant(onWarn) {
|
|
7913
7986
|
try {
|
|
7914
|
-
return
|
|
7987
|
+
return loadSecretValues();
|
|
7915
7988
|
} catch (err) {
|
|
7916
7989
|
onWarn?.(err instanceof Error ? err.message : String(err));
|
|
7917
|
-
return
|
|
7990
|
+
return {};
|
|
7918
7991
|
}
|
|
7919
7992
|
}
|
|
7920
7993
|
function makeStore(map) {
|
|
@@ -7939,6 +8012,130 @@ function requiredSecretNames(mcpServers) {
|
|
|
7939
8012
|
return [...names].sort();
|
|
7940
8013
|
}
|
|
7941
8014
|
|
|
8015
|
+
// src/harness-usable.ts
|
|
8016
|
+
import { spawn as spawn4 } from "child_process";
|
|
8017
|
+
|
|
8018
|
+
// src/harness-versions.ts
|
|
8019
|
+
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
8020
|
+
function parseVersionToken(raw) {
|
|
8021
|
+
if (!raw) return null;
|
|
8022
|
+
const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
8023
|
+
return m ? m[0] : null;
|
|
8024
|
+
}
|
|
8025
|
+
async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
|
|
8026
|
+
try {
|
|
8027
|
+
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
8028
|
+
const res = await fetchImpl(`${base}/global/health`, {
|
|
8029
|
+
headers: { accept: "application/json" }
|
|
8030
|
+
});
|
|
8031
|
+
if (!res.ok) return null;
|
|
8032
|
+
return extractOpencodeVersion(await res.json());
|
|
8033
|
+
} catch {
|
|
8034
|
+
return null;
|
|
8035
|
+
}
|
|
8036
|
+
}
|
|
8037
|
+
function extractOpencodeVersion(json) {
|
|
8038
|
+
if (!json || typeof json !== "object") return null;
|
|
8039
|
+
const obj = json;
|
|
8040
|
+
const candidates = [obj.version];
|
|
8041
|
+
for (const v of Object.values(obj)) {
|
|
8042
|
+
if (v && typeof v === "object") candidates.push(v.version);
|
|
8043
|
+
}
|
|
8044
|
+
for (const c of candidates) {
|
|
8045
|
+
if (typeof c === "string") {
|
|
8046
|
+
const parsed = parseVersionToken(c);
|
|
8047
|
+
if (parsed) return parsed;
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
return null;
|
|
8051
|
+
}
|
|
8052
|
+
function emptyHarnessVersions() {
|
|
8053
|
+
return { ...EMPTY };
|
|
8054
|
+
}
|
|
8055
|
+
|
|
8056
|
+
// src/harness-usable.ts
|
|
8057
|
+
var API_KEY_VARIABLES = {
|
|
8058
|
+
"claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
|
|
8059
|
+
codex: ["CODEX_API_KEY"]
|
|
8060
|
+
};
|
|
8061
|
+
function apiKeyVariable(runtime, env = process.env) {
|
|
8062
|
+
return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
|
|
8063
|
+
}
|
|
8064
|
+
var CHECK_TIMEOUT_MS = 5e3;
|
|
8065
|
+
async function checkHarnessUsable(runtime, deps = {}) {
|
|
8066
|
+
const env = deps.env ?? process.env;
|
|
8067
|
+
const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
|
|
8068
|
+
const run = deps.run ?? runBounded;
|
|
8069
|
+
const resolution = resolveBundled(runtime);
|
|
8070
|
+
if (resolution.status !== "present" || !resolution.path) {
|
|
8071
|
+
return {
|
|
8072
|
+
status: "bundle_missing",
|
|
8073
|
+
bundled: resolution.status === "present" ? "executable_missing" : resolution.status
|
|
8074
|
+
};
|
|
8075
|
+
}
|
|
8076
|
+
const path = resolution.path;
|
|
8077
|
+
const version = run(path, ["--version"], env).then(
|
|
8078
|
+
(r) => r.code === 0 ? parseVersionToken(r.output) : null
|
|
8079
|
+
);
|
|
8080
|
+
const variable = apiKeyVariable(runtime, env);
|
|
8081
|
+
if (variable) return { status: "api_key", variable, version: await version };
|
|
8082
|
+
const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
|
|
8083
|
+
const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
|
|
8084
|
+
if (answer.error) return { status: "check_failed", reason: answer.error };
|
|
8085
|
+
const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
|
|
8086
|
+
if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
|
|
8087
|
+
return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
|
|
8088
|
+
}
|
|
8089
|
+
function readClaudeStatus(answer) {
|
|
8090
|
+
const start = answer.output.indexOf("{");
|
|
8091
|
+
const end = answer.output.lastIndexOf("}");
|
|
8092
|
+
if (start < 0 || end < start) return "unreadable";
|
|
8093
|
+
try {
|
|
8094
|
+
const parsed = JSON.parse(answer.output.slice(start, end + 1));
|
|
8095
|
+
const loggedIn = parsed?.loggedIn;
|
|
8096
|
+
return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
|
|
8097
|
+
} catch {
|
|
8098
|
+
return "unreadable";
|
|
8099
|
+
}
|
|
8100
|
+
}
|
|
8101
|
+
function readCodexStatus(answer) {
|
|
8102
|
+
if (answer.code === 0) return true;
|
|
8103
|
+
if (/not logged in/i.test(answer.output)) return false;
|
|
8104
|
+
if (/config/i.test(answer.output)) return "config";
|
|
8105
|
+
return "unreadable";
|
|
8106
|
+
}
|
|
8107
|
+
function runBounded(path, args, env) {
|
|
8108
|
+
return new Promise((resolve) => {
|
|
8109
|
+
let settled = false;
|
|
8110
|
+
let out = "";
|
|
8111
|
+
const done = (result) => {
|
|
8112
|
+
if (settled) return;
|
|
8113
|
+
settled = true;
|
|
8114
|
+
clearTimeout(timer);
|
|
8115
|
+
resolve(result);
|
|
8116
|
+
};
|
|
8117
|
+
let child;
|
|
8118
|
+
try {
|
|
8119
|
+
child = spawn4(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
|
|
8120
|
+
} catch {
|
|
8121
|
+
resolve({ code: null, output: "", error: "spawn" });
|
|
8122
|
+
return;
|
|
8123
|
+
}
|
|
8124
|
+
const capture = (chunk) => {
|
|
8125
|
+
if (out.length < 8192) out += chunk.toString();
|
|
8126
|
+
};
|
|
8127
|
+
child.stdout?.on("data", capture);
|
|
8128
|
+
child.stderr?.on("data", capture);
|
|
8129
|
+
const timer = setTimeout(() => {
|
|
8130
|
+
child.kill("SIGKILL");
|
|
8131
|
+
done({ code: null, output: out, error: "timeout" });
|
|
8132
|
+
}, CHECK_TIMEOUT_MS);
|
|
8133
|
+
timer.unref?.();
|
|
8134
|
+
child.once("error", () => done({ code: null, output: out, error: "spawn" }));
|
|
8135
|
+
child.once("close", (code) => done({ code, output: out }));
|
|
8136
|
+
});
|
|
8137
|
+
}
|
|
8138
|
+
|
|
7942
8139
|
// src/transcript-writer.ts
|
|
7943
8140
|
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
7944
8141
|
import { basename as basename3, dirname as dirname7, join as join19 } from "path";
|
|
@@ -8083,7 +8280,7 @@ function hostReport() {
|
|
|
8083
8280
|
}
|
|
8084
8281
|
|
|
8085
8282
|
// src/providers/local-process.ts
|
|
8086
|
-
import { spawn as
|
|
8283
|
+
import { spawn as spawn5 } from "child_process";
|
|
8087
8284
|
import { constants as constants2 } from "os";
|
|
8088
8285
|
var DEFAULT_KILL_GRACE_MS = 3e4;
|
|
8089
8286
|
var DEFAULT_REAP_GRACE_MS = 3e3;
|
|
@@ -8094,7 +8291,7 @@ function exitCode(code, signal) {
|
|
|
8094
8291
|
}
|
|
8095
8292
|
function createLocalProcessProvider(opts = {}) {
|
|
8096
8293
|
const killGraceMs = opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
8097
|
-
const spawnFn = opts.spawnFn ??
|
|
8294
|
+
const spawnFn = opts.spawnFn ?? spawn5;
|
|
8098
8295
|
const reapGraceMs = opts.reapGraceMs ?? DEFAULT_REAP_GRACE_MS;
|
|
8099
8296
|
const killFn = opts.killFn ?? ((pid, signal) => process.kill(pid, signal));
|
|
8100
8297
|
const groupAlive = (pgid) => {
|
|
@@ -8296,44 +8493,6 @@ function checkReason(check) {
|
|
|
8296
8493
|
}
|
|
8297
8494
|
}
|
|
8298
8495
|
|
|
8299
|
-
// src/harness-versions.ts
|
|
8300
|
-
var EMPTY = { claudeCode: null, opencode: null, codex: null };
|
|
8301
|
-
function parseVersionToken(raw) {
|
|
8302
|
-
if (!raw) return null;
|
|
8303
|
-
const m = raw.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
8304
|
-
return m ? m[0] : null;
|
|
8305
|
-
}
|
|
8306
|
-
async function probeOpencodeVersion(serverUrl, fetchImpl = fetch) {
|
|
8307
|
-
try {
|
|
8308
|
-
const base = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
|
|
8309
|
-
const res = await fetchImpl(`${base}/global/health`, {
|
|
8310
|
-
headers: { accept: "application/json" }
|
|
8311
|
-
});
|
|
8312
|
-
if (!res.ok) return null;
|
|
8313
|
-
return extractOpencodeVersion(await res.json());
|
|
8314
|
-
} catch {
|
|
8315
|
-
return null;
|
|
8316
|
-
}
|
|
8317
|
-
}
|
|
8318
|
-
function extractOpencodeVersion(json) {
|
|
8319
|
-
if (!json || typeof json !== "object") return null;
|
|
8320
|
-
const obj = json;
|
|
8321
|
-
const candidates = [obj.version];
|
|
8322
|
-
for (const v of Object.values(obj)) {
|
|
8323
|
-
if (v && typeof v === "object") candidates.push(v.version);
|
|
8324
|
-
}
|
|
8325
|
-
for (const c of candidates) {
|
|
8326
|
-
if (typeof c === "string") {
|
|
8327
|
-
const parsed = parseVersionToken(c);
|
|
8328
|
-
if (parsed) return parsed;
|
|
8329
|
-
}
|
|
8330
|
-
}
|
|
8331
|
-
return null;
|
|
8332
|
-
}
|
|
8333
|
-
function emptyHarnessVersions() {
|
|
8334
|
-
return { ...EMPTY };
|
|
8335
|
-
}
|
|
8336
|
-
|
|
8337
8496
|
// src/harness-copy.ts
|
|
8338
8497
|
import { homedir as homedir8 } from "os";
|
|
8339
8498
|
import { join as join21 } from "path";
|
|
@@ -8369,90 +8528,6 @@ function spaced(sentence) {
|
|
|
8369
8528
|
return sentence ? ` ${sentence}` : "";
|
|
8370
8529
|
}
|
|
8371
8530
|
|
|
8372
|
-
// src/harness-usable.ts
|
|
8373
|
-
import { spawn as spawn5 } from "child_process";
|
|
8374
|
-
var API_KEY_VARIABLES = {
|
|
8375
|
-
"claude-code": ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
|
|
8376
|
-
codex: ["CODEX_API_KEY"]
|
|
8377
|
-
};
|
|
8378
|
-
function apiKeyVariable(runtime, env = process.env) {
|
|
8379
|
-
return API_KEY_VARIABLES[runtime].find((name) => !!env[name]?.trim()) ?? null;
|
|
8380
|
-
}
|
|
8381
|
-
var CHECK_TIMEOUT_MS = 5e3;
|
|
8382
|
-
async function checkHarnessUsable(runtime, deps = {}) {
|
|
8383
|
-
const env = deps.env ?? process.env;
|
|
8384
|
-
const resolveBundled = deps.resolveBundled ?? ((r) => resolveBundledBinary(r));
|
|
8385
|
-
const run = deps.run ?? runBounded;
|
|
8386
|
-
const resolution = resolveBundled(runtime);
|
|
8387
|
-
if (resolution.status !== "present" || !resolution.path) {
|
|
8388
|
-
return {
|
|
8389
|
-
status: "bundle_missing",
|
|
8390
|
-
bundled: resolution.status === "present" ? "executable_missing" : resolution.status
|
|
8391
|
-
};
|
|
8392
|
-
}
|
|
8393
|
-
const path = resolution.path;
|
|
8394
|
-
const version = run(path, ["--version"], env).then(
|
|
8395
|
-
(r) => r.code === 0 ? parseVersionToken(r.output) : null
|
|
8396
|
-
);
|
|
8397
|
-
const variable = apiKeyVariable(runtime, env);
|
|
8398
|
-
if (variable) return { status: "api_key", variable, version: await version };
|
|
8399
|
-
const statusArgs = runtime === "codex" ? ["login", "status"] : ["auth", "status", "--json"];
|
|
8400
|
-
const [answer, v] = await Promise.all([run(path, statusArgs, env), version]);
|
|
8401
|
-
if (answer.error) return { status: "check_failed", reason: answer.error };
|
|
8402
|
-
const signedIn = runtime === "codex" ? readCodexStatus(answer) : readClaudeStatus(answer);
|
|
8403
|
-
if (typeof signedIn === "string") return { status: "check_failed", reason: signedIn };
|
|
8404
|
-
return signedIn ? { status: "signed_in", version: v } : { status: "signed_out", version: v };
|
|
8405
|
-
}
|
|
8406
|
-
function readClaudeStatus(answer) {
|
|
8407
|
-
const start = answer.output.indexOf("{");
|
|
8408
|
-
const end = answer.output.lastIndexOf("}");
|
|
8409
|
-
if (start < 0 || end < start) return "unreadable";
|
|
8410
|
-
try {
|
|
8411
|
-
const parsed = JSON.parse(answer.output.slice(start, end + 1));
|
|
8412
|
-
const loggedIn = parsed?.loggedIn;
|
|
8413
|
-
return typeof loggedIn === "boolean" ? loggedIn : "unreadable";
|
|
8414
|
-
} catch {
|
|
8415
|
-
return "unreadable";
|
|
8416
|
-
}
|
|
8417
|
-
}
|
|
8418
|
-
function readCodexStatus(answer) {
|
|
8419
|
-
if (answer.code === 0) return true;
|
|
8420
|
-
if (/not logged in/i.test(answer.output)) return false;
|
|
8421
|
-
if (/config/i.test(answer.output)) return "config";
|
|
8422
|
-
return "unreadable";
|
|
8423
|
-
}
|
|
8424
|
-
function runBounded(path, args, env) {
|
|
8425
|
-
return new Promise((resolve) => {
|
|
8426
|
-
let settled = false;
|
|
8427
|
-
let out = "";
|
|
8428
|
-
const done = (result) => {
|
|
8429
|
-
if (settled) return;
|
|
8430
|
-
settled = true;
|
|
8431
|
-
clearTimeout(timer);
|
|
8432
|
-
resolve(result);
|
|
8433
|
-
};
|
|
8434
|
-
let child;
|
|
8435
|
-
try {
|
|
8436
|
-
child = spawn5(path, args, { stdio: ["ignore", "pipe", "pipe"], env });
|
|
8437
|
-
} catch {
|
|
8438
|
-
resolve({ code: null, output: "", error: "spawn" });
|
|
8439
|
-
return;
|
|
8440
|
-
}
|
|
8441
|
-
const capture = (chunk) => {
|
|
8442
|
-
if (out.length < 8192) out += chunk.toString();
|
|
8443
|
-
};
|
|
8444
|
-
child.stdout?.on("data", capture);
|
|
8445
|
-
child.stderr?.on("data", capture);
|
|
8446
|
-
const timer = setTimeout(() => {
|
|
8447
|
-
child.kill("SIGKILL");
|
|
8448
|
-
done({ code: null, output: out, error: "timeout" });
|
|
8449
|
-
}, CHECK_TIMEOUT_MS);
|
|
8450
|
-
timer.unref?.();
|
|
8451
|
-
child.once("error", () => done({ code: null, output: out, error: "spawn" }));
|
|
8452
|
-
child.once("close", (code) => done({ code, output: out }));
|
|
8453
|
-
});
|
|
8454
|
-
}
|
|
8455
|
-
|
|
8456
8531
|
// src/harness-detect.ts
|
|
8457
8532
|
var DEFAULT_OPENCODE_SERVER_URL = "http://127.0.0.1:4096";
|
|
8458
8533
|
var DEFAULT_OPENCODE_PROBE_MS = 1e3;
|
|
@@ -10254,7 +10329,16 @@ var CompanionSupervisor = class {
|
|
|
10254
10329
|
},
|
|
10255
10330
|
runner: {
|
|
10256
10331
|
provider: this.runnerProvider,
|
|
10257
|
-
command:
|
|
10332
|
+
command: wrappedRunnerCommand(
|
|
10333
|
+
this.config.runner?.command,
|
|
10334
|
+
this.runnerCommand ?? selfRunnerCommand()
|
|
10335
|
+
),
|
|
10336
|
+
...this.config.runner?.command ? {
|
|
10337
|
+
boxed: true,
|
|
10338
|
+
secrets: () => loadSecretValuesTolerant(
|
|
10339
|
+
(m) => this.log.warn({ err: m }, "Could not read the secrets file for a boxed turn.")
|
|
10340
|
+
)
|
|
10341
|
+
} : {},
|
|
10258
10342
|
...this.runnerEnv ? { env: this.runnerEnv } : {},
|
|
10259
10343
|
reportRunnerGone: (report) => this.reportRunnerGone(report),
|
|
10260
10344
|
connectorHealth: this.connectorHealth,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.190",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|