orc-brain 1.0.0
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 +21 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +65 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +757 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +4 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +5 -0
- package/dist/main.js.map +1 -0
- package/package.json +59 -0
- package/ui/assets/index-BVNjuVcM.css +1 -0
- package/ui/assets/index-Cq1SRHUK.js +66 -0
- package/ui/index.html +13 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
/** `orc` CLI — a thin client over the orc-brain HTTP API (§9). */
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, mkdtempSync, } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { checkProviderEnv, liveAuthCheck, runDoctorSync, } from "@orc-brain/core";
|
|
9
|
+
import { api, streamEvents } from "./client.js";
|
|
10
|
+
/** Prints JSON when `--json`, otherwise the provided human renderer. */
|
|
11
|
+
function output(json, data, human) {
|
|
12
|
+
if (json)
|
|
13
|
+
console.log(JSON.stringify(data, null, 2));
|
|
14
|
+
else
|
|
15
|
+
human();
|
|
16
|
+
}
|
|
17
|
+
/** This package's root (one level above `src/` in dev, `dist/` when built). */
|
|
18
|
+
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
/** CLI version, read from package.json so `orc --version` tracks releases. */
|
|
20
|
+
const VERSION = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8")).version;
|
|
21
|
+
/**
|
|
22
|
+
* Locates the built web UI: `<pkg>/ui` in the published npm package (bundled
|
|
23
|
+
* at `prepack`), `packages/ui/dist` when running from the monorepo.
|
|
24
|
+
*/
|
|
25
|
+
function resolveUiDist() {
|
|
26
|
+
return [join(pkgRoot, "ui"), join(pkgRoot, "..", "ui", "dist")].find((dir) => existsSync(join(dir, "index.html")));
|
|
27
|
+
}
|
|
28
|
+
/** Builds the `orc` command tree. */
|
|
29
|
+
export function buildCli() {
|
|
30
|
+
const program = new Command();
|
|
31
|
+
program
|
|
32
|
+
.name("orc")
|
|
33
|
+
.description("Local orchestrator brain for Claude Code sub-agents")
|
|
34
|
+
.version(VERSION);
|
|
35
|
+
// --- orc serve -----------------------------------------------------------
|
|
36
|
+
program
|
|
37
|
+
.command("serve")
|
|
38
|
+
.description("Start the orchestrator daemon + web UI")
|
|
39
|
+
.option("--port <port>", "port to bind", "4173")
|
|
40
|
+
.option("--state-dir <dir>", "state directory (.orc)")
|
|
41
|
+
.action(async (opts) => {
|
|
42
|
+
// Refuse to start on API-key billing (§2 preflight).
|
|
43
|
+
const env = checkProviderEnv();
|
|
44
|
+
if (!env.ok) {
|
|
45
|
+
console.error(`Refusing to start: unset ${env.offenders.join(", ")} — ` +
|
|
46
|
+
`these switch billing off the Max subscription (§2).`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const { createServer } = await import("@orc-brain/server");
|
|
50
|
+
const app = createServer({
|
|
51
|
+
stateDir: opts.stateDir,
|
|
52
|
+
uiDist: resolveUiDist(),
|
|
53
|
+
});
|
|
54
|
+
await app.listen({ port: Number(opts.port), host: "127.0.0.1" });
|
|
55
|
+
console.error(`orc serve listening on http://127.0.0.1:${opts.port}`);
|
|
56
|
+
});
|
|
57
|
+
// --- orc doctor ----------------------------------------------------------
|
|
58
|
+
program
|
|
59
|
+
.command("doctor")
|
|
60
|
+
.description("Verify CLI auth (subscription, no API key), versions, git")
|
|
61
|
+
.option("--live", "also run a live subscription/auth probe")
|
|
62
|
+
.option("--json", "output JSON")
|
|
63
|
+
.action(async (opts) => {
|
|
64
|
+
const checks = runDoctorSync();
|
|
65
|
+
const live = opts.live ? await liveAuthCheck() : undefined;
|
|
66
|
+
const allOk = checks.every((c) => c.ok) && (!live || live.ok);
|
|
67
|
+
output(!!opts.json, { checks, live }, () => {
|
|
68
|
+
for (const c of checks) {
|
|
69
|
+
console.log(`${c.ok ? "✓" : "✗"} ${c.name} — ${c.detail}`);
|
|
70
|
+
}
|
|
71
|
+
if (live) {
|
|
72
|
+
console.log(`${live.ok ? "✓" : "✗"} live auth — ` +
|
|
73
|
+
(live.ok
|
|
74
|
+
? `apiKeySource=${live.apiKeySource ?? "?"} model=${live.model ?? "?"}`
|
|
75
|
+
: (live.error ?? "failed")));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
process.exit(allOk ? 0 : 1);
|
|
79
|
+
});
|
|
80
|
+
// --- orc status ----------------------------------------------------------
|
|
81
|
+
program
|
|
82
|
+
.command("status [run-id]")
|
|
83
|
+
.description("Run state, scopes, budget, active subagents")
|
|
84
|
+
.option("--watch", "stream live updates")
|
|
85
|
+
.option("--json", "output JSON")
|
|
86
|
+
.action(async (runId, opts) => {
|
|
87
|
+
const id = runId ?? (await latestRunId());
|
|
88
|
+
if (!id) {
|
|
89
|
+
console.error("no runs found");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
await printStatus(id, !!opts.json);
|
|
93
|
+
if (opts.watch) {
|
|
94
|
+
await streamEvents(`?run_id=${id}`, () => void printStatus(id, false));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// --- orc tail ------------------------------------------------------------
|
|
98
|
+
program
|
|
99
|
+
.command("tail <task-id>")
|
|
100
|
+
.description("Live transcript of one subagent")
|
|
101
|
+
.option("-f, --follow", "follow the stream")
|
|
102
|
+
.action(async (taskId, opts) => {
|
|
103
|
+
const { task } = await api(`/api/tasks/${taskId}`);
|
|
104
|
+
if (!opts.follow) {
|
|
105
|
+
console.log(`session ${task.session_id ?? "(pending)"}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const runId = await latestRunId();
|
|
109
|
+
await streamEvents(runId ? `?run_id=${runId}` : "", (e) => {
|
|
110
|
+
const d = e.data;
|
|
111
|
+
const p = d?.payload;
|
|
112
|
+
if (!p || p.task_id !== taskId)
|
|
113
|
+
return;
|
|
114
|
+
if (e.event === "tool.call")
|
|
115
|
+
console.log(` ⚙ ${p.tool_name}: ${p.input_summary}`);
|
|
116
|
+
else if (e.event === "text.delta")
|
|
117
|
+
process.stdout.write(String(p.delta));
|
|
118
|
+
else if (e.event === "task.state")
|
|
119
|
+
console.log(`\n[${p.status}]`);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
// --- orc run start|pause|resume|stop ------------------------------------
|
|
123
|
+
const run = program.command("run").description("Run lifecycle");
|
|
124
|
+
run
|
|
125
|
+
.command("start <goal-id>")
|
|
126
|
+
.option("--budget <usd>", "run budget in USD", "25")
|
|
127
|
+
.option("--concurrency <n>", "max concurrent workers")
|
|
128
|
+
.action(async (goalId, opts) => {
|
|
129
|
+
const res = await api("/api/runs", {
|
|
130
|
+
method: "POST",
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
goal_id: goalId,
|
|
133
|
+
budget_usd: Number(opts.budget),
|
|
134
|
+
concurrency_limit: opts.concurrency
|
|
135
|
+
? Number(opts.concurrency)
|
|
136
|
+
: undefined,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
console.log(res.run.id);
|
|
140
|
+
});
|
|
141
|
+
for (const verb of ["pause", "resume", "stop"]) {
|
|
142
|
+
run.command(`${verb} <run-id>`).action(async (runId) => {
|
|
143
|
+
const path = verb === "stop" ? "pause" : verb;
|
|
144
|
+
await api(`/api/runs/${runId}/${path}`, { method: "POST" });
|
|
145
|
+
console.log(`${verb} ${runId} ok`);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// orc run mode <run-id> <supervised|unattended> (autonomous-loop.md §3.5)
|
|
149
|
+
run
|
|
150
|
+
.command("mode <run-id> <mode>")
|
|
151
|
+
.description("Set autonomous-loop approval mode: supervised | unattended")
|
|
152
|
+
.action(async (runId, mode) => {
|
|
153
|
+
const res = await api(`/api/runs/${runId}/mode`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
body: JSON.stringify({ mode }),
|
|
156
|
+
});
|
|
157
|
+
console.log(`run ${runId} mode=${res.mode}`);
|
|
158
|
+
});
|
|
159
|
+
// --- orc panic -----------------------------------------------------------
|
|
160
|
+
program
|
|
161
|
+
.command("panic")
|
|
162
|
+
.description("Kill switch: interrupt everything")
|
|
163
|
+
.action(async () => {
|
|
164
|
+
const res = await api("/api/panic", {
|
|
165
|
+
method: "POST",
|
|
166
|
+
});
|
|
167
|
+
console.log(`panicked; aborted ${res.aborted.length} run(s)`);
|
|
168
|
+
});
|
|
169
|
+
// --- orc purge -----------------------------------------------------------
|
|
170
|
+
program
|
|
171
|
+
.command("purge")
|
|
172
|
+
.description("Wipe the orc database (goals, runs, tasks, reports, events)")
|
|
173
|
+
.option("--keep-projects", "keep the project registry")
|
|
174
|
+
.option("--yes", "confirm — required, purge is irreversible")
|
|
175
|
+
.action(async (opts) => {
|
|
176
|
+
if (!opts.yes) {
|
|
177
|
+
console.error("purge deletes every goal, run, task, report and event" +
|
|
178
|
+
(opts.keepProjects ? " (projects kept)" : ", including projects") +
|
|
179
|
+
". Re-run with --yes to confirm.");
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
const { deleted } = await api("/api/purge", {
|
|
183
|
+
method: "POST",
|
|
184
|
+
body: JSON.stringify({ keep_projects: !!opts.keepProjects }),
|
|
185
|
+
});
|
|
186
|
+
const total = Object.values(deleted).reduce((a, b) => a + b, 0);
|
|
187
|
+
console.log(`purged ${total} row(s):`);
|
|
188
|
+
for (const [table, n] of Object.entries(deleted)) {
|
|
189
|
+
if (n > 0)
|
|
190
|
+
console.log(` ${table}: ${n}`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
// --- orc plan ------------------------------------------------------------
|
|
194
|
+
const plan = program.command("plan").description("Planner (§3)");
|
|
195
|
+
plan
|
|
196
|
+
.command("run <goal-id>", { isDefault: true })
|
|
197
|
+
.description("Run the Planner → proposed scopes + tasks")
|
|
198
|
+
.option("--json", "output JSON")
|
|
199
|
+
.action(async (goalId, opts) => {
|
|
200
|
+
const res = await api(`/api/goals/${goalId}/plan`, { method: "POST" });
|
|
201
|
+
output(!!opts.json, res, () => {
|
|
202
|
+
console.log(`planned ${res.scopes.length} scope(s), ${res.tasks.length} task(s):`);
|
|
203
|
+
for (const s of res.scopes)
|
|
204
|
+
console.log(` ${s.id} ${s.name}`);
|
|
205
|
+
console.log(`approve with: orc approve ${goalId}`);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
plan
|
|
209
|
+
.command("show <goal-id>")
|
|
210
|
+
.description("Render the proposed scopes, boundaries, and tasks")
|
|
211
|
+
.option("--json", "output JSON")
|
|
212
|
+
.action(async (goalId, opts) => {
|
|
213
|
+
const data = await api(`/api/goals/${goalId}`);
|
|
214
|
+
output(!!opts.json, data, () => {
|
|
215
|
+
console.log(`${data.goal.title} [${data.goal.status}]`);
|
|
216
|
+
for (const s of data.scopes) {
|
|
217
|
+
console.log(`\n▸ ${s.name} [${s.status}] env=${s.environment} ` +
|
|
218
|
+
`tier=${s.model_tier} mode=${s.permission_mode}`);
|
|
219
|
+
console.log(` paths: ${s.path_allowlist.join(", ")}`);
|
|
220
|
+
for (const t of data.tasks.filter((t) => t.scope_id === s.id)) {
|
|
221
|
+
console.log(` · ${t.title} (${t.task_type}) [${t.status}]`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
plan
|
|
227
|
+
.command("cancel <goal-id>")
|
|
228
|
+
.description("Cancel the proposed plan: drop proposed scopes, back to draft")
|
|
229
|
+
.action(async (goalId) => {
|
|
230
|
+
await api(`/api/goals/${goalId}/plan`, { method: "DELETE" });
|
|
231
|
+
console.log(`plan cancelled; goal ${goalId} is draft again`);
|
|
232
|
+
});
|
|
233
|
+
plan
|
|
234
|
+
.command("edit <goal-id>")
|
|
235
|
+
.description("Edit the proposed plan JSON in $EDITOR, re-validate & apply")
|
|
236
|
+
.action(async (goalId) => {
|
|
237
|
+
const data = await api(`/api/goals/${goalId}`);
|
|
238
|
+
// Reconstruct an editable Plan (scopes with nested tasks by title).
|
|
239
|
+
const planScopes = data.scopes.map((s) => ({
|
|
240
|
+
name: s.name,
|
|
241
|
+
description: s.description,
|
|
242
|
+
path_allowlist: s.path_allowlist,
|
|
243
|
+
path_denylist: s.path_denylist,
|
|
244
|
+
allowed_tools: s.allowed_tools,
|
|
245
|
+
disallowed_tools: s.disallowed_tools,
|
|
246
|
+
model_tier: s.model_tier,
|
|
247
|
+
environment: s.environment,
|
|
248
|
+
permission_mode: s.permission_mode,
|
|
249
|
+
forbidden_actions: s.forbidden_actions,
|
|
250
|
+
success_criteria: s.success_criteria,
|
|
251
|
+
max_budget_usd: s.max_budget_usd,
|
|
252
|
+
tasks: data.tasks
|
|
253
|
+
.filter((t) => t.scope_id === s.id)
|
|
254
|
+
.map((t) => ({
|
|
255
|
+
title: t.title,
|
|
256
|
+
prompt: t.prompt,
|
|
257
|
+
task_type: t.task_type,
|
|
258
|
+
})),
|
|
259
|
+
}));
|
|
260
|
+
const file = join(mkdtempSync(join(tmpdir(), "orc-plan-")), "plan.json");
|
|
261
|
+
writeFileSync(file, JSON.stringify({ scopes: planScopes }, null, 2));
|
|
262
|
+
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
263
|
+
execFileSync(editor, [file], { stdio: "inherit" });
|
|
264
|
+
const edited = JSON.parse(readFileSync(file, "utf8"));
|
|
265
|
+
try {
|
|
266
|
+
const res = await api(`/api/goals/${goalId}/plan`, { method: "PUT", body: JSON.stringify(edited) });
|
|
267
|
+
console.log(`re-materialized ${res.scopes.length} scope(s)`);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
271
|
+
process.exit(2);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
// --- orc approve ---------------------------------------------------------
|
|
275
|
+
program
|
|
276
|
+
.command("approve <goal-id>")
|
|
277
|
+
.description("Approve all proposed scopes of a goal (or selected --scope)")
|
|
278
|
+
.option("--scope <id...>", "approve only these scope ids")
|
|
279
|
+
.option("--start", "then start an unattended run with the project defaults (spec 002 §R5)")
|
|
280
|
+
.action(async (goalId, opts) => {
|
|
281
|
+
if (opts.scope?.length) {
|
|
282
|
+
for (const id of opts.scope) {
|
|
283
|
+
await api(`/api/scopes/${id}/approve`, { method: "POST" });
|
|
284
|
+
}
|
|
285
|
+
console.log(`approved ${opts.scope.length} scope(s)`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const res = await api(`/api/goals/${goalId}/approve`, {
|
|
289
|
+
method: "POST",
|
|
290
|
+
body: JSON.stringify({ start_run: !!opts.start }),
|
|
291
|
+
});
|
|
292
|
+
console.log(`approved ${res.approved.length} scope(s)`);
|
|
293
|
+
if (res.run)
|
|
294
|
+
console.log(`run ${res.run.id} started (unattended)`);
|
|
295
|
+
});
|
|
296
|
+
// --- orc project add|list|show|rm|gc (spec 002 §R6, §R12) -----------------
|
|
297
|
+
const project = program
|
|
298
|
+
.command("project")
|
|
299
|
+
.description("Registered local repositories orc operates on (spec 002)");
|
|
300
|
+
project
|
|
301
|
+
.command("add [path]")
|
|
302
|
+
.description("Register a local git repository as a project (default: current directory)")
|
|
303
|
+
.option("--name <name>", "display name (default: directory basename)")
|
|
304
|
+
.option("--mode <mode>", "execution mode: worktree | in_repo", "in_repo")
|
|
305
|
+
.option("--budget <usd>", "default run budget in USD")
|
|
306
|
+
.option("--concurrency <n>", "default per-run concurrency")
|
|
307
|
+
.option("--auto-merge", "merge settled scope branches into the base branch automatically")
|
|
308
|
+
.action(async (path, opts) => {
|
|
309
|
+
const repoRoot = resolve(path ?? process.cwd());
|
|
310
|
+
const res = await api("/api/projects", {
|
|
311
|
+
method: "POST",
|
|
312
|
+
body: JSON.stringify({
|
|
313
|
+
repo_root: repoRoot,
|
|
314
|
+
name: opts.name,
|
|
315
|
+
execution_mode: opts.mode,
|
|
316
|
+
auto_merge: !!opts.autoMerge,
|
|
317
|
+
default_budget_usd: opts.budget ? Number(opts.budget) : undefined,
|
|
318
|
+
default_concurrency: opts.concurrency
|
|
319
|
+
? Number(opts.concurrency)
|
|
320
|
+
: undefined,
|
|
321
|
+
}),
|
|
322
|
+
});
|
|
323
|
+
console.log(`${res.project.id} ${res.project.repo_root}`);
|
|
324
|
+
});
|
|
325
|
+
project.command("list").action(async () => {
|
|
326
|
+
const { projects } = await api("/api/projects");
|
|
327
|
+
for (const p of projects) {
|
|
328
|
+
console.log(`${p.id} ${p.execution_mode.padEnd(8)} ${p.name} ${p.repo_root}`);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
project.command("show <id>").action(async (id) => {
|
|
332
|
+
console.log(JSON.stringify(await api(`/api/projects/${id}`), null, 2));
|
|
333
|
+
});
|
|
334
|
+
project.command("rm <id>").action(async (id) => {
|
|
335
|
+
await api(`/api/projects/${id}`, { method: "DELETE" });
|
|
336
|
+
console.log(`removed ${id}`);
|
|
337
|
+
});
|
|
338
|
+
project
|
|
339
|
+
.command("gc <id>")
|
|
340
|
+
.description("Remove orphaned worktrees of a project (branches are kept)")
|
|
341
|
+
.option("--prune-merged", "also delete orc/* branches fully merged into the current checkout")
|
|
342
|
+
.action(async (id, opts) => {
|
|
343
|
+
const res = await api(`/api/projects/${id}/gc`, {
|
|
344
|
+
method: "POST",
|
|
345
|
+
body: JSON.stringify({ prune_merged: !!opts.pruneMerged }),
|
|
346
|
+
});
|
|
347
|
+
console.log(`removed ${res.removed.length} orphaned worktree(s)`);
|
|
348
|
+
for (const p of res.removed)
|
|
349
|
+
console.log(` ${p}`);
|
|
350
|
+
if (opts.pruneMerged) {
|
|
351
|
+
console.log(`pruned ${res.pruned_branches.length} merged branch(es)`);
|
|
352
|
+
for (const b of res.pruned_branches)
|
|
353
|
+
console.log(` ${b}`);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
// --- orc feature (spec 002 §R6): objective in, plan awaits approval -------
|
|
357
|
+
program
|
|
358
|
+
.command("feature <project-id> <objective...>")
|
|
359
|
+
.description("Create a goal under a project and start planning it")
|
|
360
|
+
.action(async (projectId, objectiveWords) => {
|
|
361
|
+
const objective = objectiveWords.join(" ");
|
|
362
|
+
const { goal } = await api(`/api/projects/${projectId}/goals`, { method: "POST", body: JSON.stringify({ objective }) });
|
|
363
|
+
console.log(goal.id);
|
|
364
|
+
console.log(`planning started; review with: orc plan show ${goal.id} ` +
|
|
365
|
+
`then approve with: orc approve ${goal.id} --start`);
|
|
366
|
+
});
|
|
367
|
+
// --- orc plugin (spec 003 §R8): declarations file + secrets ---------------
|
|
368
|
+
// Declarations are edited locally in `<state-dir>/plugins.json` (the server
|
|
369
|
+
// reads it at startup); secrets go through the API so the running server
|
|
370
|
+
// registers redaction/stripping immediately.
|
|
371
|
+
const plugin = program
|
|
372
|
+
.command("plugin")
|
|
373
|
+
.description("Plugins: declared in <state-dir>/plugins.json (spec 003)");
|
|
374
|
+
plugin
|
|
375
|
+
.command("list")
|
|
376
|
+
.description("Loaded plugins and their status (from the running server)")
|
|
377
|
+
.option("--json", "output JSON")
|
|
378
|
+
.action(async (opts) => {
|
|
379
|
+
const { plugins } = await api("/api/plugins");
|
|
380
|
+
output(!!opts.json, { plugins }, () => {
|
|
381
|
+
if (!plugins.length) {
|
|
382
|
+
return console.log("no plugins declared — add one with: orc plugin add linear");
|
|
383
|
+
}
|
|
384
|
+
for (const p of plugins) {
|
|
385
|
+
console.log(`${p.name.padEnd(12)} ${(p.version ?? "-").padEnd(8)} ` +
|
|
386
|
+
`${p.status.padEnd(9)} ${p.capabilities.join(",") || "-"}` +
|
|
387
|
+
(p.error ? ` (${p.error})` : ""));
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
plugin
|
|
392
|
+
.command("add <specifier>")
|
|
393
|
+
.description("Declare a plugin: a builtin alias (linear) or an absolute module path")
|
|
394
|
+
.option("--name <name>", "plugin name (default: the builtin alias)")
|
|
395
|
+
.option("--disable", "declare it disabled")
|
|
396
|
+
.option("--state-dir <dir>", "state directory (.orc)")
|
|
397
|
+
.action(async (specifier, opts) => {
|
|
398
|
+
const name = opts.name ?? specifier;
|
|
399
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
400
|
+
console.error(`plugin name must be kebab-case — pass one with --name (got "${name}")`);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
const file = pluginsFilePath(opts.stateDir);
|
|
404
|
+
const decls = readPluginsFile(file);
|
|
405
|
+
if (decls.plugins.some((p) => p.name === name)) {
|
|
406
|
+
console.error(`plugin "${name}" already declared in ${file}`);
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
decls.plugins.push({ name, specifier, enabled: !opts.disable });
|
|
410
|
+
writePluginsFile(file, decls);
|
|
411
|
+
console.log(`declared "${name}" in ${file}`);
|
|
412
|
+
console.log("restart `orc serve` to load it");
|
|
413
|
+
});
|
|
414
|
+
plugin
|
|
415
|
+
.command("rm <name>")
|
|
416
|
+
.option("--state-dir <dir>", "state directory (.orc)")
|
|
417
|
+
.action(async (name, opts) => {
|
|
418
|
+
const file = pluginsFilePath(opts.stateDir);
|
|
419
|
+
const decls = readPluginsFile(file);
|
|
420
|
+
const before = decls.plugins.length;
|
|
421
|
+
decls.plugins = decls.plugins.filter((p) => p.name !== name);
|
|
422
|
+
if (decls.plugins.length === before) {
|
|
423
|
+
console.error(`plugin "${name}" not declared in ${file}`);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
writePluginsFile(file, decls);
|
|
427
|
+
console.log(`removed "${name}"; restart \`orc serve\` to apply`);
|
|
428
|
+
});
|
|
429
|
+
for (const verb of ["enable", "disable"]) {
|
|
430
|
+
plugin
|
|
431
|
+
.command(`${verb} <name>`)
|
|
432
|
+
.option("--state-dir <dir>", "state directory (.orc)")
|
|
433
|
+
.action(async (name, opts) => {
|
|
434
|
+
const file = pluginsFilePath(opts.stateDir);
|
|
435
|
+
const decls = readPluginsFile(file);
|
|
436
|
+
const entry = decls.plugins.find((p) => p.name === name);
|
|
437
|
+
if (!entry) {
|
|
438
|
+
console.error(`plugin "${name}" not declared in ${file}`);
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
entry.enabled = verb === "enable";
|
|
442
|
+
writePluginsFile(file, decls);
|
|
443
|
+
console.log(`${verb}d "${name}"; restart \`orc serve\` to apply`);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const secret = plugin
|
|
447
|
+
.command("secret")
|
|
448
|
+
.description("Plugin secrets (stored in <state-dir>/secrets.json, 0600)");
|
|
449
|
+
secret
|
|
450
|
+
.command("set <plugin> <key>")
|
|
451
|
+
.description("Set a secret; the value is read from stdin, never argv")
|
|
452
|
+
.action(async (pluginName, key) => {
|
|
453
|
+
const value = await readSecretFromStdin(`value for ${key}: `);
|
|
454
|
+
if (!value) {
|
|
455
|
+
console.error("empty value; aborted");
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
await api(`/api/plugins/${pluginName}/secrets`, {
|
|
459
|
+
method: "POST",
|
|
460
|
+
body: JSON.stringify({ key, value }),
|
|
461
|
+
});
|
|
462
|
+
console.log(`${key} set for ${pluginName}`);
|
|
463
|
+
});
|
|
464
|
+
secret
|
|
465
|
+
.command("unset <plugin> <key>")
|
|
466
|
+
.action(async (pluginName, key) => {
|
|
467
|
+
await api(`/api/plugins/${pluginName}/secrets/${key}`, {
|
|
468
|
+
method: "DELETE",
|
|
469
|
+
});
|
|
470
|
+
console.log(`${key} unset for ${pluginName}`);
|
|
471
|
+
});
|
|
472
|
+
// --- orc provider (spec 003 §R8): browse & import external tasks ----------
|
|
473
|
+
const provider = program
|
|
474
|
+
.command("provider")
|
|
475
|
+
.description("Task providers exposed by plugins (spec 003)");
|
|
476
|
+
provider.command("list").action(async () => {
|
|
477
|
+
const { providers } = await api("/api/providers");
|
|
478
|
+
if (!providers.length)
|
|
479
|
+
return console.log("no active task providers");
|
|
480
|
+
for (const p of providers) {
|
|
481
|
+
console.log(`${p.name} ${p.capabilities.join(",")}`);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
provider
|
|
485
|
+
.command("tasks <name>")
|
|
486
|
+
.description("List external tasks from a provider")
|
|
487
|
+
.option("--search <q>", "search text")
|
|
488
|
+
.option("--mine", "only tasks assigned to me")
|
|
489
|
+
.option("--state <state>", "filter by workflow state name")
|
|
490
|
+
.option("--team <team>", "filter by team key or name")
|
|
491
|
+
.option("--limit <n>", "max results")
|
|
492
|
+
.option("--json", "output JSON")
|
|
493
|
+
.action(async (name, opts) => {
|
|
494
|
+
const params = new URLSearchParams();
|
|
495
|
+
if (opts.search)
|
|
496
|
+
params.set("search", opts.search);
|
|
497
|
+
if (opts.mine)
|
|
498
|
+
params.set("assigned_to_me", "true");
|
|
499
|
+
if (opts.state)
|
|
500
|
+
params.set("state", opts.state);
|
|
501
|
+
if (opts.team)
|
|
502
|
+
params.set("team", opts.team);
|
|
503
|
+
if (opts.limit)
|
|
504
|
+
params.set("limit", opts.limit);
|
|
505
|
+
const qs = params.toString();
|
|
506
|
+
const { tasks } = await api(`/api/providers/${name}/tasks${qs ? `?${qs}` : ""}`);
|
|
507
|
+
output(!!opts.json, { tasks }, () => {
|
|
508
|
+
if (!tasks.length)
|
|
509
|
+
return console.log("no tasks found");
|
|
510
|
+
for (const t of tasks) {
|
|
511
|
+
console.log(`${t.identifier.padEnd(10)} ${t.state.padEnd(12)} ${t.title} ${t.url}`);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
provider
|
|
516
|
+
.command("import <name> <external-id>")
|
|
517
|
+
.description("Import an external task as a goal and start planning it")
|
|
518
|
+
.requiredOption("--project <project-id>", "target project")
|
|
519
|
+
.action(async (name, externalId, opts) => {
|
|
520
|
+
const { goal } = await api(`/api/providers/${name}/import`, {
|
|
521
|
+
method: "POST",
|
|
522
|
+
body: JSON.stringify({
|
|
523
|
+
external_id: externalId,
|
|
524
|
+
project_id: opts.project,
|
|
525
|
+
}),
|
|
526
|
+
});
|
|
527
|
+
console.log(goal.id);
|
|
528
|
+
console.log(`planning started; review with: orc plan show ${goal.id} ` +
|
|
529
|
+
`then approve with: orc approve ${goal.id} --start`);
|
|
530
|
+
});
|
|
531
|
+
// --- orc goal new|list|show ----------------------------------------------
|
|
532
|
+
const goal = program.command("goal").description("Goal management");
|
|
533
|
+
goal
|
|
534
|
+
.command("new")
|
|
535
|
+
.description("Define a goal from a JSON file or flags")
|
|
536
|
+
.option("-f, --file <path>", "JSON file with the goal definition")
|
|
537
|
+
.option("--title <title>")
|
|
538
|
+
.option("--objective <text>")
|
|
539
|
+
.option("--repo <path>", "repo root", process.cwd())
|
|
540
|
+
.action(async (opts) => {
|
|
541
|
+
const body = opts.file
|
|
542
|
+
? JSON.parse(readFileSync(opts.file, "utf8"))
|
|
543
|
+
: {
|
|
544
|
+
title: opts.title ?? "untitled goal",
|
|
545
|
+
objective: opts.objective ?? "",
|
|
546
|
+
success_criteria: [],
|
|
547
|
+
constraints: [],
|
|
548
|
+
out_of_scope: [],
|
|
549
|
+
repo_root: opts.repo,
|
|
550
|
+
};
|
|
551
|
+
const { goal: created } = await api("/api/goals", { method: "POST", body: JSON.stringify(body) });
|
|
552
|
+
console.log(created.id);
|
|
553
|
+
});
|
|
554
|
+
goal.command("list").action(async () => {
|
|
555
|
+
const { goals } = await api("/api/goals");
|
|
556
|
+
for (const g of goals)
|
|
557
|
+
console.log(`${g.id} ${g.status.padEnd(16)} ${g.title}`);
|
|
558
|
+
});
|
|
559
|
+
goal.command("show <id>").action(async (id) => {
|
|
560
|
+
console.log(JSON.stringify(await api(`/api/goals/${id}`), null, 2));
|
|
561
|
+
});
|
|
562
|
+
// --- orc tasks -----------------------------------------------------------
|
|
563
|
+
program
|
|
564
|
+
.command("tasks [run-id]")
|
|
565
|
+
.description("List tasks, optionally filtered by state")
|
|
566
|
+
.option("--state <state>", "running|blocked|failed|done|…")
|
|
567
|
+
.option("--json", "output JSON")
|
|
568
|
+
.action(async (runId, opts) => {
|
|
569
|
+
const id = runId ?? (await latestRunId());
|
|
570
|
+
if (!id)
|
|
571
|
+
return console.error("no runs found");
|
|
572
|
+
const q = opts.state ? `?state=${opts.state}` : "";
|
|
573
|
+
const { tasks } = await api(`/api/runs/${id}/tasks${q}`);
|
|
574
|
+
output(!!opts.json, { tasks }, () => {
|
|
575
|
+
for (const t of tasks) {
|
|
576
|
+
console.log(`${t.id} ${t.status.padEnd(9)} ${(t.model_used ?? "-").padEnd(7)} ${t.title}`);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
// --- orc blocked ---------------------------------------------------------
|
|
581
|
+
const blocked = program
|
|
582
|
+
.command("blocked")
|
|
583
|
+
.description("Pending escalations awaiting operator resolution (§8.5)");
|
|
584
|
+
blocked
|
|
585
|
+
.command("list [run-id]", { isDefault: true })
|
|
586
|
+
.option("--json", "output JSON")
|
|
587
|
+
.action(async (runId, opts) => {
|
|
588
|
+
const q = runId ? `?run_id=${runId}` : "";
|
|
589
|
+
const { escalations } = await api(`/api/blocked${q}`);
|
|
590
|
+
output(!!opts.json, { escalations }, () => {
|
|
591
|
+
if (!escalations.length)
|
|
592
|
+
return console.log("no blocked tasks");
|
|
593
|
+
for (const e of escalations) {
|
|
594
|
+
console.log(`${e.id} ${e.rule_id} ${e.tool_name}: ${e.input_summary}`);
|
|
595
|
+
}
|
|
596
|
+
console.log("resolve: orc blocked resolve <id> --approve-once|--deny|--skip [--msg ...]");
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
blocked
|
|
600
|
+
.command("resolve <escalation-id>")
|
|
601
|
+
.description("Resolve a blocked escalation")
|
|
602
|
+
.option("--approve-once", "single-use exemption, re-queue")
|
|
603
|
+
.option("--deny", "deny & instruct, re-queue with guidance")
|
|
604
|
+
.option("--skip", "skip the task")
|
|
605
|
+
.option("--msg <text>", "guidance message")
|
|
606
|
+
.action(async (escId, opts) => {
|
|
607
|
+
const action = opts.approveOnce
|
|
608
|
+
? "approve_once"
|
|
609
|
+
: opts.skip
|
|
610
|
+
? "skip_task"
|
|
611
|
+
: "deny_instruct";
|
|
612
|
+
await api(`/api/escalations/${escId}/resolve`, {
|
|
613
|
+
method: "POST",
|
|
614
|
+
body: JSON.stringify({ action, message: opts.msg }),
|
|
615
|
+
});
|
|
616
|
+
console.log(`resolved ${escId} (${action})`);
|
|
617
|
+
});
|
|
618
|
+
// --- orc budget ----------------------------------------------------------
|
|
619
|
+
const budget = program.command("budget").description("Budget (§7)");
|
|
620
|
+
budget
|
|
621
|
+
.command("show <run-id>")
|
|
622
|
+
.option("--json", "output JSON")
|
|
623
|
+
.action(async (runId, opts) => {
|
|
624
|
+
const data = await api(`/api/runs/${runId}/status`);
|
|
625
|
+
output(!!opts.json, data, () => {
|
|
626
|
+
const r = data.run;
|
|
627
|
+
const pct = r.budget_usd > 0
|
|
628
|
+
? Math.min(100, (r.budget_spent_usd / r.budget_usd) * 100)
|
|
629
|
+
: 0;
|
|
630
|
+
console.log(`budget ${pct.toFixed(0)}% used — $${r.budget_spent_usd.toFixed(4)} / $${r.budget_usd} (${r.budget_state})`);
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
budget
|
|
634
|
+
.command("set <run-id>")
|
|
635
|
+
.requiredOption("--usd <usd>", "new budget ceiling in USD")
|
|
636
|
+
.action(async (runId, opts) => {
|
|
637
|
+
await api(`/api/runs/${runId}/budget`, {
|
|
638
|
+
method: "POST",
|
|
639
|
+
body: JSON.stringify({ usd: Number(opts.usd) }),
|
|
640
|
+
});
|
|
641
|
+
console.log(`budget set to $${opts.usd}`);
|
|
642
|
+
});
|
|
643
|
+
// --- orc report ----------------------------------------------------------
|
|
644
|
+
program
|
|
645
|
+
.command("report [run-id]")
|
|
646
|
+
.description("Show the latest report or force generation (§11)")
|
|
647
|
+
.option("--now", "generate a fresh report now")
|
|
648
|
+
.option("--json", "output JSON")
|
|
649
|
+
.action(async (runId, opts) => {
|
|
650
|
+
const id = runId ?? (await latestRunId());
|
|
651
|
+
if (!id)
|
|
652
|
+
return console.error("no runs found");
|
|
653
|
+
if (opts.now) {
|
|
654
|
+
const { report } = await api(`/api/runs/${id}/reports`, { method: "POST" });
|
|
655
|
+
return output(!!opts.json, { report }, () => console.log(report.content_md));
|
|
656
|
+
}
|
|
657
|
+
const { reports } = await api(`/api/runs/${id}/reports`);
|
|
658
|
+
const latest = reports[0];
|
|
659
|
+
output(!!opts.json, { report: latest }, () => console.log(latest ? latest.content_md : "(no reports yet)"));
|
|
660
|
+
});
|
|
661
|
+
// --- orc audit tail ------------------------------------------------------
|
|
662
|
+
const audit = program.command("audit").description("Audit log");
|
|
663
|
+
audit
|
|
664
|
+
.command("tail <run-id>")
|
|
665
|
+
.option("--rule <id>", "filter by rule id")
|
|
666
|
+
.action(async (runId, opts) => {
|
|
667
|
+
const { events } = await api(`/api/audit/${runId}`);
|
|
668
|
+
for (const e of events) {
|
|
669
|
+
if (opts.rule && e.rule_id !== opts.rule)
|
|
670
|
+
continue;
|
|
671
|
+
console.log(JSON.stringify(e));
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
return program;
|
|
675
|
+
}
|
|
676
|
+
/** Resolves `<state-dir>/plugins.json` (default state dir: `<cwd>/.orc`). */
|
|
677
|
+
function pluginsFilePath(stateDir) {
|
|
678
|
+
return join(resolve(stateDir ?? join(process.cwd(), ".orc")), "plugins.json");
|
|
679
|
+
}
|
|
680
|
+
/** Reads the declarations file, tolerating a missing one. */
|
|
681
|
+
function readPluginsFile(path) {
|
|
682
|
+
if (!existsSync(path))
|
|
683
|
+
return { plugins: [] };
|
|
684
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
685
|
+
return { plugins: Array.isArray(parsed.plugins) ? parsed.plugins : [] };
|
|
686
|
+
}
|
|
687
|
+
/** Writes the declarations file, creating the state dir if needed. */
|
|
688
|
+
function writePluginsFile(path, data) {
|
|
689
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
690
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Reads a secret from stdin (spec 003 §R8 — never argv, which leaks to `ps`).
|
|
694
|
+
* Piped input is read whole; interactive input is prompted without echo.
|
|
695
|
+
*/
|
|
696
|
+
async function readSecretFromStdin(promptText) {
|
|
697
|
+
if (!process.stdin.isTTY) {
|
|
698
|
+
const chunks = [];
|
|
699
|
+
for await (const chunk of process.stdin)
|
|
700
|
+
chunks.push(chunk);
|
|
701
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
702
|
+
}
|
|
703
|
+
process.stderr.write(promptText);
|
|
704
|
+
return await new Promise((resolvePromise) => {
|
|
705
|
+
const stdin = process.stdin;
|
|
706
|
+
stdin.setRawMode(true);
|
|
707
|
+
stdin.resume();
|
|
708
|
+
let value = "";
|
|
709
|
+
const onData = (chunk) => {
|
|
710
|
+
for (const ch of chunk.toString("utf8")) {
|
|
711
|
+
if (ch === "\n" || ch === "\r") {
|
|
712
|
+
stdin.setRawMode(false);
|
|
713
|
+
stdin.pause();
|
|
714
|
+
stdin.off("data", onData);
|
|
715
|
+
process.stderr.write("\n");
|
|
716
|
+
resolvePromise(value);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (ch === "\u0003")
|
|
720
|
+
process.exit(130); // ^C
|
|
721
|
+
if (ch === "\u007f" || ch === "\b")
|
|
722
|
+
value = value.slice(0, -1);
|
|
723
|
+
else
|
|
724
|
+
value += ch;
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
stdin.on("data", onData);
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
/** Resolves the most recent run id from the API. */
|
|
731
|
+
async function latestRunId() {
|
|
732
|
+
try {
|
|
733
|
+
const { runs } = await api("/api/runs");
|
|
734
|
+
return runs[0]?.id;
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/** Fetches and renders a run's status. */
|
|
741
|
+
async function printStatus(runId, json) {
|
|
742
|
+
const data = await api(`/api/runs/${runId}/status`);
|
|
743
|
+
output(json, data, () => {
|
|
744
|
+
const { run, spent_usd, in_flight, task_counts } = data;
|
|
745
|
+
const pct = run.budget_usd > 0
|
|
746
|
+
? Math.min(100, (spent_usd / run.budget_usd) * 100)
|
|
747
|
+
: 0;
|
|
748
|
+
console.log(`run ${runId} [${run.state}] budget ${pct.toFixed(0)}% used ` +
|
|
749
|
+
`($${spent_usd.toFixed(4)}/$${run.budget_usd}, ${run.budget_state}) · ${in_flight} in-flight`);
|
|
750
|
+
const counts = Object.entries(task_counts)
|
|
751
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
752
|
+
.join(" ");
|
|
753
|
+
if (counts)
|
|
754
|
+
console.log(`tasks: ${counts}`);
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
//# sourceMappingURL=index.js.map
|