@founderos/runner 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Paperclip AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # @founderos/runner
2
+
3
+ Local execution runner for [FounderOS](https://founderos.fly.dev). Polls the FounderOS cloud for queued AI-agent jobs, spawns the `claude` CLI under your existing Claude Pro subscription, streams events back, and reports completion.
4
+
5
+ > **Why this exists.** FounderOS runs as a hosted control plane, but the actual LLM execution happens on _your_ machine — under _your_ authed CLI session and _your_ subscription billing. The cloud never sees your Anthropic API keys; it just enqueues work and reads back the events. See [ADR-011](https://github.com/founderos-ai/founderos/blob/main/docs/adr/011-byo-runner.md) for the full rationale.
6
+
7
+ ## Quickstart
8
+
9
+ No install required — `npx` runs the runner directly:
10
+
11
+ ```bash
12
+ npx @founderos/runner start \
13
+ --token=fos_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
14
+ --server-url=https://founderos.fly.dev
15
+ ```
16
+
17
+ ### Windows (PowerShell)
18
+
19
+ ```powershell
20
+ npx @founderos/runner start --token=fos_xxx --server-url=https://founderos.fly.dev
21
+ ```
22
+
23
+ > **Common typo:** `npm install -g @founderos/` (with a trailing slash, no
24
+ > package name) makes npm look for a local `@founderos\package.json` and
25
+ > fail with `ENOENT`. The package name is `@founderos/runner` — and you
26
+ > don't actually need to install it globally; `npx` is enough.
27
+
28
+ ### Prerequisites
29
+
30
+ - **Node.js ≥ 20** — `node --version` to check.
31
+ - **`claude` CLI** installed and authenticated — `claude --version` should work in your shell. If you use a different LLM CLI, the multi-CLI dispatcher (PHASE-S7, in progress) will support Codex, Gemini, OpenCode, Pi, and Cursor.
32
+
33
+ ### Get a token
34
+
35
+ Issue a runner token from the FounderOS dashboard → Settings → Runner Tokens. The plaintext is shown **once** at issuance — store it in a password manager. Tokens are scoped to a single company; revoke from the dashboard whenever a machine is decommissioned.
36
+
37
+ ## Install (optional)
38
+
39
+ For long-running setups (a dedicated runner machine, a service file), install the binary globally instead of using `npx`:
40
+
41
+ ```bash
42
+ npm install -g @founderos/runner
43
+ founderos-runner start --token=fos_xxx --server-url=https://founderos.fly.dev
44
+ ```
45
+
46
+ ## Configuration reference
47
+
48
+ Every option can be set via flag OR environment variable. Flags override env vars when both are present.
49
+
50
+ | Flag | Env var | Required | Default | Notes |
51
+ |---|---|---|---|---|
52
+ | `--token=<...>` | `FOUNDEROS_RUNNER_TOKEN` | yes | — | Bearer token (`fos_<32 chars>`) |
53
+ | `--server-url=<...>` | `FOUNDEROS_RUNNER_URL` | yes | — | e.g. `https://founderos.fly.dev` |
54
+ | `--claude-bin=<...>` | `FOUNDEROS_CLAUDE_BIN` | no | `claude` | Override if `claude` isn't on `PATH` |
55
+ | `--timeout-sec=<n>` | `FOUNDEROS_RUNNER_TIMEOUT_SEC` | no | `600` | Per-job hard ceiling, 1..3600 |
56
+ | `--log-level=<...>` | `FOUNDEROS_RUNNER_LOG_LEVEL` | no | `info` | `debug` \| `info` \| `warn` \| `error` |
57
+
58
+ ### Env-var form (macOS/Linux shells, scripts, systemd unit files)
59
+
60
+ ```bash
61
+ export FOUNDEROS_RUNNER_URL=https://founderos.fly.dev
62
+ export FOUNDEROS_RUNNER_TOKEN=fos_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
63
+ founderos-runner start
64
+ ```
65
+
66
+ Output:
67
+
68
+ ```
69
+ [info ] founderos-runner v0.1.0 starting {"serverUrl":"https://founderos.fly.dev","claudeBin":"claude"}
70
+ [info ] got job {"jobId":"...","agent":"Sarah"}
71
+ [info ] job completed {"jobId":"...","status":"completed","exitCode":0}
72
+ ```
73
+
74
+ The process loops until you `^C` it; SIGINT/SIGTERM finishes the current job before exiting.
75
+
76
+ ## Run as a service (macOS)
77
+
78
+ ```bash
79
+ # ~/Library/LaunchAgents/dev.founderos.runner.plist
80
+ cat > ~/Library/LaunchAgents/dev.founderos.runner.plist <<EOF
81
+ <?xml version="1.0" encoding="UTF-8"?>
82
+ <plist version="1.0">
83
+ <dict>
84
+ <key>Label</key><string>dev.founderos.runner</string>
85
+ <key>ProgramArguments</key>
86
+ <array>
87
+ <string>/usr/local/bin/founderos-runner</string>
88
+ <string>start</string>
89
+ </array>
90
+ <key>EnvironmentVariables</key>
91
+ <dict>
92
+ <key>FOUNDEROS_RUNNER_URL</key><string>https://founderos.fly.dev</string>
93
+ <key>FOUNDEROS_RUNNER_TOKEN</key><string>fos_...</string>
94
+ </dict>
95
+ <key>RunAtLoad</key><true/>
96
+ <key>KeepAlive</key><true/>
97
+ </dict>
98
+ </plist>
99
+ EOF
100
+ launchctl load ~/Library/LaunchAgents/dev.founderos.runner.plist
101
+ ```
102
+
103
+ ## Run as a service (Windows, NSSM)
104
+
105
+ Windows ships no equivalent to launchd / systemd, but [NSSM](https://nssm.cc) wraps any executable as a service:
106
+
107
+ ```powershell
108
+ # After `npm install -g @founderos/runner`
109
+ nssm install FounderOSRunner "C:\Program Files\nodejs\founderos-runner.cmd"
110
+ nssm set FounderOSRunner AppParameters "start --token=fos_xxx --server-url=https://founderos.fly.dev"
111
+ nssm set FounderOSRunner Start SERVICE_AUTO_START
112
+ nssm start FounderOSRunner
113
+ ```
114
+
115
+ ## Run as a service (Linux, systemd)
116
+
117
+ ```ini
118
+ # /etc/systemd/system/founderos-runner.service
119
+ [Unit]
120
+ Description=FounderOS local runner
121
+ After=network.target
122
+
123
+ [Service]
124
+ Type=simple
125
+ ExecStart=/usr/local/bin/founderos-runner start
126
+ Restart=always
127
+ Environment="FOUNDEROS_RUNNER_URL=https://founderos.fly.dev"
128
+ Environment="FOUNDEROS_RUNNER_TOKEN=fos_..."
129
+ User=YOUR_USER
130
+
131
+ [Install]
132
+ WantedBy=default.target
133
+ ```
134
+
135
+ ## What does the runner do, exactly?
136
+
137
+ 1. **Long-poll** `GET /api/runner/jobs/next` (≤ 30 s) for queued work.
138
+ 2. **Atomic claim** `POST /api/runner/jobs/:id/claim` — the cloud transitions the job from `queued` → `claimed` in a single SQL `UPDATE…WHERE status='queued'`. Multiple runners polling the same token race; the lowest-latency one wins.
139
+ 3. **Spawn** `claude --print --output-format stream-json --verbose` with the prompt on stdin. If the cloud passed `--resume sessionId`, it threads through.
140
+ 4. **Stream events** — every line of stream-json (assistant messages, tool uses, tool results, the final result) is parsed and POSTed in 50 ms / 32-event batches to `POST /api/runner/jobs/:id/events`.
141
+ 5. **Complete** — when claude exits, post the exit code, total cost (parsed from the `result` event), `sessionId` (for `--resume` next time), and CLI version to `POST /api/runner/jobs/:id/complete`.
142
+
143
+ ## Security
144
+
145
+ - Token is hashed at rest server-side (sha256). Constant-time compare on every request.
146
+ - Tokens scope to a single company. A token issued for company A cannot read jobs for company B.
147
+ - Plaintext tokens are never logged. Audit-log entries store an 8-char preview only.
148
+ - Revoke from the dashboard whenever a machine is decommissioned; revoked tokens get a 401 on the next request.
149
+
150
+ See the [runner threat model](https://github.com/founderos-ai/founderos/blob/main/docs/security/runner-threat-model.md).
151
+
152
+ ## Troubleshooting
153
+
154
+ | Symptom | Likely cause | Fix |
155
+ |---|---|---|
156
+ | `npm error code ENOENT` ... `@founderos\package.json` | Trailing slash typo (`npm install -g @founderos/`) | Use `npx @founderos/runner start --token=...` |
157
+ | `config error: FOUNDEROS_RUNNER_TOKEN is required` | No token passed | Add `--token=fos_...` or set `FOUNDEROS_RUNNER_TOKEN` |
158
+ | `config error: ... must match fos_<32 alphanumeric>` | Token format wrong | Re-issue a fresh token from the dashboard |
159
+ | `unknown or malformed flag(s): --token` | Flag without value (`--token` not `--token=fos_...`) | Use `--token=fos_xxx` form |
160
+ | `claude: command not found` (during a job) | `claude` CLI not on `PATH` | Install Claude Code, or pass `--claude-bin=/full/path/to/claude` |
161
+ | 401 Unauthorized on every request | Token revoked or expired | Re-issue from dashboard → Settings → Runner Tokens |
162
+
163
+ For anything else, the runner emits structured `[debug]` logs when `--log-level=debug` is set — open an issue with the relevant log lines.
164
+
165
+ ## Local development
166
+
167
+ ```bash
168
+ pnpm install
169
+ pnpm --filter @founderos/runner test # unit tests, no live spawn
170
+ pnpm --filter @founderos/runner build # tsc → dist/
171
+ ```
172
+
173
+ ## License
174
+
175
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Typed HTTP client for the four runner-side endpoints in
3
+ * docs/api/runner-openapi.yaml. Uses Node's built-in fetch (Node 20+).
4
+ *
5
+ * Idempotency:
6
+ * - /events POST is server-side append-only; the runner is responsible for
7
+ * not re-sending the same eventId on retry. We track sent eventIds per
8
+ * job in memory; only retry on 5xx (network errors).
9
+ * - /complete is server-rejected on double-submit (409). We swallow the 409
10
+ * gracefully so a crashed-and-restarted runner that already completed a
11
+ * job before crashing doesn't loop on it.
12
+ */
13
+ import type { RunnerConfig } from "./config.js";
14
+ export interface JobDescriptor {
15
+ jobId: string;
16
+ agentId: string;
17
+ agentName: string;
18
+ createdAt: string;
19
+ }
20
+ /**
21
+ * Adapter type carried from the cloud through to the runner.
22
+ *
23
+ * S7.0.1 — kept as a plain string union here (NOT `AgentAdapterType` from
24
+ * `@founderos/shared`) so the runner package can stay free of the shared
25
+ * deep dep. The DB CHECK constraint at migration 0105 + the cloud-side
26
+ * Zod schema enforce validity before this field reaches the runner.
27
+ *
28
+ * Default fallback: "claude_local". Pre-S7 rows that wrote "byo_runner"
29
+ * still map to claude via the dispatcher's legacy-fallback path.
30
+ */
31
+ export type RunnerAdapterType = "claude_local" | "codex_local" | "gemini_local" | "opencode_local" | "pi_local" | "cursor_local" | "openclaw_gateway" | "hermes_local" | "byo_runner" | "process" | "http" | (string & {});
32
+ export interface JobPayload {
33
+ jobId: string;
34
+ agentId: string;
35
+ agentName?: string;
36
+ prompt: string;
37
+ sessionId: string | null;
38
+ runtimeConfig: {
39
+ model?: string | null;
40
+ maxTurns?: number | null;
41
+ timeoutSec?: number;
42
+ instructionsFileContent?: string | null;
43
+ [k: string]: unknown;
44
+ };
45
+ promptHash: string;
46
+ /**
47
+ * S7.0.1 — adapter type the runner should dispatch on. Server returns
48
+ * this from the claim API (`server/src/routes/runner.ts:303`). May be
49
+ * absent on responses from a pre-S7.0.1 server build — defaults to
50
+ * "claude_local" at the consumer.
51
+ */
52
+ adapterType?: RunnerAdapterType;
53
+ addDirs?: string[];
54
+ }
55
+ export type RunnerEventKind = "stdout_line" | "stderr_line" | "claude_message" | "claude_tool_use" | "claude_tool_result" | "claude_result";
56
+ export interface RunnerEvent {
57
+ eventId: string;
58
+ kind: RunnerEventKind;
59
+ ts: string;
60
+ payload?: Record<string, unknown> | string;
61
+ }
62
+ export interface CompletionBody {
63
+ status: "completed" | "failed" | "cancelled";
64
+ exitCode: number;
65
+ signal?: string | null;
66
+ elapsedSec?: number;
67
+ costMicros?: number;
68
+ sessionId?: string | null;
69
+ cliVersion?: string;
70
+ errorMessage?: string | null;
71
+ }
72
+ export declare class ApiError extends Error {
73
+ readonly status: number;
74
+ readonly body: string;
75
+ constructor(status: number, body: string, message?: string);
76
+ }
77
+ /** Generate a runner-side event id. Exposed so the spawner can stamp events
78
+ * before they reach the API client. */
79
+ export declare function makeEventId(): string;
80
+ export declare class RunnerApiClient {
81
+ private readonly config;
82
+ private readonly fetchImpl;
83
+ constructor(config: RunnerConfig, fetchImpl?: typeof fetch);
84
+ private url;
85
+ private headers;
86
+ /**
87
+ * Long-poll for the next claimable job. Server holds the connection up to
88
+ * 30s. Returns:
89
+ * - { kind: "job", job } on a hit
90
+ * - { kind: "empty" } on 204 timeout (caller re-polls)
91
+ * - throws ApiError on non-2xx
92
+ */
93
+ getNext(signal?: AbortSignal): Promise<{
94
+ kind: "job";
95
+ job: JobDescriptor;
96
+ } | {
97
+ kind: "empty";
98
+ }>;
99
+ /**
100
+ * Atomic claim. Returns:
101
+ * - { kind: "claimed", payload } on success
102
+ * - { kind: "lost" } on 409 (race lost)
103
+ * - { kind: "gone" } on 404 (cancelled / unknown)
104
+ * - throws ApiError on other non-2xx
105
+ */
106
+ claim(jobId: string): Promise<{
107
+ kind: "claimed";
108
+ payload: JobPayload;
109
+ } | {
110
+ kind: "lost";
111
+ } | {
112
+ kind: "gone";
113
+ }>;
114
+ /**
115
+ * Append a batch of events. Caller batches with a 50ms / 32-event window.
116
+ * Idempotent on eventId; the server doesn't dedup but does reject when the
117
+ * job is in a terminal state (409). We retry on 5xx + network errors with
118
+ * a small backoff; on 409 we surface the terminal state to the caller so
119
+ * it can stop spawning.
120
+ */
121
+ appendEvents(jobId: string, events: RunnerEvent[]): Promise<{
122
+ kind: "ok";
123
+ } | {
124
+ kind: "terminal";
125
+ }>;
126
+ /**
127
+ * Mark the job terminal. 409 (already terminal) is swallowed — the runner
128
+ * may have crashed mid-complete and the cloud already accepted the prior
129
+ * call. We log + move on rather than spinning forever.
130
+ */
131
+ complete(jobId: string, body: CompletionBody): Promise<{
132
+ kind: "ok";
133
+ } | {
134
+ kind: "already_terminal";
135
+ }>;
136
+ }
137
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/api.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Typed HTTP client for the four runner-side endpoints in
3
+ * docs/api/runner-openapi.yaml. Uses Node's built-in fetch (Node 20+).
4
+ *
5
+ * Idempotency:
6
+ * - /events POST is server-side append-only; the runner is responsible for
7
+ * not re-sending the same eventId on retry. We track sent eventIds per
8
+ * job in memory; only retry on 5xx (network errors).
9
+ * - /complete is server-rejected on double-submit (409). We swallow the 409
10
+ * gracefully so a crashed-and-restarted runner that already completed a
11
+ * job before crashing doesn't loop on it.
12
+ */
13
+ import { randomUUID } from "node:crypto";
14
+ import { setTimeout as sleep } from "node:timers/promises";
15
+ export class ApiError extends Error {
16
+ status;
17
+ body;
18
+ constructor(status, body, message) {
19
+ super(message ?? `API ${status}: ${body.slice(0, 200)}`);
20
+ this.status = status;
21
+ this.body = body;
22
+ this.name = "ApiError";
23
+ }
24
+ }
25
+ /** Generate a runner-side event id. Exposed so the spawner can stamp events
26
+ * before they reach the API client. */
27
+ export function makeEventId() {
28
+ return randomUUID();
29
+ }
30
+ export class RunnerApiClient {
31
+ config;
32
+ fetchImpl;
33
+ constructor(config, fetchImpl = fetch) {
34
+ this.config = config;
35
+ this.fetchImpl = fetchImpl;
36
+ }
37
+ url(path) {
38
+ return `${this.config.serverUrl}${path}`;
39
+ }
40
+ headers(extra) {
41
+ return {
42
+ authorization: `Bearer ${this.config.token}`,
43
+ "user-agent": "founderos-runner",
44
+ ...(extra ?? {}),
45
+ };
46
+ }
47
+ /**
48
+ * Long-poll for the next claimable job. Server holds the connection up to
49
+ * 30s. Returns:
50
+ * - { kind: "job", job } on a hit
51
+ * - { kind: "empty" } on 204 timeout (caller re-polls)
52
+ * - throws ApiError on non-2xx
53
+ */
54
+ async getNext(signal) {
55
+ const res = await this.fetchImpl(this.url("/api/runner/jobs/next"), {
56
+ method: "GET",
57
+ headers: this.headers(),
58
+ signal,
59
+ });
60
+ if (res.status === 204)
61
+ return { kind: "empty" };
62
+ if (!res.ok) {
63
+ throw new ApiError(res.status, await res.text().catch(() => ""));
64
+ }
65
+ const job = (await res.json());
66
+ return { kind: "job", job };
67
+ }
68
+ /**
69
+ * Atomic claim. Returns:
70
+ * - { kind: "claimed", payload } on success
71
+ * - { kind: "lost" } on 409 (race lost)
72
+ * - { kind: "gone" } on 404 (cancelled / unknown)
73
+ * - throws ApiError on other non-2xx
74
+ */
75
+ async claim(jobId) {
76
+ const res = await this.fetchImpl(this.url(`/api/runner/jobs/${jobId}/claim`), {
77
+ method: "POST",
78
+ headers: this.headers({ "content-type": "application/json" }),
79
+ });
80
+ if (res.status === 409)
81
+ return { kind: "lost" };
82
+ if (res.status === 404)
83
+ return { kind: "gone" };
84
+ if (!res.ok) {
85
+ throw new ApiError(res.status, await res.text().catch(() => ""));
86
+ }
87
+ const payload = (await res.json());
88
+ return { kind: "claimed", payload };
89
+ }
90
+ /**
91
+ * Append a batch of events. Caller batches with a 50ms / 32-event window.
92
+ * Idempotent on eventId; the server doesn't dedup but does reject when the
93
+ * job is in a terminal state (409). We retry on 5xx + network errors with
94
+ * a small backoff; on 409 we surface the terminal state to the caller so
95
+ * it can stop spawning.
96
+ */
97
+ async appendEvents(jobId, events) {
98
+ if (events.length === 0)
99
+ return { kind: "ok" };
100
+ const body = JSON.stringify({ events });
101
+ const headers = this.headers({ "content-type": "application/json" });
102
+ const maxAttempts = 4;
103
+ let lastErr;
104
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
105
+ try {
106
+ const res = await this.fetchImpl(this.url(`/api/runner/jobs/${jobId}/events`), {
107
+ method: "POST",
108
+ headers,
109
+ body,
110
+ });
111
+ if (res.status === 204)
112
+ return { kind: "ok" };
113
+ if (res.status === 409)
114
+ return { kind: "terminal" };
115
+ if (res.status >= 500) {
116
+ lastErr = new ApiError(res.status, await res.text().catch(() => ""));
117
+ }
118
+ else {
119
+ throw new ApiError(res.status, await res.text().catch(() => ""));
120
+ }
121
+ }
122
+ catch (err) {
123
+ lastErr = err;
124
+ }
125
+ // 100ms, 300ms, 700ms backoff between retries.
126
+ await sleep(100 * (attempt * attempt));
127
+ }
128
+ throw lastErr instanceof Error ? lastErr : new Error("appendEvents: unreachable");
129
+ }
130
+ /**
131
+ * Mark the job terminal. 409 (already terminal) is swallowed — the runner
132
+ * may have crashed mid-complete and the cloud already accepted the prior
133
+ * call. We log + move on rather than spinning forever.
134
+ */
135
+ async complete(jobId, body) {
136
+ const res = await this.fetchImpl(this.url(`/api/runner/jobs/${jobId}/complete`), {
137
+ method: "POST",
138
+ headers: this.headers({ "content-type": "application/json" }),
139
+ body: JSON.stringify(body),
140
+ });
141
+ if (res.status === 204)
142
+ return { kind: "ok" };
143
+ if (res.status === 409)
144
+ return { kind: "already_terminal" };
145
+ throw new ApiError(res.status, await res.text().catch(() => ""));
146
+ }
147
+ }
148
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `founderos-runner` CLI entry. Supports a single command:
4
+ *
5
+ * founderos-runner start [--token=...] [--server-url=...] [--claude-bin=...]
6
+ * [--timeout-sec=N] [--log-level=info]
7
+ * founderos-runner --version
8
+ * founderos-runner --help
9
+ *
10
+ * Flags override the matching FOUNDEROS_* environment variables. Both
11
+ * forms are supported because Windows PowerShell users find env-var
12
+ * setup awkward (`$env:FOUNDEROS_RUNNER_TOKEN="..."` vs `--token=...`),
13
+ * and macOS/Linux users in shell scripts often prefer env vars.
14
+ */
15
+ import { type RunnerConfigOverrides } from "./config.js";
16
+ /**
17
+ * Parse `--key=value` and `--key value` flag forms. Returns parsed
18
+ * overrides + any unknown args (which we treat as errors today).
19
+ *
20
+ * Argv shape: caller has already stripped node + script + the "start"
21
+ * subcommand. Only flag tokens remain.
22
+ */
23
+ export declare function parseFlags(argv: string[]): {
24
+ overrides: RunnerConfigOverrides;
25
+ unknown: string[];
26
+ };
27
+ export declare function main(argv?: string[]): Promise<number>;
28
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAiC,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAwCxF;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAC1C,SAAS,EAAE,qBAAqB,CAAC;IACjC,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAuDA;AAED,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAqClF"}