@peakflo/agent-harness 0.2.8 → 0.2.9-rc.2
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 +67 -1
- package/dist/cli.js +29 -0
- package/dist/harness-discovery.d.ts +173 -0
- package/dist/harness-discovery.js +812 -0
- package/dist/harness.d.ts +98 -1
- package/dist/harness.js +300 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +14 -2
- package/dist/node-floor.d.ts +46 -0
- package/dist/node-floor.js +76 -0
- package/dist/options.js +17 -1
- package/dist/store/conversation-store.js +2 -0
- package/dist/store/sqlite-store.d.ts +17 -0
- package/dist/store/sqlite-store.js +26 -0
- package/dist/types.d.ts +83 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@ Run a Peakflo coding agent on your machine.
|
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
|
-
Use Node.js 22.19 or newer and install Git.
|
|
7
|
+
Use Node.js 22.19 or newer and install Git. The runner checks the Node.js
|
|
8
|
+
version when it starts and stops with one line if it is older (see
|
|
9
|
+
"Troubleshooting"). Install the Pi coding agent once:
|
|
8
10
|
|
|
9
11
|
```sh
|
|
10
12
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
|
|
@@ -36,6 +38,70 @@ Optional flags include `--health-port <port>`, `--runtime <desktop|self-hosted>`
|
|
|
36
38
|
`--workspace-root <path>`, and `--github-proxy-port <port>`. Run
|
|
37
39
|
`npx @peakflo/agent-harness@rc --help` for the full list.
|
|
38
40
|
|
|
41
|
+
## Harnesses found on your machine
|
|
42
|
+
|
|
43
|
+
On a desktop or self-hosted machine the runner looks for Codex (`codex`),
|
|
44
|
+
Claude Code (`claude`), Cursor (`cursor-agent`), Pi (`pi`) and OpenCode
|
|
45
|
+
(`opencode`). For each one it reports whether it is installed, its version,
|
|
46
|
+
whether it is signed in, and the models it offers. Peakflo shows this list in
|
|
47
|
+
the agent editor and in the chat details.
|
|
48
|
+
|
|
49
|
+
Finding a harness does not mean the runner can use it. The runner runs turns
|
|
50
|
+
with Pi only; the other harnesses are shown as "Detected, not yet runnable by
|
|
51
|
+
this runner".
|
|
52
|
+
|
|
53
|
+
**When it looks.** Once after start, without delaying registration, and then
|
|
54
|
+
every five minutes while no turn is running. A harness you install or sign in
|
|
55
|
+
to later appears without a restart. At most two of these programs run at the
|
|
56
|
+
same time.
|
|
57
|
+
|
|
58
|
+
**A slow machine.** Some of these programs take several seconds to start. When
|
|
59
|
+
one does not answer in time, Peakflo shows it as "Installed — check timed out,
|
|
60
|
+
will retry", not as unavailable. The runner checks it again after 30 seconds,
|
|
61
|
+
then 60, then 120, and after that with the normal five-minute pass. A state
|
|
62
|
+
that was already known (signed in, or not signed in) is kept when a later
|
|
63
|
+
check times out.
|
|
64
|
+
|
|
65
|
+
**What it runs.** To do this the runner starts those programs, as your user,
|
|
66
|
+
with `--version` and a status or model-list command. It looks in the absolute
|
|
67
|
+
directories of your `PATH` and in the usual install directories
|
|
68
|
+
(`~/.local/bin`, `~/.opencode/bin`, `/opt/homebrew/bin`, `/usr/local/bin`,
|
|
69
|
+
`~/.npm-global/bin`, `~/.volta/bin`, `~/.bun/bin`, and the directory of the
|
|
70
|
+
running `node`). Relative `PATH` entries are ignored. The programs are started
|
|
71
|
+
without the runner's own `PF_*` variables, so they do not receive the
|
|
72
|
+
enrolment token.
|
|
73
|
+
|
|
74
|
+
**What it sends.** Names, versions, a signed-in yes/no with a fixed reason,
|
|
75
|
+
and model names. It does not send file paths, account names, tokens, API keys
|
|
76
|
+
or the content of credential files. Model names that look like an email
|
|
77
|
+
address or a key are dropped; that check is a safety net, not a guarantee
|
|
78
|
+
about what a third-party program prints.
|
|
79
|
+
|
|
80
|
+
**Turning it off.** Start with `--no-harness-discovery`, or set
|
|
81
|
+
`PF_HARNESS_DISCOVERY=0`. The runner then starts none of these programs and
|
|
82
|
+
reports no list. Running turns with Pi is not affected.
|
|
83
|
+
|
|
84
|
+
## Troubleshooting
|
|
85
|
+
|
|
86
|
+
**`npx @peakflo/agent-harness` fails with a 404.** Your `~/.npmrc` maps the
|
|
87
|
+
`@peakflo` scope to another registry (for example GitHub Packages). Name the
|
|
88
|
+
public registry for this one command:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
npx --@peakflo:registry=https://registry.npmjs.org @peakflo/agent-harness@latest start
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**"pf-agent-harness needs Node.js 22.19 or newer".** Switch to a newer
|
|
95
|
+
Node.js, for example `nvm install 22 && nvm use 22`, and start again. npm only
|
|
96
|
+
prints an `EBADENGINE` warning for an older Node.js and installs anyway; the
|
|
97
|
+
runner is what stops. The requirement comes from Pi, which the runner starts
|
|
98
|
+
with the same Node.js. To start on an older Node.js at your own risk, set
|
|
99
|
+
`PF_ALLOW_UNSUPPORTED_NODE=1`.
|
|
100
|
+
|
|
101
|
+
**"Durable store unavailable, running in memory".** The runner could not
|
|
102
|
+
create or open its database under `--workspace-root`. It creates that
|
|
103
|
+
directory itself when it is missing, so check that the path is writable.
|
|
104
|
+
|
|
39
105
|
## Releasing
|
|
40
106
|
|
|
41
107
|
`.github/scripts/agent-harness-release.mjs` publishes this package
|
package/dist/cli.js
CHANGED
|
@@ -38,6 +38,7 @@ const adapters_1 = require("./adapters");
|
|
|
38
38
|
const contract_1 = require("./contract");
|
|
39
39
|
const credential_store_1 = require("./credential-store");
|
|
40
40
|
const harness_1 = require("./harness");
|
|
41
|
+
const node_floor_1 = require("./node-floor");
|
|
41
42
|
const options_1 = require("./options");
|
|
42
43
|
const version_1 = require("./version");
|
|
43
44
|
const USAGE = `pf-agent-harness — headless Peakflo coding-agent harness
|
|
@@ -75,6 +76,14 @@ Options:
|
|
|
75
76
|
--max-sessions <n> concurrent sessions (env PF_MAX_CONCURRENT_SESSIONS,
|
|
76
77
|
default 10 for desktop/self-hosted; sandbox is configured by control plane)
|
|
77
78
|
--labels <a,b> routing labels (env PF_RUNNER_LABELS)
|
|
79
|
+
--no-harness-discovery do not look for Codex, Claude Code, Cursor,
|
|
80
|
+
Pi and OpenCode on this machine, and report
|
|
81
|
+
none (env PF_HARNESS_DISCOVERY=0). On by
|
|
82
|
+
default for desktop/self-hosted; never runs
|
|
83
|
+
in a sandbox.
|
|
84
|
+
|
|
85
|
+
Needs Node.js 22.19 or newer; start stops with one line on an older one.
|
|
86
|
+
PF_ALLOW_UNSUPPORTED_NODE=1 starts anyway, at your own risk.
|
|
78
87
|
|
|
79
88
|
In a managed sandbox pf-bootstrap supplies these through the environment, so
|
|
80
89
|
the enrolment token never appears in /proc/<pid>/cmdline.
|
|
@@ -139,6 +148,26 @@ async function main() {
|
|
|
139
148
|
process.exitCode = 2;
|
|
140
149
|
return;
|
|
141
150
|
}
|
|
151
|
+
/*
|
|
152
|
+
* THE NODE.JS FLOOR — checked first, before the browser sign-in, so nobody
|
|
153
|
+
* signs in and picks an organisation only to be told to change Node. See
|
|
154
|
+
* `node-floor.ts` for where the floor comes from and why a sandbox only
|
|
155
|
+
* warns.
|
|
156
|
+
*/
|
|
157
|
+
const nodeFloor = (0, node_floor_1.checkNodeFloor)({
|
|
158
|
+
current: process.versions.node,
|
|
159
|
+
floor: (0, version_1.getNodeFloor)(),
|
|
160
|
+
runtime: readOptionValue(argv.slice(1), 'runtime') ??
|
|
161
|
+
process.env.PF_RUNNER_RUNTIME,
|
|
162
|
+
env: process.env
|
|
163
|
+
});
|
|
164
|
+
if (nodeFloor.action === 'refuse') {
|
|
165
|
+
process.stderr.write(`${nodeFloor.message}\n`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
if (nodeFloor.action === 'warn') {
|
|
169
|
+
process.stderr.write(`Warning: ${nodeFloor.message}\n`);
|
|
170
|
+
}
|
|
142
171
|
/*
|
|
143
172
|
* NO TOKEN? OFFER TO SIGN IN, BUT ONLY WHERE THAT CAN POSSIBLY WORK.
|
|
144
173
|
*
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HARNESS DISCOVERY — which coding harnesses are installed on this machine.
|
|
3
|
+
*
|
|
4
|
+
* Modelled on 20x, where the harness code came from:
|
|
5
|
+
* - `src/main/agent-installer/detect.js` finds the binaries (`claude`,
|
|
6
|
+
* `codex`, `cursor-agent`, `opencode`, `pi`) with `--version`, after it
|
|
7
|
+
* puts the well-known install directories on PATH;
|
|
8
|
+
* - `src/renderer/src/types/index.ts` holds the static model lists the 20x
|
|
9
|
+
* agent form shows for Claude Code, Codex and Cursor.
|
|
10
|
+
*
|
|
11
|
+
* WHAT THIS IS NOT. It is not the list of backends this runner can run.
|
|
12
|
+
* `capabilities.backends` stays that list and the control plane routes on it.
|
|
13
|
+
* An entry here says "this CLI is on the machine", and `runnable` says whether
|
|
14
|
+
* the runner can also drive it. Detection never implies turn support.
|
|
15
|
+
*
|
|
16
|
+
* RULES THIS MODULE KEEPS:
|
|
17
|
+
*
|
|
18
|
+
* 1. NEVER THROWS, NEVER REJECTS. One broken CLI must not hide the others, so
|
|
19
|
+
* every harness is probed on its own, inside its own try/catch and its own
|
|
20
|
+
* deadline.
|
|
21
|
+
* 2. TIME-LIMITED. Each command has a timeout and each harness has a total
|
|
22
|
+
* deadline. A CLI that hangs is killed and reported as timed out.
|
|
23
|
+
* 3. NO SECRETS LEAVE THIS FILE. Sign-in is decided from an exit code, one
|
|
24
|
+
* boolean of a status reply, or the existence of an environment variable or
|
|
25
|
+
* a file. No status text is kept. Every `reason` is a constant from
|
|
26
|
+
* `REASONS` below, never something a CLI printed. Model ids and names are
|
|
27
|
+
* the only CLI text that is reported, and each one is checked first.
|
|
28
|
+
* 4. NO GLOBAL SIDE EFFECTS. `process.env.PATH` is not changed; the search path
|
|
29
|
+
* is local and is handed to each child process.
|
|
30
|
+
*/
|
|
31
|
+
import type { RunnerHarness, RunnerHarnessModel } from './types';
|
|
32
|
+
/**
|
|
33
|
+
* One command: `--version`, a status check, or a model listing.
|
|
34
|
+
*
|
|
35
|
+
* MEASURED, NOT GUESSED. On the Mac where the first release candidate was
|
|
36
|
+
* tried, with two runners and other work going on, `cursor-agent --version`
|
|
37
|
+
* took 9.4 s cold and 3.7 s warm ALONE, and 6-10 s when five harnesses were
|
|
38
|
+
* probed at once; `opencode --version` took 3-5 s alone and 7-9 s beside the
|
|
39
|
+
* others. The first limit was 8 s, so both lost on a busy machine and were
|
|
40
|
+
* shown as broken. The limit is now well above the slowest start seen — and
|
|
41
|
+
* the pass no longer starts every CLI at once (`DISCOVERY_CONCURRENCY`),
|
|
42
|
+
* which is what made each of them slow.
|
|
43
|
+
*/
|
|
44
|
+
export declare const DISCOVERY_COMMAND_TIMEOUT_MS = 20000;
|
|
45
|
+
/**
|
|
46
|
+
* The budget for finding out a harness's STATE: its version and whether it is
|
|
47
|
+
* signed in. The model listing is NOT inside this budget — it has one command
|
|
48
|
+
* timeout of its own and can never change the state (see `probeHarness`).
|
|
49
|
+
*/
|
|
50
|
+
export declare const DISCOVERY_HARNESS_TIMEOUT_MS = 45000;
|
|
51
|
+
/**
|
|
52
|
+
* How many harnesses are probed at the same time. Each harness runs its
|
|
53
|
+
* commands one after another, so this is also the most CLI processes
|
|
54
|
+
* discovery ever has running. Two, not five: these CLIs are heavy to start
|
|
55
|
+
* (most are bundled Node programs), and five cold starts at once roughly
|
|
56
|
+
* doubled the start time of each on the test machine.
|
|
57
|
+
*/
|
|
58
|
+
export declare const DISCOVERY_CONCURRENCY = 2;
|
|
59
|
+
/**
|
|
60
|
+
* Every sentence a `reason` can be. A closed set on purpose: text printed by a
|
|
61
|
+
* CLI can hold an account email or a key, and a constant cannot.
|
|
62
|
+
*/
|
|
63
|
+
export declare const REASONS: {
|
|
64
|
+
readonly notInstalled: "Not installed on this machine.";
|
|
65
|
+
readonly versionTimedOut: "Installed. The version check timed out and will be tried again.";
|
|
66
|
+
readonly versionFailed: "The version check failed.";
|
|
67
|
+
readonly discoveryTimedOut: "Installed. The check timed out and will be tried again.";
|
|
68
|
+
readonly discoveryFailed: "Discovery failed.";
|
|
69
|
+
readonly signInTimedOut: "Installed. The sign-in check timed out and will be tried again.";
|
|
70
|
+
readonly signInUnknown: "Sign-in could not be confirmed.";
|
|
71
|
+
readonly codexSignIn: "Not signed in. Run `codex login` on this machine.";
|
|
72
|
+
readonly claudeSignIn: "Not signed in. Run `claude auth login` on this machine.";
|
|
73
|
+
readonly cursorSignIn: "Not signed in. Run `cursor-agent login` on this machine.";
|
|
74
|
+
};
|
|
75
|
+
/** 20x `CLAUDE_MODELS`. Claude Code has no command that lists models. */
|
|
76
|
+
export declare const CLAUDE_STATIC_MODELS: ReadonlyArray<RunnerHarnessModel>;
|
|
77
|
+
/** 20x `CODEX_MODELS`. Used only when Codex's own model cache cannot be read. */
|
|
78
|
+
export declare const CODEX_STATIC_MODELS: ReadonlyArray<RunnerHarnessModel>;
|
|
79
|
+
/** 20x `CURSOR_MODELS`. Used only when `cursor-agent models` gives nothing. */
|
|
80
|
+
export declare const CURSOR_STATIC_MODELS: ReadonlyArray<RunnerHarnessModel>;
|
|
81
|
+
export interface DiscoveryExecResult {
|
|
82
|
+
/** Exit code, or null when the process did not exit by itself. */
|
|
83
|
+
code: number | null;
|
|
84
|
+
stdout: string;
|
|
85
|
+
stderr: string;
|
|
86
|
+
timedOut: boolean;
|
|
87
|
+
}
|
|
88
|
+
/** Everything that touches the machine. Replaced in tests. */
|
|
89
|
+
export interface DiscoveryDeps {
|
|
90
|
+
/** Run one command. MUST NOT reject. */
|
|
91
|
+
exec(command: string, args: string[], timeoutMs: number): Promise<DiscoveryExecResult>;
|
|
92
|
+
/** Absolute path of a command, or null when it is not installed. */
|
|
93
|
+
resolveCommand(command: string): string | null;
|
|
94
|
+
realpath(path: string): string;
|
|
95
|
+
/** File text, or null when it cannot be read. */
|
|
96
|
+
readFile(path: string): string | null;
|
|
97
|
+
exists(path: string): boolean;
|
|
98
|
+
env: NodeJS.ProcessEnv;
|
|
99
|
+
home: string;
|
|
100
|
+
now(): Date;
|
|
101
|
+
}
|
|
102
|
+
export interface DiscoverHarnessesOptions {
|
|
103
|
+
/** Backend names this runner can run turns with (`capabilities.backends`). */
|
|
104
|
+
runnableBackends?: readonly string[];
|
|
105
|
+
commandTimeoutMs?: number;
|
|
106
|
+
harnessTimeoutMs?: number;
|
|
107
|
+
/** Harnesses probed at once. Defaults to `DISCOVERY_CONCURRENCY`. */
|
|
108
|
+
concurrency?: number;
|
|
109
|
+
/**
|
|
110
|
+
* Probe ONLY these harnesses and return only their entries. Used to check
|
|
111
|
+
* again the ones whose last check timed out, without starting the others.
|
|
112
|
+
*/
|
|
113
|
+
only?: readonly string[];
|
|
114
|
+
deps?: Partial<DiscoveryDeps>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Where to look for a harness binary: PATH first, then the directories the
|
|
118
|
+
* harness installers use. The well-known directories come from 20x
|
|
119
|
+
* `detect.js` (`ensureAgentPaths` and `getPiCommandCandidates`). The directory
|
|
120
|
+
* of the running `node` is added because a runner started by launchd or
|
|
121
|
+
* systemd has a short PATH, and an npm-installed CLI sits beside that `node`.
|
|
122
|
+
*/
|
|
123
|
+
export declare function discoverySearchPath(env: NodeJS.ProcessEnv, home: string, platform?: NodeJS.Platform): string[];
|
|
124
|
+
/**
|
|
125
|
+
* The environment a harness CLI is started with.
|
|
126
|
+
*
|
|
127
|
+
* EVERY `PF_*` VARIABLE IS REMOVED. Those are this runner's own settings and
|
|
128
|
+
* include `PF_ENROLMENT_TOKEN`; a third-party CLI has no use for them and
|
|
129
|
+
* must not be handed them. The rest is kept, because each CLI finds its own
|
|
130
|
+
* login through it (`HOME`, `CODEX_HOME`, `ANTHROPIC_API_KEY`, ...).
|
|
131
|
+
*/
|
|
132
|
+
export declare function discoveryChildEnv(env: NodeJS.ProcessEnv, searchPath: readonly string[]): NodeJS.ProcessEnv;
|
|
133
|
+
/**
|
|
134
|
+
* Keep only entries that look like a model. Anything that looks like a secret
|
|
135
|
+
* is dropped whole: a model list has no reason to hold one, so the safe
|
|
136
|
+
* reading of such a line is that it is not a model.
|
|
137
|
+
*/
|
|
138
|
+
export declare function sanitizeHarnessModels(models: ReadonlyArray<{
|
|
139
|
+
id: unknown;
|
|
140
|
+
name?: unknown;
|
|
141
|
+
default?: unknown;
|
|
142
|
+
}>): RunnerHarnessModel[];
|
|
143
|
+
/** The first version-like token, as 20x `detect.js` reads it. */
|
|
144
|
+
export declare function parseVersion(output: string): string | null;
|
|
145
|
+
export declare function saysSignedOut(text: string): boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Codex keeps the models its account can use in `models_cache.json`. This is a
|
|
148
|
+
* model cache, not a credential file; only `slug`, `display_name`,
|
|
149
|
+
* `visibility` and `priority` are read from it.
|
|
150
|
+
*/
|
|
151
|
+
export declare function parseCodexModelCache(text: string | null): RunnerHarnessModel[];
|
|
152
|
+
/** The top-level `model = "..."` of `config.toml`. Nothing else is read. */
|
|
153
|
+
export declare function parseCodexDefaultModel(text: string | null): string | null;
|
|
154
|
+
/**
|
|
155
|
+
* `cursor-agent models` prints one `id - Name` line per model and marks the
|
|
156
|
+
* default with `(current, default)` or `(default)`.
|
|
157
|
+
*/
|
|
158
|
+
export declare function parseCursorModels(output: string): RunnerHarnessModel[];
|
|
159
|
+
/** `opencode models` prints one `provider/model` id per line. */
|
|
160
|
+
export declare function parseOpencodeModels(output: string): RunnerHarnessModel[];
|
|
161
|
+
/** Every harness this build knows how to look for, in display order. */
|
|
162
|
+
export declare const KNOWN_HARNESSES: ReadonlyArray<{
|
|
163
|
+
name: string;
|
|
164
|
+
label: string;
|
|
165
|
+
}>;
|
|
166
|
+
/**
|
|
167
|
+
* Look for every known harness. ALWAYS resolves, with one entry per known
|
|
168
|
+
* harness in a fixed order — a harness that is missing is reported as
|
|
169
|
+
* `installed: false`, so the reader can tell "not installed" from "this runner
|
|
170
|
+
* does not report harnesses". With `only`, just those harnesses are probed and
|
|
171
|
+
* returned.
|
|
172
|
+
*/
|
|
173
|
+
export declare function discoverHarnesses(options?: DiscoverHarnessesOptions): Promise<RunnerHarness[]>;
|