ework-daemon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dog and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # ework-daemon
2
+
3
+ Issue-Driven AI Development Bridge. Connects an issue tracker (the [ework-web](../ework) SQLite shim, or real Gitea) to [OpenCode](https://github.com/sst/opencode) AI agents.
4
+
5
+ > Issue-driven AI development daemon. Listens on `:3101` by default; SQLite at `~/.local/share/ework-daemon/ework-daemon.db`; systemd unit `ework-daemon.service`.
6
+
7
+ ## What it does
8
+
9
+ You file an issue → the daemon spawns an OpenCode subprocess to handle it → the AI autonomously reads, replies, and closes via the `opencode-ework` plugin's tools (`issue`, `comments`, `floor`, `reply`, `search`). New user comments are forwarded to the running session. Stuck sessions (30 min no output) get a nudge.
10
+
11
+ **The daemon only does 4 things:**
12
+
13
+ 1. **New issue** → auto-spawn OpenCode.
14
+ 2. **`@<bot>` mention** on existing issue → trigger a new or existing session.
15
+ 3. **Existing session + new user comment** → forward to OpenCode (wrapped as a system message).
16
+ 4. **Stuck detection** → 30 min no stdout → stuck alert.
17
+
18
+ It does **not** post AI replies, auto-commit, or auto-merge. The AI does all issue interaction itself via the plugin.
19
+
20
+ ## Architecture
21
+
22
+ ```
23
+ ework-web (:3002) ework-daemon (:3101) OpenCode (subprocess)
24
+ │ │ │
25
+ │ ── issue webhook ────────────▸ │ │
26
+ │ │ ── Bun.spawn ──────────────▸ │
27
+ │ │ │
28
+ │ ◂── plugin GET /issues/n ──────┼───────────────────────────────┤
29
+ │ ◂── plugin POST /comments ─────┼───────────────────────────────┤
30
+ │ │ ◂── stdout streaming ─────────┤
31
+ │ ── new-comment webhook ─────▸ │ │
32
+ │ │ ── stdin (wrapped) ────────▸ │
33
+ │ │ ◂── completion signal ───────┤
34
+ │ ◂── plugin POST /close ────────┼───────────────────────────────┤
35
+ ```
36
+
37
+ The OpenCode subprocess loads the [`opencode-ework`](../opencode-ework) plugin, which speaks Gitea REST. The daemon injects `GITEA_URL`, `GITEA_TOKEN`, `BOT_TOKEN`, `BOT_USERNAME` env vars at spawn, so the plugin picks them up automatically. The plugin is backend-agnostic — same plugin works against real Gitea or any Gitea-compatible shim.
38
+
39
+ ## Prerequisites
40
+
41
+ - [Bun](https://bun.sh/) runtime (uses `Bun.serve`, `Bun.spawn`, `bun:sqlite` — **Node.js not supported**)
42
+ - [OpenCode](https://github.com/sst/opencode) binary
43
+ - A running [ework-web](../ework) instance (or real Gitea)
44
+ - A bot user PAT on that instance with comment/write permissions
45
+
46
+ The `tea` CLI is **no longer required** — the OpenCode subprocess uses the `opencode-ework` plugin's `reply` tool (HTTP `POST` with `BOT_TOKEN`), not `tea comment`. The `scripts/setup.ts` wizard still configures tea as a convenience for ad-hoc admin tasks; this will be removed in a future cleanup.
47
+
48
+ ## Quick start
49
+
50
+ ### 1. Install Bun
51
+
52
+ ```bash
53
+ curl -fsSL https://bun.sh/install | bash
54
+ ```
55
+
56
+ ### 2. Clone + setup
57
+
58
+ ```bash
59
+ git clone https://github.com/ranxianglei/ework-daemon.git
60
+ cd ework-daemon
61
+ bun install
62
+ bun run setup # interactive wizard: checks deps, creates data dir, writes .env
63
+ ```
64
+
65
+ ### 3. Configure
66
+
67
+ Edit `.env` (created from `.env.example` by the wizard):
68
+
69
+ ```bash
70
+ $EDITOR .env
71
+ ```
72
+
73
+ Required values:
74
+
75
+ - `GITEA_URL` — your ework-web (e.g. `http://localhost:3002`) or real Gitea URL
76
+ - `GITEA_TOKEN` — admin-scope read token
77
+ - `BOT_USERNAME` — bot user login (must match the user `BOT_TOKEN` belongs to)
78
+ - `BOT_TOKEN` — bot user PAT (used by the plugin to post replies)
79
+ - `GITEA_WEBHOOK_SECRET` — HMAC secret shared with the issue tracker's webhook config
80
+ - `DAEMON_ENV=production` — must be set to enable production mode (else falls back to test)
81
+ - `OPENCODE_BASE_WORKDIR` — parent directory for opencode work dirs (e.g. `/home/<user>/projects`)
82
+
83
+ ### 4. Run
84
+
85
+ ```bash
86
+ # Test mode (.env.test)
87
+ bun start
88
+
89
+ # Production (.env)
90
+ bun start:prod
91
+ ```
92
+
93
+ ### 5. Configure the webhook
94
+
95
+ On ework-web (or Gitea), go to the repository's **Settings → Webhooks → Add Webhook → Gitea**:
96
+
97
+ | Field | Value |
98
+ | --- | --- |
99
+ | Target URL | `http://<daemon-ip>:3101/webhook` |
100
+ | Content type | `application/json` |
101
+ | Secret | (the `GITEA_WEBHOOK_SECRET` from `.env`) |
102
+ | Trigger events | Issues, Issue Comment |
103
+
104
+ ## Usage
105
+
106
+ ### New issue → auto pickup
107
+
108
+ Create an issue. The daemon replies "🔄 picked up" and spawns an OpenCode session. The AI reads the issue via the plugin's `issue` tool and works autonomously.
109
+
110
+ ### Existing issue → trigger via mention
111
+
112
+ Comment with `@<bot>` (where `<bot>` is your configured `BOT_USERNAME`, e.g. `ework-daemon`):
113
+
114
+ ```
115
+ @ework-daemon this issue still needs work
116
+ ```
117
+
118
+ ### Ongoing session → add instructions
119
+
120
+ Once a session is running, just comment (no mention needed):
121
+
122
+ ```
123
+ Also add a "remember me" checkbox
124
+ ```
125
+
126
+ The daemon wraps the comment as a system message and pipes it to the running opencode session's stdin. The AI replies via the plugin's `reply` tool.
127
+
128
+ ### Close → stop
129
+
130
+ Closing the issue terminates the OpenCode subprocess and cleans up the session.
131
+
132
+ ## Development
133
+
134
+ ### Watch mode
135
+
136
+ ```bash
137
+ bun dev # test mode (.env.test) — auto-restart on file changes
138
+ bun dev:prod # production config (.env)
139
+ ```
140
+
141
+ ### Type check
142
+
143
+ ```bash
144
+ bun run check # tsc --noEmit
145
+ ```
146
+
147
+ ### Compile standalone binary
148
+
149
+ ```bash
150
+ bun run build # → ./ework-daemon
151
+ bun run install-global # build + cp to ~/.local/bin/
152
+ ```
153
+
154
+ The standalone binary can run without source: `ework-daemon status`, `ework-daemon issues`, etc. (see `src/cli.ts` for the CLI surface).
155
+
156
+ ### Smoke tests
157
+
158
+ ```bash
159
+ bun run test:gitea # ework-web / Gitea connectivity check
160
+ ```
161
+
162
+ End-to-end tests live at `scripts/test-e2e.ts` — run manually (not yet in `bun test` format).
163
+
164
+ ## Production deployment (systemd)
165
+
166
+ A systemd unit template is provided at `scripts/ework-daemon.service`. After running `./scripts/deploy.sh`:
167
+
168
+ ```bash
169
+ sudo cp scripts/ework-daemon.service /etc/systemd/system/
170
+ sudo systemctl daemon-reload
171
+ sudo systemctl enable --now ework-daemon
172
+ ```
173
+
174
+ Management commands:
175
+
176
+ ```bash
177
+ sudo systemctl status ework-daemon
178
+ sudo journalctl -u ework-daemon -f
179
+ ```
180
+
181
+ **Restart safety**: restarting kills all running OpenCode subprocesses. Always check first:
182
+
183
+ ```bash
184
+ curl -s http://localhost:3101/api/processes
185
+ # non-empty → wait; empty → safe to restart
186
+ ```
187
+
188
+ ### Deploy workflow
189
+
190
+ ```bash
191
+ cd /path/to/ework-daemon
192
+ bun run check && git add -A && git commit -m "..."
193
+ ./scripts/deploy.sh
194
+ # (verify no processes running) then:
195
+ sudo systemctl restart ework-daemon
196
+ ```
197
+
198
+ ## Environment variables
199
+
200
+ | Variable | Required | Default | Purpose |
201
+ | --- | --- | --- | --- |
202
+ | `DAEMON_ENV` | ✅ | `test` | `production` to use `.env` values |
203
+ | `GITEA_URL` | ✅ | — | ework-web or real Gitea base URL |
204
+ | `GITEA_TOKEN` | ✅ | — | Admin-scope read token |
205
+ | `GITEA_WEBHOOK_SECRET` | ✅ | — | HMAC secret for webhook verification |
206
+ | `BOT_USERNAME` | ✅ | — | Bot user login (anti-recursion check) |
207
+ | `BOT_TOKEN` | ✅ | — | Bot user PAT (passed to opencode subprocess) |
208
+ | `DAEMON_PORT` | — | `3101` | Listen port |
209
+ | `DAEMON_HOST` | — | `0.0.0.0` | Bind host (use `127.0.0.1` for local-only) |
210
+ | `OPENCODE_BINARY` | — | `opencode` | OpenCode binary path |
211
+ | `OPENCODE_BASE_WORKDIR` | ✅ | — | Parent dir for opencode work dirs |
212
+ | `COMPLETION_CHECK_API_KEY` | — | — | Optional: model API key for completion judgment |
213
+ | `COMPLETION_CHECK_BASE_URL` | — | — | Optional: model API base URL |
214
+ | `COMPLETION_CHECK_MODEL` | — | — | Optional: model name |
215
+ | `DAEMON_STUCK_THRESHOLD_MS` | — | `1800000` | Stuck threshold (30 min) |
216
+ | `DAEMON_MAX_STUCK_NUDGES` | — | `1` | Max stuck nudges before giving up |
217
+
218
+ Full list in `.env.example` and `src/config.ts`.
219
+
220
+ ## Data storage
221
+
222
+ | Data | Path |
223
+ | --- | --- |
224
+ | Production DB | `~/.local/share/ework-daemon/ework-daemon.db` |
225
+ | Test DB | `test/ework-daemon-test.db` (gitignored) |
226
+ | Large-message archive | `<workdir>/.ework-daemon/comment-NNN.txt` |
227
+ | Code repo work dirs | `$OPENCODE_BASE_WORKDIR/<repo>/` |
228
+
229
+ Honors `XDG_DATA_HOME`.
230
+
231
+ ## Anti-recursion
232
+
233
+ - The OpenCode subprocess posts comments via the `opencode-ework` plugin's `reply` tool, using `BOT_TOKEN` (which posts as user `BOT_USERNAME`).
234
+ - The daemon's webhook handler drops any webhook whose `comment.user.login === BOT_USERNAME`. **Critical**: `BOT_USERNAME` must exactly match the user that `BOT_TOKEN` belongs to, or this check fails and the daemon recurses into itself.
235
+ - Same-issue concurrent requests are serialized through an in-memory queue.
236
+
237
+ ## Project structure
238
+
239
+ ```
240
+ src/
241
+ ├── config.ts # Zod env validation (test/production dual mode)
242
+ ├── gitea.ts # Gitea-compatible REST client (admin read + bot write)
243
+ ├── server.ts # Webhook HTTP service (HMAC-SHA256)
244
+ ├── opencode.ts # OpenCode process management (queue, streaming, stuck detection)
245
+ ├── op.ts # SQLite persistence (Issue / OpSession / Message)
246
+ ├── cli.ts # CLI client
247
+ ├── index.ts # Entry
248
+ └── trackers/
249
+ ├── types.ts # IssueTracker interface + 3-entity model
250
+ └── gitea-tracker.ts # Gitea implementation (ework-web or real Gitea)
251
+ ```
252
+
253
+ See [AGENTS.md](./AGENTS.md) for the full development spec — it is the highest-priority doc for contributors.
254
+
255
+ ## Related projects
256
+
257
+ - [`ework`](../ework) — Standalone SQLite-backed issue tracker with Gitea-compatible REST shim. The default backend for ework-daemon.
258
+ - [`opencode-ework`](../opencode-ework) — Plugin loaded by the OpenCode subprocess. Provides `issue`, `comments`, `floor`, `reply`, `search` tools.
259
+
260
+ ## License
261
+
262
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import "../src/index.ts";
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "ework-daemon",
3
+ "version": "0.1.0",
4
+ "description": "Issue-driven AI development daemon. Spawns opencode subprocesses to resolve Gitea issues.",
5
+ "module": "src/index.ts",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "dog and contributors",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/ranxianglei/ework-daemon.git"
12
+ },
13
+ "homepage": "https://github.com/ranxianglei/ework-daemon",
14
+ "bugs": {
15
+ "url": "https://github.com/ranxianglei/ework-daemon/issues"
16
+ },
17
+ "keywords": [
18
+ "issue-driven",
19
+ "ai-agent",
20
+ "opencode",
21
+ "gitea",
22
+ "automation",
23
+ "daemon",
24
+ "bridge"
25
+ ],
26
+ "bin": {
27
+ "ework-daemon": "./src/cli.ts",
28
+ "ework-daemon-server": "./bin/ework-daemon-server.js"
29
+ },
30
+ "files": [
31
+ "src",
32
+ "bin",
33
+ "scripts/ework-daemon.service",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "engines": {
38
+ "bun": ">=1.1.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "setup": "bun run scripts/setup.ts",
45
+ "start": "bun --env-file=.env.test run src/index.ts",
46
+ "start:prod": "bun --env-file=.env run src/index.ts",
47
+ "dev": "bun --env-file=.env.test --watch src/index.ts",
48
+ "dev:prod": "bun --env-file=.env --watch src/index.ts",
49
+ "check": "tsc --noEmit",
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "bun test",
52
+ "test:gitea": "bun run scripts/test-gitea.ts",
53
+ "build": "bun build src/cli.ts --compile --outfile ework-daemon",
54
+ "install-global": "bun run build && cp ework-daemon ~/.local/bin/"
55
+ },
56
+ "devDependencies": {
57
+ "@types/bun": "latest"
58
+ },
59
+ "peerDependencies": {
60
+ "typescript": "^5"
61
+ },
62
+ "dependencies": {
63
+ "zod": "^4.4.3"
64
+ }
65
+ }
@@ -0,0 +1,22 @@
1
+ [Unit]
2
+ Description=ework-daemon — issue-driven AI dev daemon
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ User=ework
9
+ Group=ework
10
+ WorkingDirectory=%h/.local/share/ework-daemon
11
+ ExecStart=bun run src/index.ts
12
+ Restart=on-failure
13
+ RestartSec=5
14
+ KillMode=process
15
+ EnvironmentFile=%h/.local/share/ework-daemon/.env
16
+ Environment="PATH=/usr/local/bin:/usr/bin:/bin:%h/.local/bin:%h/.bun/bin"
17
+ StandardOutput=journal
18
+ StandardError=journal
19
+ SyslogIdentifier=ework-daemon
20
+
21
+ [Install]
22
+ WantedBy=multi-user.target
package/src/cli.ts ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env bun
2
+
3
+ const BASE = process.env.EDAEMON_URL || "http://localhost:3101";
4
+
5
+ async function api(path: string, init?: RequestInit) {
6
+ const res = await fetch(`${BASE}${path}`, init);
7
+ if (!res.ok) {
8
+ const body = await res.text();
9
+ console.error(`Error ${res.status}: ${body}`);
10
+ process.exit(1);
11
+ }
12
+ return res.json();
13
+ }
14
+
15
+ function fmtTime(ts: number | null) {
16
+ if (!ts) return "-";
17
+ const d = Date.now() - ts;
18
+ if (d < 60_000) return `${Math.floor(d / 1000)}s ago`;
19
+ if (d < 3_600_000) return `${Math.floor(d / 60_000)}m ago`;
20
+ return `${Math.floor(d / 3_600_000)}h ago`;
21
+ }
22
+
23
+ function fmtState(s: string) {
24
+ const colors: Record<string, string> = {
25
+ active: "\x1b[32m",
26
+ idle: "\x1b[36m",
27
+ running: "\x1b[33m",
28
+ closed: "\x1b[90m",
29
+ created: "\x1b[34m",
30
+ };
31
+ return `${colors[s] || ""}${s}\x1b[0m`;
32
+ }
33
+
34
+ // ─── Commands ───
35
+
36
+ async function cmdStatus() {
37
+ const data = (await api("/api/status")) as Record<string, unknown>;
38
+ console.log("Daemon: " + JSON.stringify(data.daemon));
39
+ console.log("Running: " + data.running);
40
+ console.log("Pending: " + data.pending);
41
+ console.log("Processes: " + data.processes);
42
+ console.log("Observed: " + data.observedIssues);
43
+ console.log("Issues: " + data.issues);
44
+ console.log("Sessions: " + data.sessions);
45
+ }
46
+
47
+ async function cmdIssues() {
48
+ const issues = (await api("/api/issues")) as Record<string, unknown>[];
49
+ if (issues.length === 0) {
50
+ console.log("No issues.");
51
+ return;
52
+ }
53
+ for (const issue of issues) {
54
+ const scopeKey = issue.trackerScopeKey as string;
55
+ const issueId = issue.trackerIssueId as string;
56
+ const state = fmtState(issue.state as string);
57
+ console.log(` ${state} ${issue.trackerType}:${scopeKey}#${issueId} "${issue.title}" id=${issue.id}`);
58
+ }
59
+ }
60
+
61
+ async function cmdSessions() {
62
+ const sessions = (await api("/api/sessions")) as Record<string, unknown>[];
63
+ if (sessions.length === 0) {
64
+ console.log("No sessions.");
65
+ return;
66
+ }
67
+ for (const s of sessions) {
68
+ const state = fmtState(s.state as string);
69
+ const pid = s.opencodePid ? `pid=${s.opencodePid}` : "";
70
+ const sess = s.opencodeSessionId ? `session=${(s.opencodeSessionId as string).slice(0, 8)}` : "";
71
+ const extras = [pid, sess].filter(Boolean).join(", ");
72
+ console.log(` ${state} @${s.name} ${extras} id=${s.id}`);
73
+ }
74
+ }
75
+
76
+ async function cmdIssue(id: string) {
77
+ const data = (await api(`/api/issues/${id}`)) as Record<string, unknown>;
78
+ console.log("ID: " + data.id);
79
+ console.log("Tracker: " + data.trackerType);
80
+ console.log("Scope: " + data.trackerScopeKey);
81
+ console.log("Issue ID: " + data.trackerIssueId);
82
+ console.log("State: " + fmtState(data.state as string));
83
+ console.log("Title: " + data.title);
84
+ console.log("Created: " + data.createdAt);
85
+ const sessions = data.sessions as Record<string, unknown>[] | undefined;
86
+ if (sessions && sessions.length > 0) {
87
+ console.log("Sessions:");
88
+ for (const s of sessions) {
89
+ console.log(` @${s.name} ${fmtState(s.state as string)} id=${s.id}`);
90
+ }
91
+ }
92
+ }
93
+
94
+ async function cmdSession(id: string) {
95
+ const data = await api(`/api/sessions/${id}`) as Record<string, unknown>;
96
+ console.log("ID: " + data.id);
97
+ console.log("Name: " + data.name);
98
+ console.log("State: " + fmtState(data.state as string));
99
+ console.log("Issue ID: " + data.issueId);
100
+ console.log("Workdir: " + (data.workdir || "-"));
101
+ console.log("PID: " + (data.opencodePid || "-"));
102
+ console.log("Session: " + (data.opencodeSessionId || "-"));
103
+ console.log("Created: " + data.createdAt);
104
+ }
105
+
106
+ async function cmdSessionMessages(id: string) {
107
+ const messages = (await api(`/api/sessions/${id}/messages`)) as Record<string, unknown>[];
108
+ if (messages.length === 0) {
109
+ console.log("No messages.");
110
+ return;
111
+ }
112
+ for (const m of messages) {
113
+ const status = fmtState(m.status as string);
114
+ const preview = (m.content as string).slice(0, 60).replace(/\n/g, " ");
115
+ console.log(` ${status} ${preview}... id=${(m.id as string).slice(0, 8)}`);
116
+ }
117
+ }
118
+
119
+ async function cmdQueue() {
120
+ const q = (await api("/api/queue")) as Record<string, number>;
121
+ const entries = Object.entries(q);
122
+ if (entries.length === 0) {
123
+ console.log("Queue empty.");
124
+ return;
125
+ }
126
+ for (const [key, depth] of entries) {
127
+ console.log(` ${key}: ${depth}`);
128
+ }
129
+ }
130
+
131
+ async function cmdProcesses() {
132
+ const procs = (await api("/api/processes")) as Record<string, unknown>[];
133
+ if (procs.length === 0) {
134
+ console.log("No running processes.");
135
+ return;
136
+ }
137
+ for (const p of procs) {
138
+ console.log(
139
+ ` ▶ ${p.key} pid=${p.pid} last=${fmtTime(p.lastOutputAt as number | null)}`
140
+ );
141
+ }
142
+ }
143
+
144
+ async function cmdRetry(msgId: string) {
145
+ const res = (await api(`/api/messages/${msgId}/retry`, { method: "PATCH" })) as Record<string, unknown>;
146
+ console.log(`Retried: ${res.ok} id=${msgId}`);
147
+ }
148
+
149
+ async function cmdStop(key: string) {
150
+ const res = (await api(`/api/processes/${encodeURIComponent(key)}`, { method: "DELETE" })) as Record<string, unknown>;
151
+ console.log(`Stopped: ${res.stopped}`);
152
+ }
153
+
154
+ // ─── Main ───
155
+
156
+ function usage() {
157
+ console.log(`Usage: ework-daemon <command> [args...]
158
+
159
+ Commands:
160
+ status Daemon overview
161
+ issues List all issues
162
+ issue <id> Show issue detail + sessions
163
+ sessions List all sessions
164
+ session <id> Show session detail
165
+ messages <sessionId> Show messages for a session
166
+ queue Show pending queues
167
+ processes Show running processes
168
+ retry <msgId> Retry a failed message
169
+ stop <key> Kill process by runtime key
170
+
171
+ Environment:
172
+ EDAEMON_URL Daemon URL (default: http://localhost:3101)
173
+ `);
174
+ }
175
+
176
+ const args = process.argv.slice(2);
177
+ const cmd = args[0];
178
+
179
+ if (!cmd) {
180
+ usage();
181
+ process.exit(0);
182
+ }
183
+
184
+ const commands: Record<string, () => Promise<void>> = {
185
+ status: cmdStatus,
186
+ issues: cmdIssues,
187
+ sessions: cmdSessions,
188
+ queue: cmdQueue,
189
+ processes: cmdProcesses,
190
+ issue: async () => {
191
+ if (!args[1]) { console.error("Usage: ework-daemon issue <id>"); process.exit(1); }
192
+ await cmdIssue(args[1]);
193
+ },
194
+ session: async () => {
195
+ if (!args[1]) { console.error("Usage: ework-daemon session <id>"); process.exit(1); }
196
+ await cmdSession(args[1]);
197
+ },
198
+ messages: async () => {
199
+ if (!args[1]) { console.error("Usage: ework-daemon messages <sessionId>"); process.exit(1); }
200
+ await cmdSessionMessages(args[1]);
201
+ },
202
+ retry: async () => {
203
+ if (!args[1]) { console.error("Usage: ework-daemon retry <msgId>"); process.exit(1); }
204
+ await cmdRetry(args[1]);
205
+ },
206
+ stop: async () => {
207
+ if (!args[1]) { console.error("Usage: ework-daemon stop <key>"); process.exit(1); }
208
+ await cmdStop(args[1]);
209
+ },
210
+ };
211
+
212
+ if (!commands[cmd]) {
213
+ console.error(`Unknown command: ${cmd}`);
214
+ usage();
215
+ process.exit(1);
216
+ }
217
+
218
+ commands[cmd]!();