tono 0.1.0 → 0.2.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.
Files changed (72) hide show
  1. package/LICENSE +21 -674
  2. package/README.md +300 -0
  3. package/dist/cli/commands/config.js +86 -0
  4. package/dist/cli/commands/config.js.map +1 -0
  5. package/dist/cli/commands/configure.js +260 -0
  6. package/dist/cli/commands/configure.js.map +1 -0
  7. package/dist/cli/commands/gateway.js +194 -0
  8. package/dist/cli/commands/gateway.js.map +1 -0
  9. package/dist/cli/commands/init.js +89 -0
  10. package/dist/cli/commands/init.js.map +1 -0
  11. package/dist/cli/commands/open.js +23 -0
  12. package/dist/cli/commands/open.js.map +1 -0
  13. package/dist/cli/commands/start.js +116 -0
  14. package/dist/cli/commands/start.js.map +1 -0
  15. package/dist/cli/index.js +78 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/cli/launchd.js +56 -0
  18. package/dist/cli/launchd.js.map +1 -0
  19. package/dist/cli/prompt.js +46 -0
  20. package/dist/cli/prompt.js.map +1 -0
  21. package/dist/server/agents/claude-code.js +80 -0
  22. package/dist/server/agents/claude-code.js.map +1 -0
  23. package/dist/server/agents/codex.js +52 -0
  24. package/dist/server/agents/codex.js.map +1 -0
  25. package/dist/server/agents/opencode.js +50 -0
  26. package/dist/server/agents/opencode.js.map +1 -0
  27. package/dist/server/agents/registry.js +16 -0
  28. package/dist/server/agents/registry.js.map +1 -0
  29. package/dist/server/agents/types.js +40 -0
  30. package/dist/server/agents/types.js.map +1 -0
  31. package/dist/server/app.js +325 -0
  32. package/dist/server/app.js.map +1 -0
  33. package/dist/server/config/load.js +91 -0
  34. package/dist/server/config/load.js.map +1 -0
  35. package/dist/server/config/manager.js +38 -0
  36. package/dist/server/config/manager.js.map +1 -0
  37. package/dist/server/config/schema.json +151 -0
  38. package/dist/server/db/client.js +136 -0
  39. package/dist/server/db/client.js.map +1 -0
  40. package/dist/server/db/queries.js +158 -0
  41. package/dist/server/db/queries.js.map +1 -0
  42. package/dist/server/db/schema.sql +61 -0
  43. package/dist/server/events.js +5 -0
  44. package/dist/server/events.js.map +1 -0
  45. package/dist/server/git/worktrees.js +225 -0
  46. package/dist/server/git/worktrees.js.map +1 -0
  47. package/dist/server/github/gh.js +172 -0
  48. package/dist/server/github/gh.js.map +1 -0
  49. package/dist/server/pty/manager.js +129 -0
  50. package/dist/server/pty/manager.js.map +1 -0
  51. package/dist/server/pty/ring-buffer.js +40 -0
  52. package/dist/server/pty/ring-buffer.js.map +1 -0
  53. package/dist/server/server.js +36 -0
  54. package/dist/server/server.js.map +1 -0
  55. package/dist/server/workers/github-poller.js +185 -0
  56. package/dist/server/workers/github-poller.js.map +1 -0
  57. package/dist/server/workers/pr-watcher.js +111 -0
  58. package/dist/server/workers/pr-watcher.js.map +1 -0
  59. package/dist/server/workers/scheduler.js +209 -0
  60. package/dist/server/workers/scheduler.js.map +1 -0
  61. package/dist/server/ws/pty.js +72 -0
  62. package/dist/server/ws/pty.js.map +1 -0
  63. package/dist/shared/types.js +23 -0
  64. package/dist/shared/types.js.map +1 -0
  65. package/dist/shared/version.js +18 -0
  66. package/dist/shared/version.js.map +1 -0
  67. package/dist/web/assets/index-5VFn-lxF.js +129 -0
  68. package/dist/web/assets/index-CZHd5NaX.css +1 -0
  69. package/dist/web/index.html +19 -0
  70. package/package.json +79 -6
  71. package/scripts/fix-node-pty.mjs +35 -0
  72. package/index.js +0 -2
package/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # tono
2
+
3
+ A self-hosted gateway that turns labeled GitHub issues and PRs into running CLI agents.
4
+
5
+ > *The name is the middle of **au·tono·mous** — fitting for a tool whose whole job is letting agents run unattended.*
6
+
7
+ > **Heads up: this project was entirely vibe-coded by AI.** Every line of code, every commit, every README revision (including this one). Read accordingly — there are no human-reviewed parts. Use at your own risk.
8
+
9
+ ---
10
+
11
+ ## What it does
12
+
13
+ You add a label to a GitHub issue or PR. Tono notices, spins up the matching CLI agent (Claude Code, Codex CLI, or OpenCode) inside a fresh git worktree on your machine, points it at the issue or PR, and lets it open the PR / post the review when it's done. The browser UI streams the live terminal so you can watch the agent work or jump in.
14
+
15
+ It's built to run quietly in the background on a machine of your choice — a Mac Mini under your desk, an old laptop, whatever — and pick up work as it comes.
16
+
17
+ ```
18
+ GitHub issue (label: tono-claude) GitHub PR (label: tono-claude-review)
19
+ │ │
20
+ ▼ ▼
21
+ gh issue list gh pr list
22
+ │ │
23
+ ▼ ▼
24
+ worktree off baseBranch worktree at PR head (detached)
25
+ │ │
26
+ ▼ ▼
27
+ PTY: claude <prompt> PTY: claude <prompt>
28
+ │ │
29
+ ▼ ▼
30
+ gh pr create gh pr review --comment
31
+
32
+
33
+ gh pr view (poll for merge)
34
+ ```
35
+
36
+ The crucial design choice: tono **does not implement an agent**. It orchestrates external CLI agents — Claude Code, Codex CLI, OpenCode — by spawning them in a real PTY so their full TUI output is preserved.
37
+
38
+ ## Two kinds of work, one orchestrator
39
+
40
+ | Kind | Trigger | Worktree | Agent's job | Output |
41
+ |---|---|---|---|---|
42
+ | **implement** | label on issue | branch off `baseBranch` | implement the issue, push, run `gh pr create` | a new PR |
43
+ | **review** | label on PR | detached HEAD at PR's head | read the diff, run `gh pr review --comment` (or `--approve` / `--request-changes`) | a review on the PR |
44
+
45
+ Each agent has its own queue and its own concurrency cap **per kind**, so a slow implement doesn't starve fast reviews and vice versa.
46
+
47
+ ## Trigger labels (convention, not config)
48
+
49
+ Labels are fixed by convention. There are six total — apply whichever you want and tono picks the right agent + kind:
50
+
51
+ | Agent | Implement issues | Review PRs |
52
+ |---|---|---|
53
+ | `claude-code` | `tono-claude` | `tono-claude-review` |
54
+ | `codex` | `tono-codex` | `tono-codex-review` |
55
+ | `opencode` | `tono-opencode` | `tono-opencode-review` |
56
+
57
+ Run `tono config labels` to print the table for your current config.
58
+
59
+ ## Requirements
60
+
61
+ - **macOS.** Linux support is plausible but untested; the launchd integration is macOS-only.
62
+ - **Node 20+**.
63
+ - **[`gh`](https://cli.github.com)** installed and authenticated (`gh auth login`). Tono shells out to `gh` for issue polling, PR polling, and PR-merge tracking.
64
+ - **The agent CLI(s) you want to use, on `$PATH`.** At least one of:
65
+ - **Claude Code** — [install instructions](https://docs.claude.com/en/docs/claude-code/quickstart). Verify `claude --version`.
66
+ - **Codex CLI** — `npm install -g @openai/codex` or follow the [Codex CLI repo](https://github.com/openai/codex). Verify `codex --version`.
67
+ - **OpenCode** — see [opencode.ai](https://opencode.ai). Verify `opencode --version`.
68
+
69
+ ## Install
70
+
71
+ Tono is on npm — install it globally with whichever package manager you use:
72
+
73
+ ```bash
74
+ # npm (recommended — runs install scripts by default, so native modules just work)
75
+ npm install -g tono
76
+
77
+ # yarn
78
+ yarn global add tono
79
+
80
+ # pnpm
81
+ pnpm add -g tono
82
+ pnpm approve-builds -g # one-time: allow better-sqlite3 and node-pty to fetch prebuilds
83
+ ```
84
+
85
+ Verify it landed on your `$PATH`:
86
+
87
+ ```bash
88
+ tono --version # → 0.2.0
89
+ which tono # → /usr/local/bin/tono (or your manager's bin path)
90
+ ```
91
+
92
+ To upgrade later:
93
+
94
+ ```bash
95
+ npm install -g tono@latest
96
+ tono gateway restart # picks up the new binary in the background daemon
97
+ ```
98
+
99
+ To uninstall:
100
+
101
+ ```bash
102
+ tono gateway uninstall # remove the LaunchAgent first
103
+ npm uninstall -g tono
104
+ rm -rf ~/.tono # optional — drops config, db, worktrees
105
+ ```
106
+
107
+ > **About native modules:** tono pulls in `better-sqlite3` and `node-pty`, which need to compile or fetch a prebuilt binary. Both ship prebuilds for macOS arm64/x64; npm and yarn run their install scripts automatically. **pnpm v10 disables install scripts by default** for safety, hence the extra `pnpm approve-builds -g` step.
108
+
109
+ ## Get started
110
+
111
+ ### 1. Configure tono
112
+
113
+ ```bash
114
+ tono configure
115
+ ```
116
+
117
+ The wizard walks you through:
118
+
119
+ - Bind host / port for the dashboard (defaults: `0.0.0.0:7040`)
120
+ - Poll interval
121
+ - Workspaces root (default: `~/.tono/workspaces`)
122
+ - Which agents to enable (claude-code, codex, opencode) — for each:
123
+ - Command name
124
+ - Per-kind concurrency caps (implement / review)
125
+ - Repos to watch — for each:
126
+ - GitHub slug (`owner/repo`)
127
+ - Path to your existing local clone, **or leave blank** to let tono bare-clone via `gh` into `~/.tono/workspaces/.bare/`
128
+ - Base branch
129
+ - Which of your enabled agents are enrolled on this repo
130
+
131
+ Re-run `tono configure` later, edit `~/.tono/config.json` directly, or use the **Config** page in the web UI.
132
+
133
+ ### 2. Run the gateway
134
+
135
+ ```bash
136
+ tono gateway start # installs + loads a macOS LaunchAgent
137
+ tono open # opens the dashboard in your browser
138
+ ```
139
+
140
+ The gateway runs as a background daemon (`com.tono.gateway`), survives reboots, and auto-restarts on crash. Logs go to `~/.tono/logs/daemon.{out,err}.log`.
141
+
142
+ ### 3. Trigger your first agent
143
+
144
+ Two ways:
145
+
146
+ - **From GitHub.** Apply one of the convention labels (`tono-claude`, `tono-codex-review`, etc.) to an issue or PR in a watched repo. Within `pollIntervalSeconds` (default 60s), tono queues a task and dispatches the matching agent.
147
+ - **From the dashboard.** Click **+ Start session**, pick a repo, pick an agent, type the issue number. Tono fetches the issue body via `gh` and queues an implement task immediately. (Manual review-task trigger isn't wired through the UI yet — apply the review label on the PR.)
148
+
149
+ Click into the task to watch the live terminal.
150
+
151
+ ## Reaching the dashboard
152
+
153
+ - **From the gateway machine:** `tono open` (or `http://localhost:7040`).
154
+ - **From another device on your LAN:** `http://<host-ip>:7040`. Find your host IP with `ipconfig getifaddr en0` on macOS.
155
+ - **From anywhere via Tailscale:** install Tailscale on the gateway machine and any device you want to use, then visit `http://<gateway-tailnet-ip>:7040`.
156
+
157
+ There is **no auth in v1.** Either bind to LAN-only or use Tailscale ACLs.
158
+
159
+ ## CLI reference
160
+
161
+ | Command | Purpose |
162
+ |---|---|
163
+ | `tono init` | Write default config + schema + database to `~/.tono/`. |
164
+ | `tono configure` | Interactive setup wizard. Edits or creates the config. |
165
+ | `tono start` | Run the orchestrator in the **foreground** (dev mode). |
166
+ | `tono gateway start` | Install + load the macOS LaunchAgent so the orchestrator runs in the background and at login. |
167
+ | `tono gateway stop` | Unload the LaunchAgent (config kept). |
168
+ | `tono gateway restart` | Reload after upgrading or changing the plist. |
169
+ | `tono gateway status` | Show LaunchAgent state and HTTP health. |
170
+ | `tono gateway uninstall` | Unload and remove the LaunchAgent. |
171
+ | `tono gateway logs [out\|err]` | Tail the gateway log files. |
172
+ | `tono open` | Open the web UI in your default browser. |
173
+ | `tono config validate` | Validate `~/.tono/config.json` against the schema. |
174
+ | `tono config labels` | Print the GitHub labels each configured agent listens for. |
175
+ | `tono config path` | Print the config file path. |
176
+
177
+ ## Configuration
178
+
179
+ Lives at `~/.tono/config.json`, validated against `~/.tono/config.schema.json`. Most fields are reachable from the **Config** page in the web UI; this is the underlying shape:
180
+
181
+ ```jsonc
182
+ {
183
+ "$schema": "./config.schema.json",
184
+ "server": { "host": "0.0.0.0", "port": 7040 },
185
+ "github": { "pollIntervalSeconds": 60 },
186
+ "workspaces": { "root": "~/.tono/workspaces" },
187
+ "repos": [
188
+ {
189
+ "slug": "owner/repo",
190
+ "path": "/Users/me/code/repo", // optional; leave out to bare-clone via gh
191
+ "baseBranch": "main",
192
+ "agents": ["claude-code", "codex"] // optional; omit to enroll every declared agent
193
+ }
194
+ ],
195
+ "agents": {
196
+ "claude-code": {
197
+ "command": "claude",
198
+ "args": ["--dangerously-skip-permissions"],
199
+ "concurrency": { "implement": 2, "review": 4 },
200
+ "promptTemplates": {
201
+ "implement": "...", // {issueNumber} {issueTitle} {issueBody} {repoSlug} {branch} {baseBranch}
202
+ "review": "..." // adds {prUrl} for review tasks
203
+ }
204
+ },
205
+ "codex": { /* same shape; command "codex", args [] */ },
206
+ "opencode": { /* same shape; command "opencode", args ["run", "{prompt}"] */ }
207
+ }
208
+ }
209
+ ```
210
+
211
+ Notes:
212
+ - **Labels are convention.** There is no `triggerLabel` field. Each enrolled agent listens for `tono-<short>` (implement) and `tono-<short>-review` (review), where `<short>` is `claude` for `claude-code` and the agent name otherwise.
213
+ - **The `{prompt}` placeholder in `args`** lets agents that take prompts as flags (like `opencode run "<prompt>"`) coexist with agents that take prompts as final positionals (like `claude` and `codex`). If `{prompt}` is absent from `args`, tono appends the rendered prompt as the last arg.
214
+ - **Per-(agent, kind) concurrency.** A `concurrency: { implement: 2, review: 4 }` block means up to 2 implement tasks and up to 4 review tasks of that agent run simultaneously. Set either to `0` to disable that kind for the agent.
215
+ - **Live config reloads** (repo add/remove, prompt template edits, concurrency changes) take effect on the poller's next tick. Server host/port changes require `tono gateway restart`.
216
+
217
+ ## Web UI
218
+
219
+ - **Dashboard.** Tasks grouped by phase: Running → Awaiting review (PR open) → Queued → Recent. Plus any live shells. Buttons: `+ Start session` (manual trigger) and `+ New terminal` (free shell on the host).
220
+ - **Session view.** Full xterm.js terminal connected to the live PTY over WebSocket. Resume-safe (close the tab, come back later). Buttons: **Mark done** (free the slot without killing the agent), **Kill session**, **Cleanup worktree**, **Resume / Retry** (uses `claude --resume <id>` for Claude Code; codex / opencode start fresh on retry).
221
+ - **Config.** Per-section forms (Server, GitHub, Workspaces, Repos, Agents). Add new agents from the **Add agent** card at the bottom. Each section has its own Save button. A "Show raw JSON" toggle reveals the unstructured editor.
222
+
223
+ ## Task lifecycle
224
+
225
+ ```
226
+ implement task:
227
+ queued ──► running ──► (agent runs gh pr create)
228
+
229
+ ├──► pr_open ──► merged (PR-watcher polls; auto-cleans worktree)
230
+ │ ├─► pr_closed
231
+
232
+ └──► completed (no PR; Mark done or session exited 0)
233
+ failed ← spawn / non-zero exit before any PR
234
+ cleaned ← worktree manually removed
235
+
236
+ review task:
237
+ queued ──► running ──► completed (agent posts review and exits)
238
+ failed ← spawn / non-zero exit
239
+ cleaned ← worktree manually removed (also drops refs/tono/pr-N)
240
+ ```
241
+
242
+ The PR watcher polls every `pollIntervalSeconds` and uses `gh pr list --head <branch>` to discover PRs even when our streaming detection misses the URL. Once a PR is merged, tono runs `git worktree remove` automatically. Review tasks have nothing to merge, so the watcher ignores them.
243
+
244
+ ## Files on disk
245
+
246
+ ```
247
+ ~/.tono/
248
+ ├── config.json # JSON-Schema-validated config
249
+ ├── config.schema.json
250
+ ├── tono.db # SQLite: tasks (with kind), sessions, issues_seen
251
+ ├── logs/
252
+ │ ├── daemon.out.log # gateway stdout
253
+ │ ├── daemon.err.log
254
+ │ ├── task-N.log # raw PTY bytes per task (4MB ring + tee to disk)
255
+ │ └── shell-XXXX.log # free shells from "+ New terminal"
256
+ └── workspaces/
257
+ ├── .bare/<owner>__<repo>.git # only if you didn't supply a `path`
258
+ ├── <owner>__<repo>__issue-N/ # implement worktree
259
+ └── <owner>__<repo>__pr-review-N/ # review worktree (detached HEAD)
260
+ ```
261
+
262
+ ## Stack
263
+
264
+ | Layer | Pick |
265
+ |---|---|
266
+ | HTTP | [Hono](https://hono.dev) on the Node adapter |
267
+ | WebSocket | `ws` |
268
+ | PTY | `node-pty` |
269
+ | DB | `better-sqlite3` |
270
+ | Schema validation | `ajv` |
271
+ | Frontend | React 19 + Vite 6 + Tailwind v4 + xterm.js (WebGL renderer) |
272
+
273
+ A single Node process runs the HTTP server, GitHub poller (issues + PRs), task scheduler, PR-merge watcher, and owns every PTY.
274
+
275
+ ## Roadmap
276
+
277
+ What works today:
278
+
279
+ - Implement flow: GitHub issue → labeled trigger → agent runs in a worktree → PR opened → merge tracked → worktree cleaned.
280
+ - Review flow: GitHub PR → labeled trigger → agent runs against the PR's head in a detached worktree → review posted via `gh pr review`.
281
+ - Three agent types: Claude Code, Codex CLI, OpenCode.
282
+ - Per-(agent, kind) queues and concurrency, so reviews never starve implements (and vice versa).
283
+ - Manual implement triggering from the UI (no need to label first).
284
+ - `claude --resume` for Claude Code retry; codex / opencode retry runs fresh.
285
+ - Live PTY streaming with WebGL-rendered xterm.js, scrollback ring buffer, reconnect-safe.
286
+ - Free-form `+ New terminal` shells (browser-based SSH-lite to the gateway machine).
287
+ - macOS launchd background gateway with `tono gateway` lifecycle commands.
288
+ - Live config edits via the web UI without restarting.
289
+
290
+ What's next:
291
+
292
+ - **Webhook triggers** to drop polling latency from ~60s to sub-second (Tailscale Funnel or smee.io).
293
+ - **Manual review-task trigger** in the UI (today: label the PR).
294
+ - **Distributed workers.** Designed but unbuilt. The gateway becomes a coordinator; workers connect over WebSocket and run agents on their own filesystems. Tailscale required, no app-level auth.
295
+ - **Cost / token tracking.**
296
+ - **Inline review comments** posted by tono itself rather than asking the agent to call `gh api`.
297
+
298
+ ## License
299
+
300
+ [MIT](./LICENSE).
@@ -0,0 +1,86 @@
1
+ import { configPath, loadConfig, ConfigError, resolveRepoAgents } from "../../server/config/load.js";
2
+ import { labelForAgentKind } from "../../shared/types.js";
3
+ export function runConfigPath() {
4
+ console.log(configPath());
5
+ }
6
+ export function runConfigValidate() {
7
+ try {
8
+ const cfg = loadConfig();
9
+ console.log(`config valid: ${configPath()}`);
10
+ console.log(` workspaces root: ${cfg.workspaces.root}`);
11
+ console.log(` poll interval: ${cfg.github.pollIntervalSeconds}s`);
12
+ const declaredAgents = Object.keys(cfg.agents);
13
+ console.log(` agents declared: ${declaredAgents.length === 0 ? "(none)" : declaredAgents.join(", ")}`);
14
+ for (const [name, a] of Object.entries(cfg.agents)) {
15
+ if (!a)
16
+ continue;
17
+ console.log(` - ${name}: command="${a.command}" implement=${a.concurrency.implement} review=${a.concurrency.review}`);
18
+ }
19
+ console.log(` repos watched: ${cfg.repos.length}`);
20
+ for (const r of cfg.repos) {
21
+ const enabled = resolveRepoAgents(cfg, r.agents);
22
+ console.log(` - ${r.slug} (base=${r.baseBranch}, agents=[${enabled.join(", ")}])`);
23
+ }
24
+ }
25
+ catch (err) {
26
+ if (err instanceof ConfigError) {
27
+ console.error(err.message);
28
+ process.exit(1);
29
+ }
30
+ throw err;
31
+ }
32
+ }
33
+ export function runConfigLabels() {
34
+ try {
35
+ const cfg = loadConfig();
36
+ const declared = Object.keys(cfg.agents);
37
+ if (declared.length === 0) {
38
+ console.log("no agents declared in config; nothing to label.");
39
+ return;
40
+ }
41
+ console.log("Labels you can apply on GitHub to trigger tasks:");
42
+ console.log("");
43
+ const rows = [];
44
+ for (const agent of declared) {
45
+ rows.push({ agent, kind: "implement (issue)", label: labelForAgentKind(agent, "implement") });
46
+ rows.push({ agent, kind: "review (PR)", label: labelForAgentKind(agent, "review") });
47
+ }
48
+ const w = (s, n) => s.padEnd(n);
49
+ console.log(` ${w("AGENT", 14)}${w("KIND", 20)}LABEL`);
50
+ for (const r of rows)
51
+ console.log(` ${w(r.agent, 14)}${w(r.kind, 20)}${r.label}`);
52
+ }
53
+ catch (err) {
54
+ if (err instanceof ConfigError) {
55
+ console.error(err.message);
56
+ process.exit(1);
57
+ }
58
+ throw err;
59
+ }
60
+ }
61
+ export function runConfig(args) {
62
+ const sub = args[0];
63
+ switch (sub) {
64
+ case "path":
65
+ runConfigPath();
66
+ return;
67
+ case "validate":
68
+ runConfigValidate();
69
+ return;
70
+ case "labels":
71
+ runConfigLabels();
72
+ return;
73
+ case undefined:
74
+ case "--help":
75
+ case "-h":
76
+ console.log("tono config <subcommand>\n" +
77
+ " path print the config file path\n" +
78
+ " validate validate the current config against the schema\n" +
79
+ " labels print the GitHub labels each configured agent listens for");
80
+ return;
81
+ default:
82
+ console.error(`unknown subcommand: config ${sub}`);
83
+ process.exit(2);
84
+ }
85
+ }
86
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/cli/commands/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,260 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { ConfigError, configPath, SCHEMA_PATH, tonoHome, validate, } from "../../server/config/load.js";
4
+ import { openDb } from "../../server/db/client.js";
5
+ import { createPrompter } from "../prompt.js";
6
+ import { AGENT_TYPES, labelForAgentKind, } from "../../shared/types.js";
7
+ const SLUG_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
8
+ const IMPLEMENT_TEMPLATE_BASE = "You are working on GitHub issue #{issueNumber} in {repoSlug}.\n\n" +
9
+ "Title: {issueTitle}\n\n" +
10
+ "{issueBody}\n\n" +
11
+ "Implement the changes needed to resolve this issue. The current branch is " +
12
+ "{branch}, based on {baseBranch}. When implementation is complete, commit with " +
13
+ "a clear message, push the branch with `git push -u origin {branch}`, and open a " +
14
+ "pull request with `gh pr create --base {baseBranch} --fill`. If the issue is " +
15
+ "unclear or unsafe to proceed, leave a comment on the issue explaining why with " +
16
+ "`gh issue comment {issueNumber} --body \"<reason>\"` and stop.";
17
+ const REVIEW_TEMPLATE_BASE = "You are reviewing GitHub PR #{issueNumber} in {repoSlug}: \"{issueTitle}\".\n" +
18
+ "PR URL: {prUrl}\n" +
19
+ "The PR's head branch ({branch}) is checked out at the current working directory.\n\n" +
20
+ "Description:\n{issueBody}\n\n" +
21
+ "Read the diff (`gh pr diff {issueNumber}`) and inspect the changed files. Form a " +
22
+ "thorough review: correctness, edge cases, security, performance, readability.\n\n" +
23
+ "When done, post your review with " +
24
+ "`gh pr review {issueNumber} --comment --body \"<review body>\"` (use --request-changes " +
25
+ "if blocking, or --approve if good to merge). For inline file/line comments use " +
26
+ "`gh api repos/{repoSlug}/pulls/{issueNumber}/comments`.\n\n" +
27
+ "Do not push commits. Do not create new PRs.";
28
+ const DEFAULT_AGENT_COMMANDS = {
29
+ "claude-code": { command: "claude", args: ["--dangerously-skip-permissions"], conc: { implement: 2, review: 4 } },
30
+ codex: { command: "codex", args: [], conc: { implement: 1, review: 2 } },
31
+ opencode: { command: "opencode", args: ["run", "{prompt}"], conc: { implement: 1, review: 2 } },
32
+ };
33
+ function readExisting() {
34
+ const path = configPath();
35
+ if (!existsSync(path))
36
+ return null;
37
+ try {
38
+ return JSON.parse(readFileSync(path, "utf8"));
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ export async function runConfigure() {
45
+ const p = createPrompter();
46
+ try {
47
+ const existing = readExisting();
48
+ p.print("");
49
+ p.print("\x1b[1mtono configure\x1b[0m");
50
+ p.print("\x1b[2m" + (existing
51
+ ? `editing ${configPath()}`
52
+ : `creating ${configPath()}`) + "\x1b[0m");
53
+ p.print("");
54
+ // Server
55
+ const host = await p.ask("Bind host", {
56
+ default: existing?.server?.host ?? "0.0.0.0",
57
+ required: true,
58
+ });
59
+ const port = Number(await p.ask("Server port", {
60
+ default: String(existing?.server?.port ?? 7040),
61
+ validate: (s) => {
62
+ const n = Number(s);
63
+ if (!Number.isInteger(n) || n < 1 || n > 65535)
64
+ return "must be 1–65535";
65
+ return null;
66
+ },
67
+ }));
68
+ // GitHub
69
+ const pollIntervalSeconds = Number(await p.ask("Poll interval (seconds)", {
70
+ default: String(existing?.github?.pollIntervalSeconds ?? 60),
71
+ validate: (s) => {
72
+ const n = Number(s);
73
+ if (!Number.isInteger(n) || n < 10 || n > 3600)
74
+ return "must be 10–3600";
75
+ return null;
76
+ },
77
+ }));
78
+ // Workspaces
79
+ const workspacesRoot = await p.ask("Workspaces root", {
80
+ default: existing?.workspaces?.root ?? "~/.tono/workspaces",
81
+ required: true,
82
+ });
83
+ // Agents
84
+ p.print("");
85
+ p.print("\x1b[1mAgents\x1b[0m \x1b[2m(which agent CLIs to make available)\x1b[0m");
86
+ const enabledAgents = [];
87
+ for (const a of AGENT_TYPES) {
88
+ const wasEnabled = existing?.agents?.[a] !== undefined;
89
+ const yes = await p.askYesNo(` enable ${a}?`, wasEnabled || a === "claude-code");
90
+ if (yes)
91
+ enabledAgents.push(a);
92
+ }
93
+ if (enabledAgents.length === 0) {
94
+ p.print("\x1b[31mat least one agent must be enabled.\x1b[0m");
95
+ process.exit(1);
96
+ }
97
+ const agentsConfig = {};
98
+ for (const a of enabledAgents) {
99
+ const seed = existing?.agents?.[a];
100
+ const dflt = DEFAULT_AGENT_COMMANDS[a];
101
+ const command = await p.ask(` ${a} command`, {
102
+ default: seed?.command ?? dflt.command,
103
+ required: true,
104
+ });
105
+ const implement = Number(await p.ask(` ${a} implement concurrency`, {
106
+ default: String(seed?.concurrency?.implement ?? dflt.conc.implement),
107
+ validate: (s) => {
108
+ const n = Number(s);
109
+ if (!Number.isInteger(n) || n < 0 || n > 32)
110
+ return "0–32";
111
+ return null;
112
+ },
113
+ }));
114
+ const review = Number(await p.ask(` ${a} review concurrency`, {
115
+ default: String(seed?.concurrency?.review ?? dflt.conc.review),
116
+ validate: (s) => {
117
+ const n = Number(s);
118
+ if (!Number.isInteger(n) || n < 0 || n > 32)
119
+ return "0–32";
120
+ return null;
121
+ },
122
+ }));
123
+ agentsConfig[a] = {
124
+ command,
125
+ args: seed?.args ?? dflt.args,
126
+ concurrency: { implement, review },
127
+ promptTemplates: {
128
+ implement: seed?.promptTemplates?.implement ?? IMPLEMENT_TEMPLATE_BASE,
129
+ review: seed?.promptTemplates?.review ?? REVIEW_TEMPLATE_BASE,
130
+ },
131
+ };
132
+ }
133
+ // Repos
134
+ p.print("");
135
+ p.print("\x1b[1mRepos to watch\x1b[0m \x1b[2m(leave slug blank to finish)\x1b[0m");
136
+ const repos = [];
137
+ const seedRepos = existing?.repos ?? [];
138
+ let i = 0;
139
+ while (true) {
140
+ const seed = seedRepos[i];
141
+ const indexLabel = `Repo ${i + 1}`;
142
+ p.print("");
143
+ const slug = await p.ask(`${indexLabel} — owner/repo`, {
144
+ default: seed?.slug,
145
+ validate: (s) => {
146
+ if (s === "")
147
+ return null;
148
+ if (!SLUG_RE.test(s))
149
+ return "must be owner/repo";
150
+ if (repos.some((r) => r.slug === s))
151
+ return "already added";
152
+ return null;
153
+ },
154
+ });
155
+ if (slug === "")
156
+ break;
157
+ const path = await p.ask(`${indexLabel} — path to local clone (blank = tono will bare-clone via gh)`, {
158
+ default: seed?.path ?? "",
159
+ validate: (s) => (s === "" || s.startsWith("/") ? null : "use an absolute path or leave blank"),
160
+ });
161
+ const baseBranch = await p.ask(`${indexLabel} — base branch`, {
162
+ default: seed?.baseBranch ?? "main",
163
+ required: true,
164
+ });
165
+ // Per-repo agent enrollment (subset of declared agents)
166
+ p.print(`${indexLabel} — which agents are enabled on this repo?`);
167
+ const seededRepoAgents = new Set(seed?.agents ?? enabledAgents);
168
+ const repoAgents = [];
169
+ for (const a of enabledAgents) {
170
+ const wasOn = seededRepoAgents.has(a);
171
+ const yes = await p.askYesNo(` enroll ${a}?`, wasOn);
172
+ if (yes)
173
+ repoAgents.push(a);
174
+ }
175
+ repos.push({
176
+ slug,
177
+ baseBranch,
178
+ ...(path ? { path } : {}),
179
+ ...(repoAgents.length > 0 && repoAgents.length < enabledAgents.length ? { agents: repoAgents } : {}),
180
+ });
181
+ i++;
182
+ const more = await p.askYesNo("Add another repo?", false);
183
+ if (!more)
184
+ break;
185
+ }
186
+ // Build the config
187
+ const cfg = {
188
+ $schema: "./config.schema.json",
189
+ server: { host, port },
190
+ github: { pollIntervalSeconds },
191
+ workspaces: { root: workspacesRoot },
192
+ repos,
193
+ agents: agentsConfig,
194
+ };
195
+ // Validate
196
+ try {
197
+ validate(cfg);
198
+ }
199
+ catch (err) {
200
+ if (err instanceof ConfigError) {
201
+ p.print("");
202
+ p.print("\x1b[31mConfiguration is invalid:\x1b[0m");
203
+ p.print(err.message);
204
+ process.exit(1);
205
+ }
206
+ throw err;
207
+ }
208
+ // Summary
209
+ p.print("");
210
+ p.print("\x1b[1mSummary\x1b[0m");
211
+ p.print(` server: http://${host}:${port}`);
212
+ p.print(` poll interval: ${pollIntervalSeconds}s`);
213
+ p.print(` workspaces: ${workspacesRoot}`);
214
+ p.print(` agents:`);
215
+ for (const [name, a] of Object.entries(agentsConfig)) {
216
+ if (!a)
217
+ continue;
218
+ p.print(` - ${name}: command="${a.command}" implement=${a.concurrency.implement} review=${a.concurrency.review}`);
219
+ }
220
+ if (repos.length === 0) {
221
+ p.print(` repos: \x1b[2m(none — poller will idle)\x1b[0m`);
222
+ }
223
+ else {
224
+ p.print(` repos:`);
225
+ for (const r of repos) {
226
+ const enabled = r.agents ?? enabledAgents;
227
+ p.print(` - ${r.slug} (${r.baseBranch}, agents=[${enabled.join(", ")}])`);
228
+ }
229
+ }
230
+ p.print("");
231
+ p.print("\x1b[1mLabels you'll apply on GitHub:\x1b[0m");
232
+ for (const a of enabledAgents) {
233
+ p.print(` ${a}: \x1b[36m${labelForAgentKind(a, "implement")}\x1b[0m for issues, \x1b[36m${labelForAgentKind(a, "review")}\x1b[0m for PRs`);
234
+ }
235
+ p.print("");
236
+ const ok = await p.askYesNo(`Write to ${configPath()}?`, true);
237
+ if (!ok) {
238
+ p.print("\x1b[2maborted; no changes written\x1b[0m");
239
+ return;
240
+ }
241
+ const home = tonoHome();
242
+ mkdirSync(home, { recursive: true });
243
+ mkdirSync(join(home, "logs"), { recursive: true });
244
+ mkdirSync(join(home, "workspaces"), { recursive: true });
245
+ const schemaDest = join(home, "config.schema.json");
246
+ copyFileSync(SCHEMA_PATH, schemaDest);
247
+ p.print(`wrote ${schemaDest}`);
248
+ mkdirSync(dirname(configPath()), { recursive: true });
249
+ writeFileSync(configPath(), JSON.stringify(cfg, null, 2) + "\n");
250
+ p.print(`wrote ${configPath()}`);
251
+ const db = openDb();
252
+ db.close();
253
+ p.print("");
254
+ p.print("\x1b[32mdone.\x1b[0m run \x1b[1mtono start\x1b[0m or \x1b[1mtono gateway install\x1b[0m");
255
+ }
256
+ finally {
257
+ p.close();
258
+ }
259
+ }
260
+ //# sourceMappingURL=configure.js.map