@peakflo/agent-harness 0.2.17 → 0.2.18
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 +42 -0
- package/dist/adapter-resolver.d.ts +3 -1
- package/dist/adapter-resolver.js +2 -2
- package/dist/adapters/claude-code-adapter.d.ts +2 -1
- package/dist/adapters/claude-code-adapter.js +14 -82
- package/dist/adapters/vendor-environment.js +1 -0
- package/dist/cli.js +2 -1
- package/dist/harness-discovery.d.ts +4 -1
- package/dist/harness-discovery.js +18 -58
- package/dist/harness-path.d.ts +10 -0
- package/dist/harness-path.js +174 -0
- package/dist/harness.d.ts +2 -0
- package/dist/harness.js +33 -8
- package/dist/options.js +6 -0
- package/dist/service.d.ts +1 -1
- package/dist/service.js +34 -4
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -269,6 +269,48 @@ Cursor reports an account limit as a normal reply ("Upgrade your plan to
|
|
|
269
269
|
continue"). The runner turns a reply that is only that sentence into a failed
|
|
270
270
|
turn that says it comes from the Cursor account on the runner.
|
|
271
271
|
|
|
272
|
+
## Runner presence and Claude Code paths
|
|
273
|
+
|
|
274
|
+
Heartbeats use the last completed capability report. A slow CLI version probe
|
|
275
|
+
runs in the background and cannot delay the liveness request. Only one such
|
|
276
|
+
probe runs at a time. A later heartbeat carries the completed report.
|
|
277
|
+
|
|
278
|
+
The CLI search includes `~/.local/bin`, `~/.claude/local`, Homebrew, npm, pnpm,
|
|
279
|
+
Volta, Bun and installed NVM Node versions. If Claude is still missing, the
|
|
280
|
+
runner reads PATH from a bounded login-shell check. It keeps only absolute PATH
|
|
281
|
+
entries. It does not retain shell output. Runner credentials are removed from
|
|
282
|
+
the shell environment.
|
|
283
|
+
|
|
284
|
+
Set an absolute executable path to override discovery and the Claude adapter:
|
|
285
|
+
|
|
286
|
+
```sh
|
|
287
|
+
pf-agent-harness start --stored-credential --claude-path /absolute/path/to/claude
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`PF_CLAUDE_PATH=/absolute/path/to/claude` is the equivalent environment option.
|
|
291
|
+
`service install --claude-path /absolute/path/to/claude` saves the path in the
|
|
292
|
+
service configuration. Keep the other service install options for the existing
|
|
293
|
+
connection. `service repair --claude-path /absolute/path/to/claude` updates an
|
|
294
|
+
existing service. Select `--vendor claude-code` for a service made by Connect.
|
|
295
|
+
An invalid override fails discovery; it does not select a different executable.
|
|
296
|
+
|
|
297
|
+
Read-only checks for a Claude Code service made by Connect:
|
|
298
|
+
|
|
299
|
+
```sh
|
|
300
|
+
pf-agent-harness service status --vendor claude-code
|
|
301
|
+
command -v claude
|
|
302
|
+
claude --version
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
On macOS, this service writes logs to
|
|
306
|
+
`~/.pf-agent-harness/logs/claude-code/stdout.log` and `stderr.log`. On Linux,
|
|
307
|
+
use `journalctl --user -u pf-agent-harness-claude-code.service`. Do not share
|
|
308
|
+
tokens, credential files or unfiltered logs.
|
|
309
|
+
|
|
310
|
+
Release note: this fix needs a new `@peakflo/agent-harness` release and an
|
|
311
|
+
owner-controlled local service update. No sandbox image change is required.
|
|
312
|
+
A running service is not updated by these source changes.
|
|
313
|
+
|
|
272
314
|
## Troubleshooting
|
|
273
315
|
|
|
274
316
|
**`npx @peakflo/agent-harness` fails with a 404.** Your `~/.npmrc` maps the
|
|
@@ -68,4 +68,6 @@ export declare function vendorInstanceId(stateDir: string): string | null;
|
|
|
68
68
|
*/
|
|
69
69
|
export declare function createAdapterResolver(runtime: string,
|
|
70
70
|
/** The runner's own state directory (its workspace root). */
|
|
71
|
-
stateDir?: string
|
|
71
|
+
stateDir?: string, options?: {
|
|
72
|
+
claudePath?: string;
|
|
73
|
+
}): (backend: string, ports: AdapterPorts) => CodingAgentAdapter;
|
package/dist/adapter-resolver.js
CHANGED
|
@@ -290,14 +290,14 @@ function vendorInstanceId(stateDir) {
|
|
|
290
290
|
*/
|
|
291
291
|
function createAdapterResolver(runtime,
|
|
292
292
|
/** The runner's own state directory (its workspace root). */
|
|
293
|
-
stateDir) {
|
|
293
|
+
stateDir, options = {}) {
|
|
294
294
|
const instanceId = stateDir ? vendorInstanceId(stateDir) : null;
|
|
295
295
|
const build = (backend) => {
|
|
296
296
|
switch (backend) {
|
|
297
297
|
case 'codex':
|
|
298
298
|
return new adapters_1.CodexAppServerAdapter();
|
|
299
299
|
case 'claude-code':
|
|
300
|
-
return new adapters_1.ClaudeCodeAdapter();
|
|
300
|
+
return new adapters_1.ClaudeCodeAdapter(options.claudePath);
|
|
301
301
|
case 'cursor':
|
|
302
302
|
return new adapters_1.AcpAdapter('cursor');
|
|
303
303
|
default:
|
|
@@ -26,6 +26,7 @@ export declare enum ClaudeSystemSubtype {
|
|
|
26
26
|
THINKING_TOKENS = "thinking_tokens"
|
|
27
27
|
}
|
|
28
28
|
export declare class ClaudeCodeAdapter implements CodingAgentAdapter {
|
|
29
|
+
private readonly executableOverride?;
|
|
29
30
|
private sessions;
|
|
30
31
|
private sdkLoading;
|
|
31
32
|
private claudeExecutablePath;
|
|
@@ -36,7 +37,7 @@ export declare class ClaudeCodeAdapter implements CodingAgentAdapter {
|
|
|
36
37
|
* latency of the fixed-interval polling heartbeat.
|
|
37
38
|
*/
|
|
38
39
|
onDataAvailable?: (sessionId: string) => void;
|
|
39
|
-
constructor();
|
|
40
|
+
constructor(executableOverride?: string | undefined);
|
|
40
41
|
/**
|
|
41
42
|
* Find the Claude CLI executable path
|
|
42
43
|
*/
|
|
@@ -48,8 +48,8 @@ exports.assertClaudeIsolatedConfigVersion = assertClaudeIsolatedConfigVersion;
|
|
|
48
48
|
exports.verifyClaudeIsolatedConfigVersion = verifyClaudeIsolatedConfigVersion;
|
|
49
49
|
/**
|
|
50
50
|
* Extracted verbatim from 20x `src/main/adapters/claude-code-adapter.ts`.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* CLI resolution is shared with runner discovery. The remaining adapter
|
|
52
|
+
* behavior follows the desktop implementation. `@anthropic-ai/claude-agent-sdk` is an
|
|
53
53
|
* optional dependency (dynamically imported) and is declared ambiently in
|
|
54
54
|
* `./optional-agent-sdks.d.ts` so `tsc` succeeds when it is not installed.
|
|
55
55
|
* Source formatting (no semicolons) preserved.
|
|
@@ -57,11 +57,10 @@ exports.verifyClaudeIsolatedConfigVersion = verifyClaudeIsolatedConfigVersion;
|
|
|
57
57
|
const node_os_1 = require("node:os");
|
|
58
58
|
const node_path_1 = require("node:path");
|
|
59
59
|
const coding_agent_adapter_1 = require("./coding-agent-adapter");
|
|
60
|
+
const harness_path_1 = require("../harness-path");
|
|
60
61
|
const permission_mode_1 = require("../runtime/permission-mode");
|
|
61
62
|
const vendor_environment_1 = require("./vendor-environment");
|
|
62
63
|
let ClaudeAgentSDK = null;
|
|
63
|
-
/** Module-level cache for the resolved Claude executable path (persists across adapter instances) */
|
|
64
|
-
let resolvedClaudeExecutablePath = null;
|
|
65
64
|
const MINIMUM_ISOLATED_CONFIG_VERSION = [2, 1, 144];
|
|
66
65
|
function assertClaudeIsolatedConfigVersion(version) {
|
|
67
66
|
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:\s|$)/);
|
|
@@ -120,6 +119,7 @@ const TERMINAL_TASK_STATUSES = new Set(['completed', 'failed', 'killed', 'stoppe
|
|
|
120
119
|
*/
|
|
121
120
|
const MAX_BACKGROUND_TASK_AGE_MS = 60 * 60 * 1000; // 60 minutes
|
|
122
121
|
class ClaudeCodeAdapter {
|
|
122
|
+
executableOverride;
|
|
123
123
|
sessions = new Map();
|
|
124
124
|
sdkLoading = null;
|
|
125
125
|
claudeExecutablePath = null;
|
|
@@ -130,7 +130,8 @@ class ClaudeCodeAdapter {
|
|
|
130
130
|
* latency of the fixed-interval polling heartbeat.
|
|
131
131
|
*/
|
|
132
132
|
onDataAvailable;
|
|
133
|
-
constructor() {
|
|
133
|
+
constructor(executableOverride) {
|
|
134
|
+
this.executableOverride = executableOverride;
|
|
134
135
|
this.sdkLoading = this.loadSDK();
|
|
135
136
|
}
|
|
136
137
|
/**
|
|
@@ -140,84 +141,13 @@ class ClaudeCodeAdapter {
|
|
|
140
141
|
if (this.claudeExecutablePath) {
|
|
141
142
|
return this.claudeExecutablePath;
|
|
142
143
|
}
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
return this.claudeExecutablePath;
|
|
147
|
-
}
|
|
148
|
-
const isWin = process.platform === 'win32';
|
|
149
|
-
try {
|
|
150
|
-
const { execFile } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
151
|
-
const { promisify } = await Promise.resolve().then(() => __importStar(require('util')));
|
|
152
|
-
const execFileAsync = promisify(execFile);
|
|
153
|
-
// Try to find claude in PATH
|
|
154
|
-
const whichCmd = isWin ? 'where' : 'which';
|
|
155
|
-
const binaryName = 'claude';
|
|
156
|
-
const { stdout } = await execFileAsync(whichCmd, [binaryName]);
|
|
157
|
-
let found = stdout.trim().split(/\r?\n/)[0];
|
|
158
|
-
// On Windows, the SDK spawns the executable directly without shell,
|
|
159
|
-
// so .cmd files fail with EINVAL. Resolve .cmd → the underlying cli.js
|
|
160
|
-
// so the SDK uses `node cli.js` instead.
|
|
161
|
-
if (isWin) {
|
|
162
|
-
found = await this.resolveWindowsCmdToJs(found);
|
|
163
|
-
}
|
|
164
|
-
this.claudeExecutablePath = found;
|
|
165
|
-
resolvedClaudeExecutablePath = found;
|
|
166
|
-
console.log(`[ClaudeCodeAdapter] Found claude executable at: ${this.claudeExecutablePath}`);
|
|
167
|
-
return this.claudeExecutablePath;
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
// Common installation locations — must cover Homebrew, npm globals,
|
|
171
|
-
// pnpm globals, Volta, and NVM paths so the binary is found even when
|
|
172
|
-
// the shell PATH wasn't fully resolved (packaged macOS GUI app).
|
|
173
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
174
|
-
const commonPaths = isWin
|
|
175
|
-
? [
|
|
176
|
-
`${home}\\AppData\\Roaming\\npm\\node_modules\\@anthropic-ai\\claude-code\\cli.js`,
|
|
177
|
-
`${home}\\AppData\\Roaming\\npm\\claude.cmd`,
|
|
178
|
-
`${home}\\AppData\\Roaming\\npm\\claude.exe`,
|
|
179
|
-
`${home}\\.local\\bin\\claude.cmd`,
|
|
180
|
-
`${home}\\.local\\bin\\claude.exe`
|
|
181
|
-
]
|
|
182
|
-
: [
|
|
183
|
-
'/usr/local/bin/claude',
|
|
184
|
-
'/opt/homebrew/bin/claude',
|
|
185
|
-
`${home}/.local/bin/claude`,
|
|
186
|
-
`${home}/.npm-global/bin/claude`,
|
|
187
|
-
`${home}/Library/pnpm/claude`,
|
|
188
|
-
`${home}/.volta/bin/claude`,
|
|
189
|
-
];
|
|
190
|
-
// Dynamically detect NVM-managed npm global bin (avoids hardcoding a Node version)
|
|
191
|
-
if (!isWin && home) {
|
|
192
|
-
try {
|
|
193
|
-
const { join } = await Promise.resolve().then(() => __importStar(require('path')));
|
|
194
|
-
const { readdirSync } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
195
|
-
const nvmDir = join(home, '.nvm', 'versions', 'node');
|
|
196
|
-
const versions = readdirSync(nvmDir);
|
|
197
|
-
versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
198
|
-
for (const v of versions) {
|
|
199
|
-
commonPaths.push(join(nvmDir, v, 'bin', 'claude'));
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
// NVM not installed — skip
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
const { existsSync } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
207
|
-
for (const path of commonPaths) {
|
|
208
|
-
if (existsSync(path)) {
|
|
209
|
-
let resolved = path;
|
|
210
|
-
if (isWin && path.endsWith('.cmd')) {
|
|
211
|
-
resolved = await this.resolveWindowsCmdToJs(path);
|
|
212
|
-
}
|
|
213
|
-
this.claudeExecutablePath = resolved;
|
|
214
|
-
resolvedClaudeExecutablePath = resolved;
|
|
215
|
-
console.log(`[ClaudeCodeAdapter] Found claude executable at: ${this.claudeExecutablePath}`);
|
|
216
|
-
return this.claudeExecutablePath;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
throw new Error('Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code');
|
|
144
|
+
const found = await (0, harness_path_1.resolveClaudeCommand)(this.executableOverride);
|
|
145
|
+
if (!found) {
|
|
146
|
+
throw new Error('Claude Code not found. Install Claude Code or set --claude-path to its absolute executable path.');
|
|
220
147
|
}
|
|
148
|
+
this.claudeExecutablePath = process.platform === 'win32'
|
|
149
|
+
? await this.resolveWindowsCmdToJs(found) : found;
|
|
150
|
+
return this.claudeExecutablePath;
|
|
221
151
|
}
|
|
222
152
|
/**
|
|
223
153
|
* On Windows, .cmd wrapper files can't be spawned directly by the SDK
|
|
@@ -262,6 +192,8 @@ class ClaudeCodeAdapter {
|
|
|
262
192
|
ANTHROPIC_API_KEY: useApiKey ? config.apiKeys?.anthropic ?? process.env.ANTHROPIC_API_KEY : undefined,
|
|
263
193
|
CLAUDE_CODE_OAUTH_TOKEN: useApiKey ? undefined : process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
264
194
|
});
|
|
195
|
+
// Keep the installed GitHub guard first, and add service-safe CLI/Node paths.
|
|
196
|
+
env.PATH = (0, harness_path_1.harnessSearchPath)(env, (0, node_os_1.homedir)()).join(node_path_1.delimiter);
|
|
265
197
|
if (useApiKey)
|
|
266
198
|
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
267
199
|
else
|
package/dist/cli.js
CHANGED
|
@@ -87,6 +87,7 @@ Options:
|
|
|
87
87
|
--max-sessions <n> concurrent sessions (env PF_MAX_CONCURRENT_SESSIONS,
|
|
88
88
|
default 10 for desktop/self-hosted; sandbox is configured by control plane)
|
|
89
89
|
--labels <a,b> routing labels (env PF_RUNNER_LABELS)
|
|
90
|
+
--claude-path <path> absolute Claude CLI path (PF_CLAUDE_PATH)
|
|
90
91
|
--vendor-harnesses <list> allow codex,claude-code,cursor on this machine
|
|
91
92
|
(env PF_VENDOR_HARNESSES; default none)
|
|
92
93
|
--no-harness-discovery do not look for Codex, Claude Code, Cursor,
|
|
@@ -265,7 +266,7 @@ async function main() {
|
|
|
265
266
|
env: process.env
|
|
266
267
|
}).assertNoDuplicateRunner(options);
|
|
267
268
|
const handedOffCredential = (0, credential_store_1.takeHandedOffCredential)(startEnv);
|
|
268
|
-
const resolveAdapter = (0, adapter_resolver_1.createAdapterResolver)(options.runtime, options.workspaceRoot);
|
|
269
|
+
const resolveAdapter = (0, adapter_resolver_1.createAdapterResolver)(options.runtime, options.workspaceRoot, { claudePath: options.claudePath });
|
|
269
270
|
const harness = new harness_1.Harness(options, {
|
|
270
271
|
getAdapter: (backend, ports) => resolveAdapter(backend, ports),
|
|
271
272
|
...(handedOffCredential
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* 4. NO GLOBAL SIDE EFFECTS. `process.env.PATH` is not changed; the search path
|
|
29
29
|
* is local and is handed to each child process.
|
|
30
30
|
*/
|
|
31
|
+
import { harnessSearchPath } from './harness-path';
|
|
31
32
|
import type { RunnerHarness, RunnerHarnessModel } from './types';
|
|
32
33
|
/**
|
|
33
34
|
* One command: `--version`, a status check, or a model listing.
|
|
@@ -112,6 +113,8 @@ export interface DiscoverHarnessesOptions {
|
|
|
112
113
|
*/
|
|
113
114
|
only?: readonly string[];
|
|
114
115
|
deps?: Partial<DiscoveryDeps>;
|
|
116
|
+
/** Operator-selected Claude executable; also used by the turn adapter. */
|
|
117
|
+
claudePath?: string;
|
|
115
118
|
}
|
|
116
119
|
/**
|
|
117
120
|
* Where to look for a harness binary: PATH first, then the directories the
|
|
@@ -120,7 +123,7 @@ export interface DiscoverHarnessesOptions {
|
|
|
120
123
|
* of the running `node` is added because a runner started by launchd or
|
|
121
124
|
* systemd has a short PATH, and an npm-installed CLI sits beside that `node`.
|
|
122
125
|
*/
|
|
123
|
-
export declare
|
|
126
|
+
export declare const discoverySearchPath: typeof harnessSearchPath;
|
|
124
127
|
/**
|
|
125
128
|
* The environment a harness CLI is started with.
|
|
126
129
|
*
|
|
@@ -30,8 +30,7 @@
|
|
|
30
30
|
* is local and is handed to each child process.
|
|
31
31
|
*/
|
|
32
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
-
exports.KNOWN_HARNESSES = exports.CURSOR_STATIC_MODELS = exports.CODEX_STATIC_MODELS = exports.CLAUDE_STATIC_MODELS = exports.REASONS = exports.DISCOVERY_CONCURRENCY = exports.DISCOVERY_HARNESS_TIMEOUT_MS = exports.DISCOVERY_COMMAND_TIMEOUT_MS = void 0;
|
|
34
|
-
exports.discoverySearchPath = discoverySearchPath;
|
|
33
|
+
exports.KNOWN_HARNESSES = exports.discoverySearchPath = exports.CURSOR_STATIC_MODELS = exports.CODEX_STATIC_MODELS = exports.CLAUDE_STATIC_MODELS = exports.REASONS = exports.DISCOVERY_CONCURRENCY = exports.DISCOVERY_HARNESS_TIMEOUT_MS = exports.DISCOVERY_COMMAND_TIMEOUT_MS = void 0;
|
|
35
34
|
exports.discoveryChildEnv = discoveryChildEnv;
|
|
36
35
|
exports.sanitizeHarnessModels = sanitizeHarnessModels;
|
|
37
36
|
exports.parseVersion = parseVersion;
|
|
@@ -45,6 +44,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
45
44
|
const node_fs_1 = require("node:fs");
|
|
46
45
|
const node_os_1 = require("node:os");
|
|
47
46
|
const node_path_1 = require("node:path");
|
|
47
|
+
const harness_path_1 = require("./harness-path");
|
|
48
48
|
/**
|
|
49
49
|
* One command: `--version`, a status check, or a model listing.
|
|
50
50
|
*
|
|
@@ -142,57 +142,7 @@ exports.CURSOR_STATIC_MODELS = [
|
|
|
142
142
|
* of the running `node` is added because a runner started by launchd or
|
|
143
143
|
* systemd has a short PATH, and an npm-installed CLI sits beside that `node`.
|
|
144
144
|
*/
|
|
145
|
-
|
|
146
|
-
/*
|
|
147
|
-
* ABSOLUTE ENTRIES ONLY. A relative entry (`.`, `node_modules/.bin`) means
|
|
148
|
-
* "whatever is in the directory the runner was started from", and a file
|
|
149
|
-
* named `codex` there is not the user's Codex.
|
|
150
|
-
*/
|
|
151
|
-
const fromEnv = (env.PATH ?? env.Path ?? '')
|
|
152
|
-
.split(node_path_1.delimiter)
|
|
153
|
-
.filter((entry) => entry !== '' && (0, node_path_1.isAbsolute)(entry));
|
|
154
|
-
const known = platform === 'win32'
|
|
155
|
-
? [
|
|
156
|
-
(0, node_path_1.join)(home, 'AppData', 'Roaming', 'npm'),
|
|
157
|
-
(0, node_path_1.join)(home, '.local', 'bin'),
|
|
158
|
-
(0, node_path_1.join)(home, '.opencode', 'bin')
|
|
159
|
-
]
|
|
160
|
-
: [
|
|
161
|
-
(0, node_path_1.join)(home, '.local', 'bin'),
|
|
162
|
-
(0, node_path_1.join)(home, '.opencode', 'bin'),
|
|
163
|
-
'/opt/homebrew/bin',
|
|
164
|
-
'/usr/local/bin',
|
|
165
|
-
(0, node_path_1.join)(home, '.npm-global', 'bin'),
|
|
166
|
-
(0, node_path_1.join)(home, '.volta', 'bin'),
|
|
167
|
-
(0, node_path_1.join)(home, '.bun', 'bin')
|
|
168
|
-
];
|
|
169
|
-
return [...new Set([...fromEnv, ...known, (0, node_path_1.dirname)(process.execPath)])];
|
|
170
|
-
}
|
|
171
|
-
function isExecutableFile(path) {
|
|
172
|
-
try {
|
|
173
|
-
if (!(0, node_fs_1.statSync)(path).isFile())
|
|
174
|
-
return false;
|
|
175
|
-
(0, node_fs_1.accessSync)(path, node_fs_1.constants.X_OK);
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
function defaultResolveCommand(command, searchPath, env) {
|
|
183
|
-
// npm installs `.cmd` wrappers on Windows, so the bare name is not a file.
|
|
184
|
-
const extensions = process.platform === 'win32'
|
|
185
|
-
? (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
|
|
186
|
-
: [''];
|
|
187
|
-
for (const dir of searchPath) {
|
|
188
|
-
for (const extension of extensions) {
|
|
189
|
-
const candidate = (0, node_path_1.join)(dir, `${command}${extension}`);
|
|
190
|
-
if (isExecutableFile(candidate))
|
|
191
|
-
return candidate;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
145
|
+
exports.discoverySearchPath = harness_path_1.harnessSearchPath;
|
|
196
146
|
function defaultExec(command, args, timeoutMs, env) {
|
|
197
147
|
return new Promise((resolve) => {
|
|
198
148
|
let settled = false;
|
|
@@ -275,14 +225,24 @@ function discoveryChildEnv(env, searchPath) {
|
|
|
275
225
|
child.PATH = searchPath.join(node_path_1.delimiter);
|
|
276
226
|
return child;
|
|
277
227
|
}
|
|
278
|
-
function resolveDeps(overrides = {}) {
|
|
279
|
-
const env =
|
|
228
|
+
async function resolveDeps(overrides = {}, claudePath) {
|
|
229
|
+
const env = {
|
|
230
|
+
...(overrides.env ?? process.env),
|
|
231
|
+
...(claudePath ? { PF_CLAUDE_PATH: claudePath } : {})
|
|
232
|
+
};
|
|
233
|
+
if (!overrides.env && !env.SHELL)
|
|
234
|
+
env.SHELL = (0, harness_path_1.defaultLoginShell)();
|
|
280
235
|
const home = overrides.home ?? (0, node_os_1.homedir)();
|
|
281
|
-
const searchPath = discoverySearchPath(env, home);
|
|
236
|
+
const searchPath = (0, exports.discoverySearchPath)(env, home);
|
|
237
|
+
if (!overrides.resolveCommand &&
|
|
238
|
+
!(0, harness_path_1.resolveHarnessCommand)('claude', searchPath, env) &&
|
|
239
|
+
!env.PF_CLAUDE_PATH) {
|
|
240
|
+
searchPath.push(...(await (0, harness_path_1.loginShellSearchPath)(env)));
|
|
241
|
+
}
|
|
282
242
|
const childEnv = discoveryChildEnv(env, searchPath);
|
|
283
243
|
return {
|
|
284
244
|
exec: (command, args, timeoutMs) => defaultExec(command, args, timeoutMs, childEnv),
|
|
285
|
-
resolveCommand: (command) =>
|
|
245
|
+
resolveCommand: (command) => (0, harness_path_1.resolveHarnessCommand)(command, searchPath, env),
|
|
286
246
|
realpath: (path) => (0, node_fs_1.realpathSync)(path),
|
|
287
247
|
readFile: (path) => {
|
|
288
248
|
try {
|
|
@@ -739,7 +699,7 @@ async function discoverHarnesses(options = {}) {
|
|
|
739
699
|
let deps = null;
|
|
740
700
|
let detectedAt = new Date().toISOString();
|
|
741
701
|
try {
|
|
742
|
-
deps = resolveDeps(options.deps);
|
|
702
|
+
deps = await resolveDeps(options.deps, options.claudePath);
|
|
743
703
|
detectedAt = deps.now().toISOString();
|
|
744
704
|
}
|
|
745
705
|
catch {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const LOGIN_PATH_TIMEOUT_MS = 2000;
|
|
2
|
+
/** launchd can omit SHELL even though the account has a configured login shell. */
|
|
3
|
+
export declare function defaultLoginShell(): string | undefined;
|
|
4
|
+
export declare function harnessSearchPath(env: NodeJS.ProcessEnv, home: string, platform?: NodeJS.Platform): string[];
|
|
5
|
+
/** Only PATH is retained. Shell startup output and runner credentials are never returned. */
|
|
6
|
+
export declare function loginShellSearchPath(env: NodeJS.ProcessEnv, timeoutMs?: number): Promise<string[]>;
|
|
7
|
+
export declare function executableFile(path: string): boolean;
|
|
8
|
+
export declare function resolveHarnessCommand(command: string, searchPath: readonly string[], env: NodeJS.ProcessEnv): string | null;
|
|
9
|
+
/** Shared by discovery and the adapter, so a detected CLI is the CLI that runs. */
|
|
10
|
+
export declare function resolveClaudeCommand(explicitPath?: string): Promise<string | null>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LOGIN_PATH_TIMEOUT_MS = void 0;
|
|
4
|
+
exports.defaultLoginShell = defaultLoginShell;
|
|
5
|
+
exports.harnessSearchPath = harnessSearchPath;
|
|
6
|
+
exports.loginShellSearchPath = loginShellSearchPath;
|
|
7
|
+
exports.executableFile = executableFile;
|
|
8
|
+
exports.resolveHarnessCommand = resolveHarnessCommand;
|
|
9
|
+
exports.resolveClaudeCommand = resolveClaudeCommand;
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_os_1 = require("node:os");
|
|
13
|
+
const node_path_1 = require("node:path");
|
|
14
|
+
exports.LOGIN_PATH_TIMEOUT_MS = 2_000;
|
|
15
|
+
const PATH_MARKER = '__PF_HARNESS_PATH__';
|
|
16
|
+
/** launchd can omit SHELL even though the account has a configured login shell. */
|
|
17
|
+
function defaultLoginShell() {
|
|
18
|
+
if (process.platform === 'win32')
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
return (0, node_os_1.userInfo)().shell ?? '/bin/sh';
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return process.platform === 'darwin' ? '/bin/zsh' : '/bin/sh';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function harnessSearchPath(env, home, platform = process.platform) {
|
|
28
|
+
const inherited = (env.PATH ?? env.Path ?? '')
|
|
29
|
+
.split(node_path_1.delimiter)
|
|
30
|
+
.filter(node_path_1.isAbsolute);
|
|
31
|
+
const known = platform === 'win32'
|
|
32
|
+
? [
|
|
33
|
+
(0, node_path_1.join)(home, 'AppData', 'Roaming', 'npm'),
|
|
34
|
+
(0, node_path_1.join)(home, '.local', 'bin'),
|
|
35
|
+
(0, node_path_1.join)(home, '.opencode', 'bin')
|
|
36
|
+
]
|
|
37
|
+
: [
|
|
38
|
+
(0, node_path_1.join)(home, '.local', 'bin'),
|
|
39
|
+
(0, node_path_1.join)(home, '.claude', 'local'),
|
|
40
|
+
(0, node_path_1.join)(home, '.opencode', 'bin'),
|
|
41
|
+
'/opt/homebrew/bin',
|
|
42
|
+
'/usr/local/bin',
|
|
43
|
+
(0, node_path_1.join)(home, '.npm-global', 'bin'),
|
|
44
|
+
(0, node_path_1.join)(home, 'Library', 'pnpm'),
|
|
45
|
+
(0, node_path_1.join)(home, '.volta', 'bin'),
|
|
46
|
+
(0, node_path_1.join)(home, '.bun', 'bin')
|
|
47
|
+
];
|
|
48
|
+
if (platform !== 'win32') {
|
|
49
|
+
try {
|
|
50
|
+
const nvm = (0, node_path_1.join)(home, '.nvm', 'versions', 'node');
|
|
51
|
+
const versions = (0, node_fs_1.readdirSync)(nvm).filter((name) => /^v\d+[.\d]*$/.test(name));
|
|
52
|
+
versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
53
|
+
known.push(...versions.slice(0, 32).map((version) => (0, node_path_1.join)(nvm, version, 'bin')));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* NVM is optional. */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...new Set([...inherited, ...known, (0, node_path_1.dirname)(process.execPath)])];
|
|
60
|
+
}
|
|
61
|
+
/** Only PATH is retained. Shell startup output and runner credentials are never returned. */
|
|
62
|
+
function loginShellSearchPath(env, timeoutMs = exports.LOGIN_PATH_TIMEOUT_MS) {
|
|
63
|
+
const shell = env.SHELL;
|
|
64
|
+
if (!shell ||
|
|
65
|
+
!(0, node_path_1.isAbsolute)(shell) ||
|
|
66
|
+
!['zsh', 'bash', 'sh'].includes((0, node_path_1.basename)(shell))) {
|
|
67
|
+
return Promise.resolve([]);
|
|
68
|
+
}
|
|
69
|
+
const childEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !/^PF_/i.test(key)));
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
let settled = false;
|
|
72
|
+
let child = null;
|
|
73
|
+
let timer = null;
|
|
74
|
+
const killProbe = () => {
|
|
75
|
+
try {
|
|
76
|
+
if (process.platform !== 'win32' && child?.pid) {
|
|
77
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
child?.kill('SIGKILL');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* The shell can already have exited. */
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const finish = (paths) => {
|
|
87
|
+
if (settled)
|
|
88
|
+
return;
|
|
89
|
+
settled = true;
|
|
90
|
+
if (timer)
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
resolve(paths);
|
|
93
|
+
};
|
|
94
|
+
timer = setTimeout(() => {
|
|
95
|
+
killProbe();
|
|
96
|
+
finish([]);
|
|
97
|
+
}, timeoutMs + 100);
|
|
98
|
+
timer.unref();
|
|
99
|
+
try {
|
|
100
|
+
let output = '';
|
|
101
|
+
let bytes = 0;
|
|
102
|
+
child = (0, node_child_process_1.spawn)(shell, ['-ilc', `printf '\\n${PATH_MARKER}%s\\n' "$PATH"`], {
|
|
103
|
+
env: childEnv,
|
|
104
|
+
detached: process.platform !== 'win32',
|
|
105
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
106
|
+
});
|
|
107
|
+
child.stdout?.on('data', (chunk) => {
|
|
108
|
+
bytes += chunk.length;
|
|
109
|
+
if (bytes > 128 * 1024) {
|
|
110
|
+
killProbe();
|
|
111
|
+
finish([]);
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
output += chunk.toString();
|
|
115
|
+
});
|
|
116
|
+
child.on('close', (code) => {
|
|
117
|
+
if (code !== 0)
|
|
118
|
+
return finish([]);
|
|
119
|
+
const line = output
|
|
120
|
+
.split('\n')
|
|
121
|
+
.find((entry) => entry.startsWith(PATH_MARKER));
|
|
122
|
+
finish((line?.slice(PATH_MARKER.length) ?? '')
|
|
123
|
+
.split(node_path_1.delimiter)
|
|
124
|
+
.filter(node_path_1.isAbsolute));
|
|
125
|
+
});
|
|
126
|
+
child.on('error', () => finish([]));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
finish([]);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function executableFile(path) {
|
|
134
|
+
try {
|
|
135
|
+
if (!(0, node_fs_1.statSync)(path).isFile())
|
|
136
|
+
return false;
|
|
137
|
+
(0, node_fs_1.accessSync)(path, node_fs_1.constants.X_OK);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function resolveHarnessCommand(command, searchPath, env) {
|
|
145
|
+
if (command === 'claude' && env.PF_CLAUDE_PATH) {
|
|
146
|
+
const explicit = env.PF_CLAUDE_PATH;
|
|
147
|
+
// An invalid explicit choice must not silently select a different binary.
|
|
148
|
+
return (0, node_path_1.isAbsolute)(explicit) && executableFile(explicit) ? explicit : null;
|
|
149
|
+
}
|
|
150
|
+
const extensions = process.platform === 'win32'
|
|
151
|
+
? (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
|
|
152
|
+
: [''];
|
|
153
|
+
for (const dir of searchPath) {
|
|
154
|
+
for (const extension of extensions) {
|
|
155
|
+
const candidate = (0, node_path_1.join)(dir, `${command}${extension}`);
|
|
156
|
+
if (executableFile(candidate))
|
|
157
|
+
return candidate;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
/** Shared by discovery and the adapter, so a detected CLI is the CLI that runs. */
|
|
163
|
+
async function resolveClaudeCommand(explicitPath) {
|
|
164
|
+
const env = {
|
|
165
|
+
...process.env,
|
|
166
|
+
SHELL: process.env.SHELL ?? defaultLoginShell(),
|
|
167
|
+
...(explicitPath ? { PF_CLAUDE_PATH: explicitPath } : {})
|
|
168
|
+
};
|
|
169
|
+
const paths = harnessSearchPath(env, (0, node_os_1.homedir)());
|
|
170
|
+
const found = resolveHarnessCommand('claude', paths, env);
|
|
171
|
+
if (found || env.PF_CLAUDE_PATH)
|
|
172
|
+
return found;
|
|
173
|
+
return resolveHarnessCommand('claude', [...paths, ...(await loginShellSearchPath(env))], env);
|
|
174
|
+
}
|
package/dist/harness.d.ts
CHANGED
|
@@ -233,6 +233,7 @@ export declare class Harness {
|
|
|
233
233
|
private heartbeatIntervalMs;
|
|
234
234
|
private leaseWaitSeconds;
|
|
235
235
|
private heartbeatTimer;
|
|
236
|
+
private capabilityProbeInFlight;
|
|
236
237
|
/** Fires when the runner credential should be rotated. Durable runtimes re-persist the new one. */
|
|
237
238
|
private rotateTimer;
|
|
238
239
|
private readonly credentialStore;
|
|
@@ -429,6 +430,7 @@ export declare class Harness {
|
|
|
429
430
|
*/
|
|
430
431
|
private modelsNeedRefresh;
|
|
431
432
|
private scheduleHeartbeat;
|
|
433
|
+
private refreshCapabilities;
|
|
432
434
|
private sendHeartbeat;
|
|
433
435
|
/** A bad command must not stop the others or fail a transcript ingest. */
|
|
434
436
|
private applyCommandsSafely;
|
package/dist/harness.js
CHANGED
|
@@ -252,6 +252,7 @@ class Harness {
|
|
|
252
252
|
heartbeatIntervalMs;
|
|
253
253
|
leaseWaitSeconds = contract_1.DEFAULT_LEASE_WAIT_SECONDS;
|
|
254
254
|
heartbeatTimer = null;
|
|
255
|
+
capabilityProbeInFlight = null;
|
|
255
256
|
/** Fires when the runner credential should be rotated. Durable runtimes re-persist the new one. */
|
|
256
257
|
rotateTimer = null;
|
|
257
258
|
credentialStore;
|
|
@@ -928,6 +929,9 @@ class Harness {
|
|
|
928
929
|
try {
|
|
929
930
|
const fresh = await discover({
|
|
930
931
|
runnableBackends: (this.capabilities?.backends ?? []).map((backend) => backend.name),
|
|
932
|
+
...(this.options.claudePath
|
|
933
|
+
? { claudePath: this.options.claudePath }
|
|
934
|
+
: {}),
|
|
931
935
|
...(only ? { only } : {})
|
|
932
936
|
});
|
|
933
937
|
const report = mergeHarnessReports(this.harnessReport, fresh);
|
|
@@ -1168,16 +1172,37 @@ class Harness {
|
|
|
1168
1172
|
// Never hold the event loop open on the heartbeat alone.
|
|
1169
1173
|
this.heartbeatTimer.unref();
|
|
1170
1174
|
}
|
|
1175
|
+
refreshCapabilities() {
|
|
1176
|
+
if (this.capabilityProbeInFlight || this.stopping)
|
|
1177
|
+
return;
|
|
1178
|
+
const pass = (async () => {
|
|
1179
|
+
try {
|
|
1180
|
+
const capabilities = await (0, capabilities_1.probeCapabilities)({
|
|
1181
|
+
maxConcurrentSessions: this.options.maxConcurrentSessions,
|
|
1182
|
+
labels: this.options.labels
|
|
1183
|
+
});
|
|
1184
|
+
if (!this.stopping)
|
|
1185
|
+
this.capabilities = capabilities;
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
// A failed capability refresh must not hide an otherwise healthy runner.
|
|
1189
|
+
log('Capability refresh failed; keeping the last report');
|
|
1190
|
+
}
|
|
1191
|
+
})();
|
|
1192
|
+
this.capabilityProbeInFlight = pass;
|
|
1193
|
+
void pass.finally(() => {
|
|
1194
|
+
if (this.capabilityProbeInFlight === pass)
|
|
1195
|
+
this.capabilityProbeInFlight = null;
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1171
1198
|
async sendHeartbeat() {
|
|
1172
1199
|
try {
|
|
1173
|
-
//
|
|
1174
|
-
//
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
labels: this.options.labels
|
|
1180
|
-
});
|
|
1200
|
+
// CLI version probes can hang. Keep the last completed report and send
|
|
1201
|
+
// liveness now; one background pass updates the following heartbeat.
|
|
1202
|
+
const capabilities = this.capabilities;
|
|
1203
|
+
if (!capabilities)
|
|
1204
|
+
throw new Error('Runner has no initial capabilities.');
|
|
1205
|
+
this.refreshCapabilities();
|
|
1181
1206
|
this.applyModelCatalog(capabilities);
|
|
1182
1207
|
this.applyHarnessReport(capabilities);
|
|
1183
1208
|
this.capabilities = capabilities;
|
package/dist/options.js
CHANGED
|
@@ -219,7 +219,13 @@ function parseOptions(argv, env) {
|
|
|
219
219
|
// Resolved before the object literal, because the workspace-root DEFAULT
|
|
220
220
|
// depends on it.
|
|
221
221
|
const runtime = parseRuntime(readFlag(argv, 'runtime') ?? env.PF_RUNNER_RUNTIME);
|
|
222
|
+
const claudePath = readFlag(argv, 'claude-path') ?? env.PF_CLAUDE_PATH;
|
|
223
|
+
if (claudePath !== undefined &&
|
|
224
|
+
(!(0, node_path_1.isAbsolute)(claudePath) || /[\r\n\0]/.test(claudePath))) {
|
|
225
|
+
throw new OptionsError('Claude path must be an absolute executable path.');
|
|
226
|
+
}
|
|
222
227
|
return {
|
|
228
|
+
...(claudePath ? { claudePath } : {}),
|
|
223
229
|
controlPlaneUrl,
|
|
224
230
|
enrolmentToken: enrolmentToken ?? '',
|
|
225
231
|
healthPort: parsePort(readFlag(argv, 'health-port') ?? env.PF_HARNESS_HEALTH_PORT, DEFAULT_HEALTH_PORT),
|
package/dist/service.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export declare class ServiceManager {
|
|
|
63
63
|
install(input: string[]): Promise<void>;
|
|
64
64
|
private vendorFlag;
|
|
65
65
|
private checkNodeVersion;
|
|
66
|
-
repair(): void;
|
|
66
|
+
repair(input?: string[]): void;
|
|
67
67
|
assertNoDuplicateRunner(options: {
|
|
68
68
|
workspaceRoot: string;
|
|
69
69
|
healthPort: number;
|
package/dist/service.js
CHANGED
|
@@ -35,7 +35,8 @@ const VALUE_FLAGS = new Set([
|
|
|
35
35
|
'--runtime',
|
|
36
36
|
'--max-sessions',
|
|
37
37
|
'--labels',
|
|
38
|
-
'--vendor-harnesses'
|
|
38
|
+
'--vendor-harnesses',
|
|
39
|
+
'--claude-path'
|
|
39
40
|
]);
|
|
40
41
|
const SWITCH_FLAGS = new Set(['--no-harness-discovery']);
|
|
41
42
|
/** Service names for an instance. Only vendor names are accepted. */
|
|
@@ -172,6 +173,12 @@ function serviceStartArgs(input, env, home) {
|
|
|
172
173
|
if (env.PF_VENDOR_HARNESSES && !values.has('--vendor-harnesses')) {
|
|
173
174
|
values.set('--vendor-harnesses', safe(env.PF_VENDOR_HARNESSES));
|
|
174
175
|
}
|
|
176
|
+
const claudePath = values.get('--claude-path') ?? env.PF_CLAUDE_PATH;
|
|
177
|
+
if (claudePath !== undefined) {
|
|
178
|
+
if (!(0, node_path_1.isAbsolute)(claudePath))
|
|
179
|
+
throw new Error('Claude path must be absolute.');
|
|
180
|
+
values.set('--claude-path', safe(claudePath));
|
|
181
|
+
}
|
|
175
182
|
const vendorNames = values.get('--vendor-harnesses');
|
|
176
183
|
if (vendorNames !== undefined) {
|
|
177
184
|
const names = vendorNames
|
|
@@ -430,13 +437,36 @@ class ServiceManager {
|
|
|
430
437
|
if (floor.action === 'refuse')
|
|
431
438
|
throw new Error(floor.message);
|
|
432
439
|
}
|
|
433
|
-
repair() {
|
|
440
|
+
repair(input = []) {
|
|
434
441
|
this.supported();
|
|
435
442
|
this.checkNodeVersion();
|
|
436
443
|
const prior = this.readConfig();
|
|
437
|
-
this.
|
|
444
|
+
let claudePath = this.context.env.PF_CLAUDE_PATH;
|
|
445
|
+
for (let index = 0; index < input.length; index++) {
|
|
446
|
+
const flag = input[index];
|
|
447
|
+
if (flag === '--claude-path')
|
|
448
|
+
claudePath = input[++index];
|
|
449
|
+
else if (flag.startsWith('--claude-path='))
|
|
450
|
+
claudePath = flag.slice(14);
|
|
451
|
+
else
|
|
452
|
+
throw new Error('Service repair accepts only --claude-path.');
|
|
453
|
+
if (!claudePath)
|
|
454
|
+
throw new Error('Missing value for --claude-path.');
|
|
455
|
+
}
|
|
456
|
+
const startArgs = [...prior.startArgs];
|
|
457
|
+
if (claudePath !== undefined) {
|
|
458
|
+
if (!(0, node_path_1.isAbsolute)(claudePath))
|
|
459
|
+
throw new Error('Claude path must be absolute.');
|
|
460
|
+
const at = startArgs.indexOf('--claude-path');
|
|
461
|
+
if (at >= 0)
|
|
462
|
+
startArgs.splice(at, 2);
|
|
463
|
+
startArgs.push('--claude-path', safe(claudePath));
|
|
464
|
+
}
|
|
465
|
+
(0, options_1.parseOptions)(startArgs, {});
|
|
466
|
+
this.checkCredential(startArgs);
|
|
438
467
|
this.writeAndStart({
|
|
439
468
|
...prior,
|
|
469
|
+
startArgs,
|
|
440
470
|
nodePath: this.context.nodePath,
|
|
441
471
|
cliPath: this.context.cliPath,
|
|
442
472
|
path: this.context.env.PATH ?? prior.path
|
|
@@ -576,7 +606,7 @@ async function runServiceCommand(input, context) {
|
|
|
576
606
|
manager.status();
|
|
577
607
|
break;
|
|
578
608
|
case 'repair':
|
|
579
|
-
manager.repair();
|
|
609
|
+
manager.repair(args.slice(1));
|
|
580
610
|
break;
|
|
581
611
|
case 'uninstall':
|
|
582
612
|
manager.uninstall();
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
* Do not "improve" this by adding a `--token` flag.
|
|
39
39
|
*/
|
|
40
40
|
export interface HarnessOptions {
|
|
41
|
+
/** Absolute operator-selected Claude executable. Never a shell command. */
|
|
42
|
+
claudePath?: string;
|
|
41
43
|
/** workflow-api base url, e.g. https://stage-api.peakflo.ai */
|
|
42
44
|
controlPlaneUrl: string;
|
|
43
45
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peakflo/agent-harness",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"description": "Headless Peakflo coding-agent harness. One codebase, three hosting modes: a Daytona cloud sandbox, a customer VM, and the 20x desktop.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|