orcasynth 1.3.0 → 1.4.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.
Files changed (128) hide show
  1. package/README.md +82 -34
  2. package/dist/cli/index.js +40 -2
  3. package/dist/cli/install/index.js +39 -2
  4. package/dist/cli/menu.js +12 -30
  5. package/dist/cli/setup.js +56 -17
  6. package/dist/cli/setupWizard.js +65 -16
  7. package/dist/cli/systemd.js +24 -0
  8. package/dist/cli/update.js +30 -1
  9. package/package.json +1 -1
  10. package/web-dist/.next/BUILD_ID +1 -1
  11. package/web-dist/.next/build-manifest.json +3 -3
  12. package/web-dist/.next/server/app/_global-error.html +1 -1
  13. package/web-dist/.next/server/app/_global-error.rsc +1 -1
  14. package/web-dist/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  15. package/web-dist/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  16. package/web-dist/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  17. package/web-dist/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  18. package/web-dist/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/web-dist/.next/server/app/_not-found.html +1 -1
  20. package/web-dist/.next/server/app/_not-found.rsc +1 -1
  21. package/web-dist/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  22. package/web-dist/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  23. package/web-dist/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  24. package/web-dist/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  25. package/web-dist/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  26. package/web-dist/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  27. package/web-dist/.next/server/app/account.html +1 -1
  28. package/web-dist/.next/server/app/account.rsc +1 -1
  29. package/web-dist/.next/server/app/account.segments/_full.segment.rsc +1 -1
  30. package/web-dist/.next/server/app/account.segments/_head.segment.rsc +1 -1
  31. package/web-dist/.next/server/app/account.segments/_index.segment.rsc +1 -1
  32. package/web-dist/.next/server/app/account.segments/_tree.segment.rsc +1 -1
  33. package/web-dist/.next/server/app/account.segments/account/__PAGE__.segment.rsc +1 -1
  34. package/web-dist/.next/server/app/account.segments/account.segment.rsc +1 -1
  35. package/web-dist/.next/server/app/dash.html +1 -1
  36. package/web-dist/.next/server/app/dash.rsc +1 -1
  37. package/web-dist/.next/server/app/dash.segments/_full.segment.rsc +1 -1
  38. package/web-dist/.next/server/app/dash.segments/_head.segment.rsc +1 -1
  39. package/web-dist/.next/server/app/dash.segments/_index.segment.rsc +1 -1
  40. package/web-dist/.next/server/app/dash.segments/_tree.segment.rsc +1 -1
  41. package/web-dist/.next/server/app/dash.segments/dash/__PAGE__.segment.rsc +1 -1
  42. package/web-dist/.next/server/app/dash.segments/dash.segment.rsc +1 -1
  43. package/web-dist/.next/server/app/escalations.html +1 -1
  44. package/web-dist/.next/server/app/escalations.rsc +1 -1
  45. package/web-dist/.next/server/app/escalations.segments/_full.segment.rsc +1 -1
  46. package/web-dist/.next/server/app/escalations.segments/_head.segment.rsc +1 -1
  47. package/web-dist/.next/server/app/escalations.segments/_index.segment.rsc +1 -1
  48. package/web-dist/.next/server/app/escalations.segments/_tree.segment.rsc +1 -1
  49. package/web-dist/.next/server/app/escalations.segments/escalations/__PAGE__.segment.rsc +1 -1
  50. package/web-dist/.next/server/app/escalations.segments/escalations.segment.rsc +1 -1
  51. package/web-dist/.next/server/app/index.html +1 -1
  52. package/web-dist/.next/server/app/index.rsc +1 -1
  53. package/web-dist/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  54. package/web-dist/.next/server/app/index.segments/_full.segment.rsc +1 -1
  55. package/web-dist/.next/server/app/index.segments/_head.segment.rsc +1 -1
  56. package/web-dist/.next/server/app/index.segments/_index.segment.rsc +1 -1
  57. package/web-dist/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  58. package/web-dist/.next/server/app/kanban.html +1 -1
  59. package/web-dist/.next/server/app/kanban.rsc +1 -1
  60. package/web-dist/.next/server/app/kanban.segments/_full.segment.rsc +1 -1
  61. package/web-dist/.next/server/app/kanban.segments/_head.segment.rsc +1 -1
  62. package/web-dist/.next/server/app/kanban.segments/_index.segment.rsc +1 -1
  63. package/web-dist/.next/server/app/kanban.segments/_tree.segment.rsc +1 -1
  64. package/web-dist/.next/server/app/kanban.segments/kanban/__PAGE__.segment.rsc +1 -1
  65. package/web-dist/.next/server/app/kanban.segments/kanban.segment.rsc +1 -1
  66. package/web-dist/.next/server/app/onboarding.html +1 -1
  67. package/web-dist/.next/server/app/onboarding.rsc +1 -1
  68. package/web-dist/.next/server/app/onboarding.segments/_full.segment.rsc +1 -1
  69. package/web-dist/.next/server/app/onboarding.segments/_head.segment.rsc +1 -1
  70. package/web-dist/.next/server/app/onboarding.segments/_index.segment.rsc +1 -1
  71. package/web-dist/.next/server/app/onboarding.segments/_tree.segment.rsc +1 -1
  72. package/web-dist/.next/server/app/onboarding.segments/onboarding/__PAGE__.segment.rsc +1 -1
  73. package/web-dist/.next/server/app/onboarding.segments/onboarding.segment.rsc +1 -1
  74. package/web-dist/.next/server/app/projects.html +1 -1
  75. package/web-dist/.next/server/app/projects.rsc +1 -1
  76. package/web-dist/.next/server/app/projects.segments/_full.segment.rsc +1 -1
  77. package/web-dist/.next/server/app/projects.segments/_head.segment.rsc +1 -1
  78. package/web-dist/.next/server/app/projects.segments/_index.segment.rsc +1 -1
  79. package/web-dist/.next/server/app/projects.segments/_tree.segment.rsc +1 -1
  80. package/web-dist/.next/server/app/projects.segments/projects/__PAGE__.segment.rsc +1 -1
  81. package/web-dist/.next/server/app/projects.segments/projects.segment.rsc +1 -1
  82. package/web-dist/.next/server/app/sessions.html +1 -1
  83. package/web-dist/.next/server/app/sessions.rsc +1 -1
  84. package/web-dist/.next/server/app/sessions.segments/_full.segment.rsc +1 -1
  85. package/web-dist/.next/server/app/sessions.segments/_head.segment.rsc +1 -1
  86. package/web-dist/.next/server/app/sessions.segments/_index.segment.rsc +1 -1
  87. package/web-dist/.next/server/app/sessions.segments/_tree.segment.rsc +1 -1
  88. package/web-dist/.next/server/app/sessions.segments/sessions/__PAGE__.segment.rsc +1 -1
  89. package/web-dist/.next/server/app/sessions.segments/sessions.segment.rsc +1 -1
  90. package/web-dist/.next/server/app/settings.html +1 -1
  91. package/web-dist/.next/server/app/settings.rsc +1 -1
  92. package/web-dist/.next/server/app/settings.segments/_full.segment.rsc +1 -1
  93. package/web-dist/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  94. package/web-dist/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  95. package/web-dist/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  96. package/web-dist/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
  97. package/web-dist/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  98. package/web-dist/.next/server/app/tasks.html +1 -1
  99. package/web-dist/.next/server/app/tasks.rsc +1 -1
  100. package/web-dist/.next/server/app/tasks.segments/_full.segment.rsc +1 -1
  101. package/web-dist/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
  102. package/web-dist/.next/server/app/tasks.segments/_index.segment.rsc +1 -1
  103. package/web-dist/.next/server/app/tasks.segments/_tree.segment.rsc +1 -1
  104. package/web-dist/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +1 -1
  105. package/web-dist/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
  106. package/web-dist/.next/server/app/timeline.html +1 -1
  107. package/web-dist/.next/server/app/timeline.rsc +1 -1
  108. package/web-dist/.next/server/app/timeline.segments/_full.segment.rsc +1 -1
  109. package/web-dist/.next/server/app/timeline.segments/_head.segment.rsc +1 -1
  110. package/web-dist/.next/server/app/timeline.segments/_index.segment.rsc +1 -1
  111. package/web-dist/.next/server/app/timeline.segments/_tree.segment.rsc +1 -1
  112. package/web-dist/.next/server/app/timeline.segments/timeline/__PAGE__.segment.rsc +1 -1
  113. package/web-dist/.next/server/app/timeline.segments/timeline.segment.rsc +1 -1
  114. package/web-dist/.next/server/app/users.html +1 -1
  115. package/web-dist/.next/server/app/users.rsc +1 -1
  116. package/web-dist/.next/server/app/users.segments/_full.segment.rsc +1 -1
  117. package/web-dist/.next/server/app/users.segments/_head.segment.rsc +1 -1
  118. package/web-dist/.next/server/app/users.segments/_index.segment.rsc +1 -1
  119. package/web-dist/.next/server/app/users.segments/_tree.segment.rsc +1 -1
  120. package/web-dist/.next/server/app/users.segments/users/__PAGE__.segment.rsc +1 -1
  121. package/web-dist/.next/server/app/users.segments/users.segment.rsc +1 -1
  122. package/web-dist/.next/server/chunks/[root-of-the-server]__1wxxtv8._.js +1 -1
  123. package/web-dist/.next/server/middleware-build-manifest.js +3 -3
  124. package/web-dist/.next/server/pages/404.html +1 -1
  125. package/web-dist/.next/server/pages/500.html +1 -1
  126. /package/web-dist/.next/static/{hDw78YoaHr7BxL_Us9pPD → CxOYTELv4rEqlUTUze_od}/_buildManifest.js +0 -0
  127. /package/web-dist/.next/static/{hDw78YoaHr7BxL_Us9pPD → CxOYTELv4rEqlUTUze_od}/_clientMiddlewareManifest.js +0 -0
  128. /package/web-dist/.next/static/{hDw78YoaHr7BxL_Us9pPD → CxOYTELv4rEqlUTUze_od}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -4,13 +4,14 @@
4
4
 
5
5
  **Control autonomous coding agents — without losing control.**
6
6
 
7
- Plan work, launch isolated coding agents, watch every session, and step in
8
- before risky changes reach your codebase.
7
+ Plan the work, launch isolated coding agents, watch every session live, and step in
8
+ before a risky change ever reaches your codebase.
9
9
 
10
10
  `Plan · Dispatch · Observe · Intervene`
11
11
 
12
- Orcasynth is a self-hosted daemon that runs coding agents (Claude Code, OpenCode,
13
- Codex) in isolated `tmux` sessions — with a REST API, a CLI, and a real-time web UI.
12
+ Orcasynth is a self-hosted daemon that orchestrates autonomous coding agents
13
+ (Claude Code, OpenCode, Codex) in isolated `tmux` sessions — with a REST API, a CLI,
14
+ and a real-time Next.js web UI. No SaaS, no lock-in: your machine, your agents, your code.
14
15
 
15
16
  [![CI](https://github.com/dragocz1995/orcasynth/actions/workflows/ci.yml/badge.svg)](https://github.com/dragocz1995/orcasynth/actions/workflows/ci.yml)
16
17
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
@@ -21,32 +22,51 @@ Codex) in isolated `tmux` sessions — with a REST API, a CLI, and a real-time w
21
22
 
22
23
  ---
23
24
 
25
+ ## Why Orcasynth
26
+
27
+ Coding agents are powerful but messy to run at scale: one terminal per agent, no shared
28
+ view of what's happening, and no safety net when an agent decides to `rm -rf` something.
29
+
30
+ Orcasynth puts a control plane in front of them. Hand it a goal and it plans the work,
31
+ spawns the right agent for each step in its own `tmux` session, streams every keystroke to
32
+ your browser, and gates dangerous actions behind a human when you want it to. When you
33
+ trust it more, you turn the autonomy up; when you trust it less, you turn it down.
34
+
24
35
  ## What it does
25
36
 
26
- - **Autopilot planning.** Give the Pilot a goal; an LLM decomposes it into ordered
27
- phases, names an agent per phase, and chains them by dependency.
28
- - **Per-model descriptions & per-phase model selection.** Write a capability
29
- description for each model in Settings; flip on "Autopilot picks the model" and the
30
- planner chooses the best-suited model for each phase from those descriptions
31
- validated against your allow-list, falling back to the default on anything invalid.
32
- - **Agent-agnostic spawning.** Runs Claude Code, OpenCode, or Codex in `tmux`,
33
- configurable per task. Each agent gets the task context and closes its own task when done.
34
- - **Autonomy levels (L0–L3).** The overseer auto-clears safe permission prompts at
35
- higher autonomy and escalates destructive or uncertain ones to a human.
36
- - **Live web UI.** Tasks, a kanban board + calendar, missions with progress, a timeline,
37
- and live `tmux` session previews with one-click agent intervention. EN/CS i18n built in.
38
- - **Self-healing.** A stuck-session detector revives agents that die without closing out,
39
- and live token/cost usage is shown per run.
40
- - **Multi-user RBAC.** Per-project assignments, per-user model allow-lists, profiles & avatars,
41
- and a first-run onboarding that needs no login until the first admin is created.
42
- - **Self-hosted & lightweight.** A single SQLite-backed daemon (Hono) + a Next.js front end.
43
- No external services required beyond your LLM provider.
37
+ - **Autopilot planning.** Give the Pilot a goal and an LLM decomposes it into ordered
38
+ phases, chains them by dependency, and can name an agent per phase. Phases only start
39
+ once the phases they depend on are done.
40
+ - **Per-model descriptions & per-phase model selection.** Write a capability description
41
+ for each model in Settings, flip on "Autopilot picks the model," and the planner chooses
42
+ the best-suited model for each phase from those descriptions validated against your
43
+ allow-list, falling back to the default on anything invalid.
44
+ - **Agent-agnostic spawning.** Runs Claude Code, OpenCode, or Codex in isolated `tmux`
45
+ sessions, configurable per task. Each agent receives the task context and closes its own
46
+ task when it's done.
47
+ - **Autonomy levels (L0–L3).** Choose how much rope each mission gets from
48
+ **L0 · Recommend** (plan only, nothing runs until you approve) through **L1 · Assist**
49
+ and **L2 · Pilot** to **L3 · Auto** (full autonomy). The overseer's decision engine
50
+ auto-clears agent permission prompts when confidence is high and the action is safe, and
51
+ escalates anything destructive or uncertain to a human. Operations like `rm -rf`, dropping
52
+ tables, force-pushes, or touching `.env` always escalate, whatever the level.
53
+ - **Live web UI with one-click intervention.** Tasks, a kanban board with a calendar,
54
+ missions with phase progress, a timeline, and real-time `tmux` session previews you can
55
+ jump into and take over. Full EN/CS internationalization built in.
56
+ - **Self-healing.** A stuck-session detector revives agents that die without closing out
57
+ (and blocks the task after repeated failures instead of crash-looping). A janitor sweeps
58
+ up finished sessions. Live token and cost usage is shown per run.
59
+ - **Multi-user RBAC.** Admin and member roles, per-project assignments, per-user model
60
+ allow-lists, profiles and avatars, and a first-run onboarding that needs no login until
61
+ the first admin is created.
62
+ - **Self-hosted & lightweight.** A single SQLite-backed daemon (Hono + SSE) plus a Next.js
63
+ front end. No external services required beyond your own LLM provider.
44
64
 
45
65
  ## Screenshots
46
66
 
47
67
  <div align="center">
48
68
 
49
- **Dashboard** — live agents, active missions, autopilot spotlight, and recent outcomes at a glance.
69
+ **Dashboard** — live agents, active missions, the autopilot spotlight, and recent outcomes at a glance.
50
70
 
51
71
  ![Dashboard](docs/screenshots/dashboard.png)
52
72
 
@@ -54,7 +74,7 @@ Codex) in isolated `tmux` sessions — with a REST API, a CLI, and a real-time w
54
74
 
55
75
  | | |
56
76
  |---|---|
57
- | **Tasks** — list + detail with live agent output and token usage. ![Tasks](docs/screenshots/tasks.png) | **Kanban** — open / in-progress / blocked / closed, with mission progress. ![Kanban](docs/screenshots/kanban.png) |
77
+ | **Tasks** — list + detail with live agent output and token usage. ![Tasks](docs/screenshots/tasks.png) | **Kanban** — open / in-progress / blocked / closed, with mission progress and a calendar. ![Kanban](docs/screenshots/kanban.png) |
58
78
  | **Missions** — phase graph and task flow for an autopilot run (folded into Tasks). ![Missions](docs/screenshots/missions.png) | **Timeline** — a live activity feed across tasks, missions, and signals. ![Timeline](docs/screenshots/timeline.png) |
59
79
  | **Sessions** — real-time `tmux` agent previews with one-click intervention. ![Sessions](docs/screenshots/sessions.png) | **Terminal** — the full agent TUI, including human-in-the-loop approvals. ![Terminal](docs/screenshots/terminal.png) |
60
80
  | **Projects** — a built-in Monaco editor with the project file tree. ![Projects editor](docs/screenshots/projects-editor.png) | **Settings** — model presets & descriptions, providers, autopilot, and defaults. ![Settings](docs/screenshots/settings.png) |
@@ -86,8 +106,8 @@ orca update # update to the latest release from npm
86
106
  ```
87
107
 
88
108
  Requires **Node ≥ 22** and **tmux**. On first run, `orca` walks you through a quick
89
- setup — admin account, LLM provider + API key, default model. Your data (config, the
90
- SQLite database and logs) lives in **`~/.config/orca/`** and survives every update.
109
+ setup — admin account, LLM provider + API key, and a default model. Your data (config,
110
+ the SQLite database, and logs) lives in **`~/.config/orca/`** and survives every update.
91
111
 
92
112
  Then open <http://localhost:4500> and sign in.
93
113
 
@@ -111,12 +131,36 @@ npm start -- -p 4500
111
131
  Open <http://localhost:4500> and sign in. Configure your LLM provider and models in
112
132
  **Settings → Autopilot / Models**, then create a task or engage an autopilot mission.
113
133
 
114
- The CLI auto-starts the daemon if it isn't running:
134
+ The CLI talks to the daemon over the REST API and auto-starts it if it isn't running:
115
135
 
116
136
  ```bash
117
- node dist/cli/index.js ls # list tasks
118
- node dist/cli/index.js close <id>
137
+ node dist/cli/index.js ls # list tasks
138
+ node dist/cli/index.js close <id> # close a task
139
+ ```
140
+
141
+ ## How it works
142
+
119
143
  ```
144
+ goal
145
+
146
+
147
+ ┌───────────┐ phases + deps ┌─────────────┐ spawn ┌──────────────┐
148
+ │ Pilot │ ─────────────────► │ Overseer │ ─────────► │ Agent (tmux) │
149
+ │ (planner) │ │ (scheduler, │ │ Claude Code / │
150
+ └───────────┘ │ decisions) │ ◄───────── │ OpenCode / │
151
+ └─────────────┘ signals │ Codex │
152
+ │ └──────────────┘
153
+ │ escalate
154
+
155
+ human-in-the-loop
156
+ ```
157
+
158
+ The **Pilot** decomposes a goal into a dependency-ordered set of phases. The **Overseer**
159
+ schedules ready phases, spawns the right **Agent** for each one in its own `tmux` session,
160
+ and watches the output. A deriver reads each session and emits signals — `working`,
161
+ `needs_input`, `complete`. When an agent hits a permission prompt, the decision engine
162
+ either clears it automatically (high confidence, non-destructive, within the mission's
163
+ autonomy level) or escalates it to a human.
120
164
 
121
165
  ## Architecture
122
166
 
@@ -125,12 +169,14 @@ is a thin client over the REST API + SSE event stream.
125
169
 
126
170
  | Layer | What lives there |
127
171
  |-------|------------------|
128
- | `src/store` | SQLite stores (tasks, missions, agents, config, users) |
129
- | `src/overseer` | mission engine, scheduler, planner, decision engine, janitor |
172
+ | `src/store` | SQLite stores (tasks, missions, agents, config, users, projects, events) via `better-sqlite3` |
173
+ | `src/overseer` | mission engine, planner, scheduler, decision engine, stuck-detector, janitor |
130
174
  | `src/spawn` · `src/tmux` | agent command building + tmux driver |
131
- | `src/deriver` | derives signals from agent output (working / needs-input / complete) |
132
- | `src/api` | Hono REST server + SSE bus |
133
- | `web/modules` | feature modules (tasks, kanban, missions, sessions, timeline, …) |
175
+ | `src/deriver` | derives signals from agent output (`working` / `needs_input` / `complete`) |
176
+ | `src/integrations` | per-executor token/cost usage extraction |
177
+ | `src/api` | Hono REST server + SSE event bus |
178
+ | `src/cli` · `src/daemon` | the `orca` CLI and the daemon entrypoint |
179
+ | `web/modules` | feature modules (tasks, kanban, sessions, timeline, projects, settings, …) |
134
180
 
135
181
  See [`docs/`](./docs) for the [API](./docs/API.md), [architecture](./docs/ARCHITECTURE.md),
136
182
  [concepts](./docs/CONCEPTS.md), [CLI](./docs/CLI.md), and [development](./docs/DEVELOPMENT.md) guides.
@@ -158,3 +204,5 @@ Star the repo if you find it useful — it helps others discover the project.
158
204
  ## License
159
205
 
160
206
  [MIT](./LICENSE)
207
+ </content>
208
+ </invoke>
package/dist/cli/index.js CHANGED
@@ -7,7 +7,45 @@ import { OrcaClient } from './client.js';
7
7
  import { defaultLifecycleDeps, runLifecycle } from './commands.js';
8
8
  import { menu } from './menu.js';
9
9
  const BASE = process.env.ORCA_URL ?? 'http://localhost:4400';
10
- const USAGE = 'usage: orca [menu] | install | <up|down|status|update> | <ls|ready|sessions|close|plan submit|overseer poll|overseer decide>';
10
+ const USAGE = "usage: orca [command] [options] — run `orca --help` for the full command list";
11
+ /** The full, grouped help shown for `orca --help`. Kept as a function so the version is interpolated. */
12
+ function helpText(version) {
13
+ return `🐋 orca ${version} — control plane for autonomous coding agents
14
+
15
+ USAGE
16
+ orca open the interactive launcher menu (in a terminal)
17
+ orca <command> [options]
18
+
19
+ SETUP
20
+ install provision orca as a service: systemd units, a reverse proxy
21
+ and the first admin (run as root). See \`orca install --help\`.
22
+
23
+ SERVICE
24
+ up start the daemon (:4400) and web UI (:4500) in the background
25
+ down stop the daemon and web UI
26
+ status show which services are running and healthy
27
+ update update to the latest npm release and restart in place
28
+
29
+ TASKS
30
+ ls list all tasks (JSON)
31
+ ready list tasks ready to run (JSON)
32
+ sessions list live agent sessions (JSON)
33
+ close <id> [options] close a task
34
+ --summary "<text>" closing note
35
+ --outcome ok|fail record the outcome
36
+
37
+ AGENT-FACING (invoked by running agents — rarely needed by hand)
38
+ plan submit --phases '<json>' submit an autopilot plan (needs ORCA_PLAN_JOB)
39
+ overseer poll wait for the next decision (needs ORCA_MISSION)
40
+ overseer decide --id <id> … resolve a decision: --approve | --escalate | --choice <optionId>
41
+ [--confidence <0..1>] [--rationale "<text>"]
42
+
43
+ OPTIONS
44
+ -h, --help show this help
45
+ -v, --version print the version
46
+
47
+ Docs & issues: https://github.com/dragocz1995/orcasynth`;
48
+ }
11
49
  /** Commands that talk to the daemon API — only these justify auto-starting it. Everything else
12
50
  * (help, unknown verbs) must NOT spawn a daemon: a stray detached daemon squats the port and starves
13
51
  * the systemd-managed one into a restart loop. */
@@ -160,7 +198,7 @@ async function main() {
160
198
  }
161
199
  // Help / bare non-TTY invocation: print usage and stop. Must NOT fall through to ensureDaemon.
162
200
  if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h' || argv[0] === 'help') {
163
- console.log(USAGE);
201
+ console.log(helpText(version));
164
202
  return;
165
203
  }
166
204
  if (argv[0] === '--version' || argv[0] === '-v') {
@@ -7,7 +7,7 @@ import { ensureServiceUser, userHome } from './serviceUser.js';
7
7
  import { detectAgentClis, installCommand } from './agentClis.js';
8
8
  import { daemonUnit, webUnit } from './systemdUnits.js';
9
9
  import { detectProxy, nginxVhost, apacheVhost, certbotCommand } from './proxy.js';
10
- import { applySetup, buildSetupPlan, isFirstRun } from '../setup.js';
10
+ import { applySetup, buildSetupPlan, defaultExecForCli, isFirstRun } from '../setup.js';
11
11
  import { runSetupWizard } from '../setupWizard.js';
12
12
  import { INSTALL_INFO_PATH, serializeInstallInfo } from '../installInfo.js';
13
13
  const DAEMON_PORT = Number(process.env.ORCA_PORT ?? 4400);
@@ -237,8 +237,12 @@ async function planFromArgs(r, args) {
237
237
  : agentsRaw.split(',').map((s) => s.trim()).filter(Boolean);
238
238
  const adminUser = flag(args, '--admin-user');
239
239
  const adminPass = flag(args, '--admin-pass');
240
+ // `--autopilot-cli <claude|opencode|codex>` runs autopilot through an agent CLI (no API key);
241
+ // otherwise the --llm-* flags configure the hosted-API engine.
242
+ const autopilotCli = flag(args, '--autopilot-cli');
243
+ const pilotExec = autopilotCli ? defaultExecForCli(autopilotCli, flag(args, '--autopilot-model')) : undefined;
240
244
  const admin = adminUser && adminPass
241
- ? { username: adminUser, password: adminPass, apiUrl: flag(args, '--llm-url') ?? 'https://api.openai.com/v1', apiKey: flag(args, '--llm-key') ?? '', model: flag(args, '--llm-model') ?? 'gpt-4o-mini' }
245
+ ? { username: adminUser, password: adminPass, pilotExec, apiUrl: flag(args, '--llm-url') ?? 'https://api.openai.com/v1', apiKey: flag(args, '--llm-key') ?? '', model: flag(args, '--llm-model') ?? 'gpt-4o-mini' }
242
246
  : null;
243
247
  return {
244
248
  installTmux: !args.includes('--no-tmux'),
@@ -378,7 +382,40 @@ function planSummary(plan) {
378
382
  }
379
383
  /** `orca install` — provision a fresh Debian/Ubuntu box. Run as root. Pass `--unattended` (with flags)
380
384
  * for a non-interactive install; otherwise an interactive wizard collects every answer. */
385
+ const INSTALL_HELP = `🐋 orca install — provision a fresh Debian/Ubuntu box as an orca service (run as root)
386
+
387
+ USAGE
388
+ orca install interactive wizard (recommended)
389
+ orca install --unattended [options]
390
+
391
+ OPTIONS
392
+ --unattended run non-interactively from the flags below
393
+ --user <name> service user that runs the agents (default: orca)
394
+ --agents <list> agent CLIs to install: all | none | claude,opencode,codex
395
+ --no-tmux skip installing tmux
396
+
397
+ Deployment (pick one; default is localhost):
398
+ --domain <host> serve on a domain behind a reverse proxy (+ Let's Encrypt HTTPS)
399
+ --ip <addr> | --host <addr> serve directly on the public IP and port (no proxy)
400
+ --localhost bind to localhost only
401
+ --proxy <nginx|apache|none> reverse proxy to configure for --domain
402
+ --email <addr> contact email for Let's Encrypt renewal notices
403
+
404
+ First admin + autopilot:
405
+ --admin-user <name> create the first admin account
406
+ --admin-pass <pass> admin password
407
+ --autopilot-cli <cli> run autopilot through an agent CLI (claude|opencode|codex) — no API key
408
+ --autopilot-model <spec> model for --autopilot-cli opencode (e.g. anthropic/claude-sonnet-4-5)
409
+ --llm-url <url> hosted-API engine: base URL (default: https://api.openai.com/v1)
410
+ --llm-key <key> hosted-API engine: API key
411
+ --llm-model <name> hosted-API engine: model (default: gpt-4o-mini)
412
+
413
+ -h, --help show this help`;
381
414
  export async function install(args = []) {
415
+ if (args.includes('--help') || args.includes('-h')) {
416
+ console.log(INSTALL_HELP);
417
+ return;
418
+ }
382
419
  const r = realRunner();
383
420
  const unattended = args.includes('--unattended');
384
421
  p.intro(`🐋 orca install${unattended ? ' (unattended)' : ''}`);
package/dist/cli/menu.js CHANGED
@@ -1,12 +1,13 @@
1
- import { spawn, execFile } from 'node:child_process';
1
+ import { spawn } from 'node:child_process';
2
2
  import * as p from '@clack/prompts';
3
3
  import { status } from './launcher.js';
4
4
  import { defaultLifecycleDeps, formatStatus, runLifecycle } from './commands.js';
5
5
  import { isFirstRun } from './setup.js';
6
6
  import { runSetupWizard } from './setupWizard.js';
7
7
  import { readInstallInfo } from './installInfo.js';
8
+ import { update } from './update.js';
9
+ import { SERVICES, runCmd, systemctl, servicesActive } from './systemd.js';
8
10
  const BASE = process.env.ORCA_URL ?? 'http://localhost:4400';
9
- const SERVICES = ['orca-daemon', 'orca-web'];
10
11
  /** Open a URL in the user's default browser, cross-platform, fire-and-forget. */
11
12
  function openUrl(url) {
12
13
  const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
@@ -16,26 +17,6 @@ function openUrl(url) {
16
17
  }
17
18
  catch { /* headless box — ignore */ }
18
19
  }
19
- /** Run a command, resolving its stdout/exit code (never rejects). */
20
- function run(cmd, args) {
21
- return new Promise((resolve) => {
22
- execFile(cmd, args, (err, stdout) => {
23
- const code = err && typeof err.code === 'number' ? err.code : err ? 1 : 0;
24
- resolve({ code, stdout: stdout?.toString() ?? '' });
25
- });
26
- });
27
- }
28
- /** systemctl, transparently via sudo when we aren't root (so a non-root operator still works). */
29
- async function systemctl(...args) {
30
- const asRoot = typeof process.getuid === 'function' && process.getuid() === 0;
31
- return asRoot ? run('systemctl', args) : run('sudo', ['systemctl', ...args]);
32
- }
33
- /** Whether both ORCA units report active. */
34
- async function servicesActive() {
35
- const r = await systemctl('is-active', ...SERVICES);
36
- const states = r.stdout.trim().split('\n');
37
- return states.length > 0 && states.every((s) => s.trim() === 'active');
38
- }
39
20
  /** Launcher menu for a systemd-provisioned box (`orca install`): drives the units via systemctl and
40
21
  * shows the real public URL the operator chose — never spawns a second, port-conflicting daemon. */
41
22
  async function systemdMenu(info, version) {
@@ -68,20 +49,21 @@ async function systemdMenu(info, version) {
68
49
  continue;
69
50
  }
70
51
  if (action === 'logs') {
71
- const r = await run('journalctl', ['-u', 'orca-daemon', '-n', '20', '--no-pager']);
52
+ const r = await runCmd('journalctl', ['-u', 'orca-daemon', '-n', '20', '--no-pager']);
72
53
  p.note(r.stdout.trim() || '(no logs — try: journalctl -u orca-daemon)', 'orca-daemon');
73
54
  continue;
74
55
  }
75
56
  if (action === 'update') {
76
57
  const s = p.spinner();
77
- s.start('Updating orcasynth…');
78
- const upd = await run('npm', ['install', '-g', 'orcasynth@latest']);
79
- if (upd.code !== 0) {
80
- s.stop('Update failed see npm output above.');
81
- continue;
58
+ s.start('Checking npm for a newer version…');
59
+ try {
60
+ // Shared updater: self-locating npm --prefix + systemd-aware restart (same path as `orca update`).
61
+ const r = await update(process.env, { current: version });
62
+ s.stop(r.updated ? `Updated ${r.from} → ${r.to} and restarted.` : `Already on the latest version (${r.to}).`);
63
+ }
64
+ catch (e) {
65
+ s.stop(`Update failed: ${e.message}`);
82
66
  }
83
- await systemctl('restart', ...SERVICES);
84
- s.stop('Updated and restarted.');
85
67
  continue;
86
68
  }
87
69
  // start | stop | restart
package/dist/cli/setup.js CHANGED
@@ -1,6 +1,20 @@
1
1
  /** First-run wizard logic, kept pure/injectable so the menu shell stays thin. All persistence goes
2
2
  * through the daemon's own HTTP API (POST /users, POST /auth/login, PUT /config) — the single source
3
3
  * of truth — rather than writing the DB directly, so there is no parallel config path. */
4
+ /** Autopilot CLIs that can drive missions without an API key, in recommended order. Mirrors the agent
5
+ * programs the daemon knows about (src/shared/execs.ts). */
6
+ const AUTOPILOT_CLIS = ['claude', 'opencode', 'codex'];
7
+ /** Default autopilot exec spec for a detected agent CLI — a well-formed `<prefix>:<model>` spec that
8
+ * resolveExecutor routes to the right program (so it passes the daemon's allow-list guard without
9
+ * needing a custom model entry). opencode is provider-agnostic, so its model comes from the caller. */
10
+ export function defaultExecForCli(cli, opencodeModel = 'anthropic/claude-sonnet-4-5') {
11
+ switch (cli) {
12
+ case 'claude': return 'claude:sonnet';
13
+ case 'codex': return 'codex:gpt-5.5';
14
+ case 'opencode': return `opencode:${opencodeModel}`;
15
+ default: return '';
16
+ }
17
+ }
4
18
  /** True when the daemon has no users yet — the open setup window during which the wizard may create
5
19
  * the first admin and save the provider/key. */
6
20
  export async function isFirstRun(fetchFn, base) {
@@ -8,33 +22,58 @@ export async function isFirstRun(fetchFn, base) {
8
22
  const body = await r.json();
9
23
  return body.needsSetup === true;
10
24
  }
11
- /** Pure mapper: wizard answers → the API payloads. A blank apiKey is omitted so we never overwrite an
12
- * existing key with an empty string. */
25
+ /** Pure mapper: wizard answers → the API payloads. With a pilotExec the autopilot runs through an
26
+ * agent CLI (same exec for pilot and overseer) and no API key is sent; otherwise a blank apiKey is
27
+ * omitted so we never overwrite an existing key with an empty string. */
13
28
  export function buildSetupPlan(a) {
14
- const autopilot = { model: a.model, apiUrl: a.apiUrl };
15
- if (a.apiKey)
29
+ const autopilot = a.pilotExec
30
+ ? { pilotExec: a.pilotExec, overseerExec: a.pilotExec }
31
+ : { model: a.model, apiUrl: a.apiUrl };
32
+ if (!a.pilotExec && a.apiKey)
16
33
  autopilot.apiKey = a.apiKey;
17
34
  return { user: { username: a.username, password: a.password }, config: { autopilot } };
18
35
  }
19
- /** Create the admin (open during setup), log in for a bearer token, then save the config. The first
20
- * user created is automatically the admin (userStore.create), so the authenticated PUT /config
21
- * succeeds once users exist. */
22
- export async function applySetup(fetchFn, base, plan) {
23
- const post = (path, body, token) => fetchFn(`${base}${path}`, {
24
- method: path === '/config' ? 'PUT' : 'POST',
25
- headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
26
- body: JSON.stringify(body),
36
+ /** Create the admin (open during setup) and log in for a bearer token. The first user created is
37
+ * automatically the admin (userStore.create), so subsequent authenticated calls succeed. */
38
+ export async function createAdmin(fetchFn, base, user) {
39
+ const post = (path, body) => fetchFn(`${base}${path}`, {
40
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
27
41
  });
28
- const created = await post('/users', plan.user);
42
+ const created = await post('/users', user);
29
43
  if (!created.ok)
30
44
  throw new Error(`setup: creating the admin failed (${created.status})`);
31
- const login = await post('/auth/login', plan.user);
45
+ const login = await post('/auth/login', user);
32
46
  if (!login.ok)
33
47
  throw new Error(`setup: login failed (${login.status})`);
34
48
  const { token } = await login.json();
35
49
  if (!token)
36
50
  throw new Error('setup: login returned no token');
37
- const cfg = await post('/config', plan.config, token);
38
- if (!cfg.ok)
39
- throw new Error(`setup: saving config failed (${cfg.status})`);
51
+ return token;
52
+ }
53
+ /** Persist the config patch with an admin bearer token. */
54
+ export async function saveConfig(fetchFn, base, token, config) {
55
+ const r = await fetchFn(`${base}/config`, {
56
+ method: 'PUT', headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` }, body: JSON.stringify(config),
57
+ });
58
+ if (!r.ok)
59
+ throw new Error(`setup: saving config failed (${r.status})`);
60
+ }
61
+ /** Ask the daemon which autopilot-capable agent CLIs are installed & functional for the SERVICE USER
62
+ * (the daemon detects on its own PATH, which is who actually runs the agents), returned in
63
+ * recommended order. Requires an admin bearer token. Returns [] on any failure — callers fall back
64
+ * to the API-key engine. */
65
+ export async function fetchAvailableClis(fetchFn, base, token) {
66
+ const r = await fetchFn(`${base}/integrations/cli-status`, { headers: { authorization: `Bearer ${token}` } });
67
+ if (!r.ok)
68
+ return [];
69
+ const body = await r.json();
70
+ const functional = new Set((body.tools ?? []).filter((t) => t.functional).map((t) => t.name));
71
+ return AUTOPILOT_CLIS.filter((c) => functional.has(c));
72
+ }
73
+ /** Create the admin, log in for a bearer token, then save the config. Kept for the non-interactive
74
+ * (unattended) install path; the interactive wizard creates the admin earlier so it can probe the
75
+ * daemon for installed CLIs before choosing the autopilot engine. */
76
+ export async function applySetup(fetchFn, base, plan) {
77
+ const token = await createAdmin(fetchFn, base, plan.user);
78
+ await saveConfig(fetchFn, base, token, plan.config);
40
79
  }
@@ -1,20 +1,12 @@
1
1
  import * as p from '@clack/prompts';
2
- import { buildSetupPlan, applySetup } from './setup.js';
2
+ import { createAdmin, saveConfig, fetchAvailableClis, defaultExecForCli } from './setup.js';
3
3
  const PROVIDERS = {
4
4
  OpenAI: 'https://api.openai.com/v1',
5
5
  Anthropic: 'https://api.anthropic.com/v1',
6
6
  };
7
- /** Interactive first-run wizard: collect admin creds + LLM provider/key/model and persist them
8
- * through the daemon API at `base`. Shared by the launcher menu and `orca install`. Returns the
9
- * admin credentials on success (so the caller can run a login smoke test), or null if the operator
10
- * cancelled. Throws only on an API failure (caller reports it). */
11
- export async function runSetupWizard(base) {
12
- const username = await p.text({ message: 'Admin username', initialValue: 'admin' });
13
- if (p.isCancel(username))
14
- return null;
15
- const password = await p.password({ message: 'Admin password', validate: (v) => ((v ?? '').length < 4 ? 'At least 4 characters' : undefined) });
16
- if (p.isCancel(password))
17
- return null;
7
+ const CLI_LABEL = { claude: 'Claude Code', opencode: 'OpenCode', codex: 'Codex' };
8
+ /** Configure the hosted-API (relay) autopilot engine: provider URL + key + default model. */
9
+ async function chooseApiEngine() {
18
10
  const choice = await p.select({
19
11
  message: 'LLM provider',
20
12
  options: [...Object.keys(PROVIDERS).map((k) => ({ value: k, label: k })), { value: 'Custom', label: 'Custom (enter URL)' }],
@@ -34,16 +26,73 @@ export async function runSetupWizard(base) {
34
26
  const model = await p.text({ message: 'Default model', initialValue: 'gpt-4o-mini' });
35
27
  if (p.isCancel(model))
36
28
  return null;
37
- const answers = { username, password, apiUrl, apiKey, model };
29
+ const patch = { model, apiUrl };
30
+ if (apiKey)
31
+ patch.apiKey = apiKey;
32
+ return patch;
33
+ }
34
+ /** Pick the autopilot engine: an installed agent CLI (no API key — recommended) or a hosted API key.
35
+ * `clis` are the agent CLIs the daemon found installed for the service user, in recommended order. */
36
+ async function chooseAutopilot(clis) {
37
+ const options = [
38
+ ...clis.map((c, i) => ({ value: `cli:${c}`, label: `${CLI_LABEL[c] ?? c} CLI`, hint: i === 0 ? 'no API key — recommended' : 'no API key' })),
39
+ { value: 'apikey', label: 'LLM API key', hint: clis.length ? 'use a hosted model via an API key' : 'recommended' },
40
+ { value: 'skip', label: 'Skip for now', hint: 'configure later in the web UI' },
41
+ ];
42
+ const choice = await p.select({ message: 'How should Autopilot plan and oversee missions?', options });
43
+ if (p.isCancel(choice) || choice === 'skip')
44
+ return null;
45
+ if (choice === 'apikey')
46
+ return chooseApiEngine();
47
+ const cli = choice.slice('cli:'.length);
48
+ // opencode is provider-agnostic — ask which model it should use (it must already be authenticated).
49
+ let opencodeModel;
50
+ if (cli === 'opencode') {
51
+ const m = await p.text({ message: 'OpenCode model for autopilot', placeholder: 'provider/model', initialValue: 'anthropic/claude-sonnet-4-5' });
52
+ if (p.isCancel(m))
53
+ return null;
54
+ opencodeModel = m.trim() || undefined;
55
+ }
56
+ const exec = defaultExecForCli(cli, opencodeModel);
57
+ return { pilotExec: exec, overseerExec: exec };
58
+ }
59
+ /** Interactive first-run wizard: create the admin, then let the operator pick the autopilot engine —
60
+ * an installed agent CLI (no API key) or an LLM API key — and persist it through the daemon API at
61
+ * `base`. The admin is created up front so the CLI-detection probe can authenticate (which engines
62
+ * are available is only knowable as the service user). Shared by the launcher menu and `orca
63
+ * install`. Returns the admin credentials on success (so the caller can run a login smoke test), or
64
+ * null if the operator cancelled before any account was created. Throws only on an API failure. */
65
+ export async function runSetupWizard(base) {
66
+ const username = await p.text({ message: 'Admin username', initialValue: 'admin' });
67
+ if (p.isCancel(username))
68
+ return null;
69
+ const password = await p.password({ message: 'Admin password', validate: (v) => ((v ?? '').length < 4 ? 'At least 4 characters' : undefined) });
70
+ if (p.isCancel(password))
71
+ return null;
38
72
  const s = p.spinner();
39
- s.start('Saving…');
73
+ s.start('Creating admin…');
74
+ let token;
40
75
  try {
41
- await applySetup(fetch, base, buildSetupPlan(answers));
76
+ token = await createAdmin(fetch, base, { username, password });
42
77
  s.stop('Admin account created.');
43
- return { username, password };
44
78
  }
45
79
  catch (e) {
46
80
  s.stop(`Setup failed: ${e.message}`);
47
81
  throw e;
48
82
  }
83
+ const clis = await fetchAvailableClis(fetch, base, token);
84
+ const autopilot = await chooseAutopilot(clis);
85
+ if (autopilot) {
86
+ const s2 = p.spinner();
87
+ s2.start('Saving autopilot settings…');
88
+ try {
89
+ await saveConfig(fetch, base, token, { autopilot });
90
+ s2.stop(autopilot.pilotExec ? 'Autopilot will run through your agent CLI — no API key needed.' : 'Autopilot configured.');
91
+ }
92
+ catch (e) {
93
+ // The admin already exists and is usable; autopilot can be configured later in the web UI.
94
+ s2.stop(`Saving autopilot settings failed: ${e.message}`);
95
+ }
96
+ }
97
+ return { username, password };
49
98
  }
@@ -0,0 +1,24 @@
1
+ import { execFile } from 'node:child_process';
2
+ /** The two units `orca install` provisions. Shared so the menu and the updater drive the same names. */
3
+ export const SERVICES = ['orca-daemon', 'orca-web'];
4
+ /** Run a command, resolving its exit code + stdout (never rejects). */
5
+ export function runCmd(cmd, args) {
6
+ return new Promise((resolve) => {
7
+ execFile(cmd, args, (err, stdout) => {
8
+ const code = err && typeof err.code === 'number' ? err.code : err ? 1 : 0;
9
+ resolve({ code, stdout: stdout?.toString() ?? '' });
10
+ });
11
+ });
12
+ }
13
+ /** systemctl, transparently via sudo when we aren't root (so a non-root operator — e.g. the services'
14
+ * own www-data with passwordless sudo — still manages the units). */
15
+ export function systemctl(...args) {
16
+ const asRoot = typeof process.getuid === 'function' && process.getuid() === 0;
17
+ return asRoot ? runCmd('systemctl', args) : runCmd('sudo', ['systemctl', ...args]);
18
+ }
19
+ /** Whether all ORCA units report active. */
20
+ export async function servicesActive() {
21
+ const r = await systemctl('is-active', ...SERVICES);
22
+ const states = r.stdout.trim().split('\n');
23
+ return states.length > 0 && states.every((s) => s.trim() === 'active');
24
+ }