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.
- package/LICENSE +21 -674
- package/README.md +300 -0
- package/dist/cli/commands/config.js +86 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/configure.js +260 -0
- package/dist/cli/commands/configure.js.map +1 -0
- package/dist/cli/commands/gateway.js +194 -0
- package/dist/cli/commands/gateway.js.map +1 -0
- package/dist/cli/commands/init.js +89 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/open.js +23 -0
- package/dist/cli/commands/open.js.map +1 -0
- package/dist/cli/commands/start.js +116 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/index.js +78 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/launchd.js +56 -0
- package/dist/cli/launchd.js.map +1 -0
- package/dist/cli/prompt.js +46 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/server/agents/claude-code.js +80 -0
- package/dist/server/agents/claude-code.js.map +1 -0
- package/dist/server/agents/codex.js +52 -0
- package/dist/server/agents/codex.js.map +1 -0
- package/dist/server/agents/opencode.js +50 -0
- package/dist/server/agents/opencode.js.map +1 -0
- package/dist/server/agents/registry.js +16 -0
- package/dist/server/agents/registry.js.map +1 -0
- package/dist/server/agents/types.js +40 -0
- package/dist/server/agents/types.js.map +1 -0
- package/dist/server/app.js +325 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/config/load.js +91 -0
- package/dist/server/config/load.js.map +1 -0
- package/dist/server/config/manager.js +38 -0
- package/dist/server/config/manager.js.map +1 -0
- package/dist/server/config/schema.json +151 -0
- package/dist/server/db/client.js +136 -0
- package/dist/server/db/client.js.map +1 -0
- package/dist/server/db/queries.js +158 -0
- package/dist/server/db/queries.js.map +1 -0
- package/dist/server/db/schema.sql +61 -0
- package/dist/server/events.js +5 -0
- package/dist/server/events.js.map +1 -0
- package/dist/server/git/worktrees.js +225 -0
- package/dist/server/git/worktrees.js.map +1 -0
- package/dist/server/github/gh.js +172 -0
- package/dist/server/github/gh.js.map +1 -0
- package/dist/server/pty/manager.js +129 -0
- package/dist/server/pty/manager.js.map +1 -0
- package/dist/server/pty/ring-buffer.js +40 -0
- package/dist/server/pty/ring-buffer.js.map +1 -0
- package/dist/server/server.js +36 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/workers/github-poller.js +185 -0
- package/dist/server/workers/github-poller.js.map +1 -0
- package/dist/server/workers/pr-watcher.js +111 -0
- package/dist/server/workers/pr-watcher.js.map +1 -0
- package/dist/server/workers/scheduler.js +209 -0
- package/dist/server/workers/scheduler.js.map +1 -0
- package/dist/server/ws/pty.js +72 -0
- package/dist/server/ws/pty.js.map +1 -0
- package/dist/shared/types.js +23 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/version.js +18 -0
- package/dist/shared/version.js.map +1 -0
- package/dist/web/assets/index-5VFn-lxF.js +129 -0
- package/dist/web/assets/index-CZHd5NaX.css +1 -0
- package/dist/web/index.html +19 -0
- package/package.json +79 -6
- package/scripts/fix-node-pty.mjs +35 -0
- 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
|