@famonfa/mono 0.0.0-stage → 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.
Files changed (61) hide show
  1. package/bin/mono.mjs +210 -0
  2. package/node_modules/@anthropic-ai/claude-agent-sdk/LICENSE.md +1 -0
  3. package/node_modules/@anthropic-ai/claude-agent-sdk/README.md +69 -0
  4. package/node_modules/@anthropic-ai/claude-agent-sdk/agentSdkTypes.d.ts +1 -0
  5. package/node_modules/@anthropic-ai/claude-agent-sdk/bridge.d.ts +396 -0
  6. package/node_modules/@anthropic-ai/claude-agent-sdk/bridge.mjs +142 -0
  7. package/node_modules/@anthropic-ai/claude-agent-sdk/browser-sdk.d.ts +208 -0
  8. package/node_modules/@anthropic-ai/claude-agent-sdk/browser-sdk.js +145 -0
  9. package/node_modules/@anthropic-ai/claude-agent-sdk/core-36zyfan5.mjs +52 -0
  10. package/node_modules/@anthropic-ai/claude-agent-sdk/core-5682eevd.mjs +27 -0
  11. package/node_modules/@anthropic-ai/claude-agent-sdk/core-nne0e2ds.mjs +40 -0
  12. package/node_modules/@anthropic-ai/claude-agent-sdk/core-xpxe8nzb.mjs +26 -0
  13. package/node_modules/@anthropic-ai/claude-agent-sdk/core.d.ts +22 -0
  14. package/node_modules/@anthropic-ai/claude-agent-sdk/core.mjs +34 -0
  15. package/node_modules/@anthropic-ai/claude-agent-sdk/extractFromBunfs.d.ts +1 -0
  16. package/node_modules/@anthropic-ai/claude-agent-sdk/extractFromBunfs.js +156 -0
  17. package/node_modules/@anthropic-ai/claude-agent-sdk/manifest.json +82 -0
  18. package/node_modules/@anthropic-ai/claude-agent-sdk/manifest.zst.json +90 -0
  19. package/node_modules/@anthropic-ai/claude-agent-sdk/package.json +94 -0
  20. package/node_modules/@anthropic-ai/claude-agent-sdk/sdk-tools.d.ts +4222 -0
  21. package/node_modules/@anthropic-ai/claude-agent-sdk/sdk.d.ts +10277 -0
  22. package/node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs +169 -0
  23. package/node_modules/ws/LICENSE +20 -0
  24. package/node_modules/ws/README.md +548 -0
  25. package/node_modules/ws/browser.js +8 -0
  26. package/node_modules/ws/index.js +22 -0
  27. package/node_modules/ws/lib/buffer-util.js +131 -0
  28. package/node_modules/ws/lib/constants.js +19 -0
  29. package/node_modules/ws/lib/event-target.js +292 -0
  30. package/node_modules/ws/lib/extension.js +203 -0
  31. package/node_modules/ws/lib/limiter.js +55 -0
  32. package/node_modules/ws/lib/permessage-deflate.js +530 -0
  33. package/node_modules/ws/lib/receiver.js +743 -0
  34. package/node_modules/ws/lib/sender.js +607 -0
  35. package/node_modules/ws/lib/stream.js +161 -0
  36. package/node_modules/ws/lib/subprotocol.js +62 -0
  37. package/node_modules/ws/lib/validation.js +152 -0
  38. package/node_modules/ws/lib/websocket-server.js +562 -0
  39. package/node_modules/ws/lib/websocket.js +1421 -0
  40. package/node_modules/ws/package.json +74 -0
  41. package/node_modules/ws/wrapper.mjs +21 -0
  42. package/package.json +126 -3
  43. package/skills/workspace/SKILL.md +108 -0
  44. package/src/api.mjs +279 -0
  45. package/src/chats.mjs +419 -0
  46. package/src/commands.mjs +100 -0
  47. package/src/groups.mjs +84 -0
  48. package/src/launcher.mjs +212 -0
  49. package/src/main.mjs +42 -0
  50. package/src/paths.mjs +44 -0
  51. package/src/projects.mjs +199 -0
  52. package/src/refresh.mjs +124 -0
  53. package/src/remote.mjs +129 -0
  54. package/src/sessions.mjs +422 -0
  55. package/src/store.mjs +253 -0
  56. package/web/assets/index-BMbPwlF6.js +55 -0
  57. package/web/assets/index-Ke-WlOuz.css +1 -0
  58. package/web/icon.svg +1 -0
  59. package/web/index.html +17 -0
  60. package/web/manifest.webmanifest +9 -0
  61. package/README.md +0 -3
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "ws",
3
+ "version": "8.22.0",
4
+ "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
5
+ "keywords": [
6
+ "HyBi",
7
+ "Push",
8
+ "RFC-6455",
9
+ "WebSocket",
10
+ "WebSockets",
11
+ "real-time"
12
+ ],
13
+ "homepage": "https://github.com/websockets/ws",
14
+ "bugs": "https://github.com/websockets/ws/issues",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/websockets/ws.git"
18
+ },
19
+ "author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
20
+ "license": "MIT",
21
+ "main": "index.js",
22
+ "exports": {
23
+ ".": {
24
+ "browser": "./browser.js",
25
+ "import": "./wrapper.mjs",
26
+ "require": "./index.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "browser": "browser.js",
31
+ "engines": {
32
+ "node": ">=10.0.0"
33
+ },
34
+ "files": [
35
+ "browser.js",
36
+ "index.js",
37
+ "lib/*.js",
38
+ "wrapper.mjs"
39
+ ],
40
+ "scripts": {
41
+ "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
42
+ "integration": "mocha --throw-deprecation test/*.integration.js",
43
+ "lint": "eslint . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\""
44
+ },
45
+ "peerDependencies": {
46
+ "bufferutil": "^4.0.1",
47
+ "utf-8-validate": ">=5.0.2"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "bufferutil": {
51
+ "optional": true
52
+ },
53
+ "utf-8-validate": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "devDependencies": {
58
+ "@eslint/js": "^10.0.1",
59
+ "benchmark": "^2.1.4",
60
+ "bufferutil": "^4.0.1",
61
+ "eslint": "^10.0.1",
62
+ "eslint-config-prettier": "^10.0.1",
63
+ "eslint-plugin-prettier": "^5.0.0",
64
+ "globals": "^17.0.0",
65
+ "mocha": "^8.4.0",
66
+ "nyc": "^15.0.0",
67
+ "prettier": "^3.0.0",
68
+ "utf-8-validate": "^6.0.0"
69
+ },
70
+ "allowScripts": {
71
+ "bufferutil": true,
72
+ "utf-8-validate": true
73
+ }
74
+ }
@@ -0,0 +1,21 @@
1
+ import createWebSocketStream from './lib/stream.js';
2
+ import extension from './lib/extension.js';
3
+ import PerMessageDeflate from './lib/permessage-deflate.js';
4
+ import Receiver from './lib/receiver.js';
5
+ import Sender from './lib/sender.js';
6
+ import subprotocol from './lib/subprotocol.js';
7
+ import WebSocket from './lib/websocket.js';
8
+ import WebSocketServer from './lib/websocket-server.js';
9
+
10
+ export {
11
+ createWebSocketStream,
12
+ extension,
13
+ PerMessageDeflate,
14
+ Receiver,
15
+ Sender,
16
+ subprotocol,
17
+ WebSocket,
18
+ WebSocketServer
19
+ };
20
+
21
+ export default WebSocket;
package/package.json CHANGED
@@ -1,6 +1,129 @@
1
1
  {
2
2
  "name": "@famonfa/mono",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Mono: watch and drive your Claude Code agents from your Mac, the web or your phone",
5
+ "type": "module",
6
+ "bin": {
7
+ "mono": "bin/mono.mjs",
8
+ "ws": "bin/mono.mjs"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "skills",
14
+ "web"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "@anthropic-ai/claude-agent-sdk": "^0.3.308",
24
+ "ws": "^8.22.0"
25
+ },
26
+ "bundleDependencies": [
27
+ "@anthropic-ai/claude-agent-sdk",
28
+ "@anthropic-ai/sdk",
29
+ "@babel/runtime",
30
+ "@hono/node-server",
31
+ "@modelcontextprotocol/sdk",
32
+ "@opentelemetry/api",
33
+ "@stablelib/base64",
34
+ "accepts",
35
+ "ajv",
36
+ "ajv-formats",
37
+ "body-parser",
38
+ "bytes",
39
+ "call-bind-apply-helpers",
40
+ "call-bound",
41
+ "content-disposition",
42
+ "content-type",
43
+ "cookie",
44
+ "cookie-signature",
45
+ "cors",
46
+ "cross-spawn",
47
+ "debug",
48
+ "depd",
49
+ "dunder-proto",
50
+ "ee-first",
51
+ "encodeurl",
52
+ "es-define-property",
53
+ "es-errors",
54
+ "es-object-atoms",
55
+ "escape-html",
56
+ "etag",
57
+ "eventsource",
58
+ "eventsource-parser",
59
+ "express",
60
+ "express-rate-limit",
61
+ "fast-deep-equal",
62
+ "fast-sha256",
63
+ "fast-uri",
64
+ "finalhandler",
65
+ "forwarded",
66
+ "fresh",
67
+ "function-bind",
68
+ "get-intrinsic",
69
+ "get-proto",
70
+ "gopd",
71
+ "has-symbols",
72
+ "hasown",
73
+ "hono",
74
+ "http-errors",
75
+ "iconv-lite",
76
+ "inherits",
77
+ "ip-address",
78
+ "ipaddr.js",
79
+ "is-promise",
80
+ "isexe",
81
+ "jose",
82
+ "json-schema-to-ts",
83
+ "json-schema-traverse",
84
+ "json-schema-typed",
85
+ "math-intrinsics",
86
+ "media-typer",
87
+ "merge-descriptors",
88
+ "mime-db",
89
+ "mime-types",
90
+ "ms",
91
+ "negotiator",
92
+ "object-assign",
93
+ "object-inspect",
94
+ "on-finished",
95
+ "once",
96
+ "parseurl",
97
+ "path-key",
98
+ "path-to-regexp",
99
+ "pkce-challenge",
100
+ "proxy-addr",
101
+ "qs",
102
+ "range-parser",
103
+ "raw-body",
104
+ "require-from-string",
105
+ "router",
106
+ "safer-buffer",
107
+ "send",
108
+ "serve-static",
109
+ "setprototypeof",
110
+ "shebang-command",
111
+ "shebang-regex",
112
+ "side-channel",
113
+ "side-channel-list",
114
+ "side-channel-map",
115
+ "side-channel-weakmap",
116
+ "standardwebhooks",
117
+ "statuses",
118
+ "toidentifier",
119
+ "ts-algebra",
120
+ "type-is",
121
+ "unpipe",
122
+ "vary",
123
+ "which",
124
+ "wrappy",
125
+ "ws",
126
+ "zod",
127
+ "zod-to-json-schema"
128
+ ]
6
129
  }
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: workspace
3
+ description: Document a task visually in Mono (http://localhost:4600) — TL;DR, story map, before/after flow, test flows and technical map as editable canvases, plus short docs. Use when the user runs /workspace [TICKET], asks to "document this in my workspace", "documentalo en mi workspace", "armame el canvas / story map / flujos", or wants a visual summary of work done. Never commit or publish its output.
4
+ ---
5
+
6
+ # Workspace
7
+
8
+ Turn a task into a visual, editable workspace entry. The user reviews work by **looking at flows**, not by reading the conversation. Optimize for: understood in 30 seconds, then explore by clicking.
9
+
10
+ Mono runs locally on this machine. Data: `ws path <TASK>` prints the task folder. The UI refreshes live when you write.
11
+
12
+ CLI: `mono` or its alias `ws` (on PATH) — run `ws` with no args for usage. Don't store it in a shell variable: zsh won't word-split `$WS`.
13
+
14
+ ## Arguments
15
+
16
+ `/workspace [TICKET | slug] [board...]`
17
+
18
+ - No argument: infer the ticket from the current worktree path (`.worktrees/<TICKET>/`) or branch name.
19
+ - Extra args limit which boards to (re)generate, e.g. `/workspace ABC-123 testeo`.
20
+
21
+ ## 1. Make sure the app is running
22
+
23
+ ```bash
24
+ curl -s -o /dev/null localhost:4600/api/health || mono serve > /tmp/mono.log 2>&1 &
25
+ ```
26
+
27
+ ## 2. Gather facts (cheaply)
28
+
29
+ Read only what you need; never paste big outputs into context.
30
+
31
+ - **Existing entry:** `ws show <TASK>` — if it exists you are *updating*: keep node ids stable so the user's layout survives.
32
+ - **Conversation:** the work you just did is the best source. Use it.
33
+ - **Other agents:** `ws sessions <TASK>` prints what every linked Claude Code session and subagent did recently (sessions link by PR, branch, worktree or a mention of the task id). Use it as evidence for test results and new findings — e.g. QA agents running in another session.
34
+ - **Ticket:** if the project has a tracker CLI or API documented in its CLAUDE.md (Linear, Jira, GitHub issues), fetch only title, url, state and the first ~2000 chars of the description.
35
+ - **Code:** per repo worktree in `.worktrees/<TASK>/<repo>`: `git log --oneline origin/<base>..HEAD` and `git diff --stat origin/<base>...HEAD | tail -30`. Read specific diffs only to understand behavior.
36
+ - **PRs:** `gh pr view --json number,url,title,state` inside each worktree.
37
+ - **UI changes?** Check the diff for UI files (`.tsx`, `.css`, `.css.ts`, screens/components). If there are none, do not create the `ui` board. If there are and no screenshots exist yet, screenshot the running app (Playwright or the project's own capture tooling), then build the `ui` board from them.
38
+ - **Screenshots:** if QA or capture tools produced images (simulator screenshots, Playwright captures), copy them in with `ws asset <TASK> <files...>` and reference them as `assets/<name>.png`.
39
+
40
+ ## 3. Write the entry
41
+
42
+ Write JSON/MD to `/tmp/ws-<TASK>/` and load each with the CLI. Write everything in the user's language.
43
+
44
+ ### task.json → `ws task <TASK> /tmp/ws-<TASK>/task.json`
45
+
46
+ ```json
47
+ {
48
+ "title": "Refund + cancel en órdenes pagas",
49
+ "status": "en-curso",
50
+ "risk": "medio",
51
+ "tldr": { "problema": "…", "solucion": "…", "impacto": "…" },
52
+ "summary": "markdown corto: origen, estado, pendientes fuera de scope",
53
+ "repos": [{ "name": "api", "branch": "ABC-123-refunds", "pr": "https://github.com/…/pull/1637" }],
54
+ "linear": "https://linear.app/…/issue/ABC-123",
55
+ "tags": ["refunds"],
56
+ "relatedPrs": [1476, 1479]
57
+ }
58
+ ```
59
+
60
+ `status`: `planificado | en-curso | en-review | hecho | bloqueado`. `relatedPrs`: superseded or companion PR numbers, so agent sessions that worked on them link to this task. `projects`: optional list of project ids (the directories Mono shows: `curl -s localhost:4600/api/projects | jq -r '.[].id'`); if omitted, repos are matched to projects by name. `risk`: `bajo | medio | alto`. Each TL;DR field ≤ 2 sentences.
61
+
62
+ ### Boards → `ws board <TASK> <board-id> /tmp/ws-<TASK>/<board>.json`
63
+
64
+ Default set (skip a board when it adds nothing, e.g. no tech map for a one-file fix):
65
+
66
+ | board-id | kind | layout | What it shows |
67
+ | --- | --- | --- | --- |
68
+ | `story-map` | storymap | storymap | Actors → activities (columns, in order) → steps under each activity. Mark what changed. |
69
+ | `flujo` | flow | lanes | Lanes `Antes` / `Después` with the same steps side by side, or one lane per variant. |
70
+ | `testeo` | tests | lanes | One lane per test path; last node carries the result (`test`) and screenshot (`image`). |
71
+ | `mapa-tecnico` | tech | lanes | One lane per repo; nodes = modules/files touched; edges = data flow between them. |
72
+ | `ui` | ui | lanes | **Only if the change touched UI.** One lane per screen/state; `image` nodes with real screenshots (`status` new/changed). Skip entirely for backend-only work. |
73
+
74
+ Board file shape (flat nodes — the CLI normalizes them):
75
+
76
+ ```json
77
+ {
78
+ "title": "Flujos de testeo", "kind": "tests", "layout": "lanes", "direction": "LR", "order": 3,
79
+ "description": "Una línea que explica cómo leer el board.",
80
+ "nodes": [
81
+ { "id": "t1a", "lane": "1 · Reembolso parcial", "label": "Orden paga con loyalty" },
82
+ { "id": "t1b", "lane": "1 · Reembolso parcial", "label": "Puntos revocados", "test": "pass", "image": "assets/refund-partial.png" }
83
+ ],
84
+ "edges": [{ "from": "t1a", "to": "t1b", "label": "opcional", "dashed": false, "animated": false }]
85
+ }
86
+ ```
87
+
88
+ Never create `lane`-type nodes or set `parentId`/`position` — the app owns layout; put the lane name in each node's `lane`. If a write goes wrong, `ws undo <task> <board>` restores the previous version.
89
+
90
+ Node fields: `id` (stable, short), `type` (`card` default | `actor` | `activity` | `note` | `image`), `label`, `detail`, `status` (`new | changed | same | removed | risk`), `test` (`pass | fail | todo | skip`), `repo`, `files` (array), `image` (`assets/…`), `lane` (lanes layout), `activity` (storymap: id of the activity column), `color` (hex, notes), `link`.
91
+
92
+ Storymap specifics: `actor` nodes go in a left column; `activity` nodes become columns in array order (prefix labels with `1.`, `2.`…); steps reference their column with `activity`. Use a `note` (color `#fee2e2`) for pending items or risks outside scope.
93
+
94
+ ### Docs → `ws doc <TASK> <name>.md /tmp/ws-<TASK>/<name>.md`
95
+
96
+ Only when they add value; usually just `decisiones.md` (table: decisión | por qué, plus rollout risks). No essays.
97
+
98
+ ## Content rules
99
+
100
+ - **Labels ≤ 6 words.** `detail` ≤ 2 short lines. If it needs more, it belongs in a doc.
101
+ - **Every step gets a `status`** in story map, flow and tech map, so colors tell the story: green new, orange changed, gray unchanged, purple risk/bug (use `risk` for the "before" problems).
102
+ - **Test results must be real.** `pass`/`fail` only for paths that were actually exercised (tests run, manual QA, simulator); everything else is `todo`. Never invent results.
103
+ - **Facts only from evidence** (diff, ticket, PR, conversation). If unsure, put it in a `note` as a question.
104
+ - **Updating:** reuse the same ids. The CLI keeps positions the user arranged, keeps nodes the user edited (`edited`) or created (`manual`), and places new nodes next to their predecessor. Use `--relayout` only if the user asks to reorganize everything.
105
+
106
+ ## 4. Finish
107
+
108
+ Run `ws open <TASK>` and reply with **one or two lines**: what boards were created/updated and anything that needs the user's attention (failed tests, open questions). Don't repeat the content in chat — it lives in the workspace.
package/src/api.mjs ADDED
@@ -0,0 +1,279 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import * as store from "./store.mjs";
4
+ import * as sessions from "./sessions.mjs";
5
+ import * as chats from "./chats.mjs";
6
+ import * as groups from "./groups.mjs";
7
+ import * as projects from "./projects.mjs";
8
+ import { listCommands } from "./commands.mjs";
9
+ import { refreshStatus, startRefresh } from "./refresh.mjs";
10
+
11
+ const MIME = {
12
+ ".png": "image/png",
13
+ ".jpg": "image/jpeg",
14
+ ".jpeg": "image/jpeg",
15
+ ".gif": "image/gif",
16
+ ".webp": "image/webp",
17
+ ".svg": "image/svg+xml",
18
+ ".webmanifest": "application/manifest+json",
19
+ ".md": "text/markdown; charset=utf-8",
20
+ ".json": "application/json",
21
+ ".html": "text/html; charset=utf-8",
22
+ ".js": "text/javascript",
23
+ ".css": "text/css",
24
+ ".woff2": "font/woff2",
25
+ };
26
+
27
+ const ALLOWED_HOSTS = /^(localhost|127\.0\.0\.1)(:\d+)?$/;
28
+
29
+ function readBody(req) {
30
+ return new Promise((resolve, reject) => {
31
+ let raw = "";
32
+ req.on("data", (chunk) => (raw += chunk));
33
+ req.on("end", () => resolve(raw));
34
+ req.on("error", reject);
35
+ });
36
+ }
37
+
38
+ function send(res, status, body) {
39
+ res.statusCode = status;
40
+ res.setHeader("Content-Type", "application/json");
41
+ res.end(JSON.stringify(body ?? null));
42
+ }
43
+
44
+ export function serveStatic(res, root, rel) {
45
+ const file = path.resolve(root, rel);
46
+ if (!file.startsWith(root + path.sep) || !fs.existsSync(file) || !fs.statSync(file).isFile()) return false;
47
+ res.setHeader("Content-Type", MIME[path.extname(file).toLowerCase()] ?? "application/octet-stream");
48
+ fs.createReadStream(file).pipe(res);
49
+ return true;
50
+ }
51
+
52
+ /**
53
+ * The API can launch agents, so it only answers same-machine requests: the Host header must be
54
+ * localhost (blocks DNS rebinding) and mutating requests must come from the app's own origin
55
+ * with a JSON body (blocks other websites from posting to localhost).
56
+ */
57
+ function isTrusted(req) {
58
+ if (!ALLOWED_HOSTS.test(req.headers.host ?? "")) return false;
59
+ if (req.method === "GET") return true;
60
+ const origin = req.headers.origin;
61
+ if (origin) {
62
+ try {
63
+ if (!ALLOWED_HOSTS.test(new URL(origin).host)) return false;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+ return (req.headers["content-type"] ?? "").includes("application/json") || req.method === "DELETE";
69
+ }
70
+
71
+ function sessionList(days) {
72
+ const list = sessions.listSessions({ days });
73
+ const live = new Set(chats.liveChatIds());
74
+ const focus = chats.readSessionFocus();
75
+ for (const task of store.listTasks()) {
76
+ for (const s of sessions.sessionsForTask(task, list)) (s.taskIds ??= []).push(task.id);
77
+ }
78
+ for (const s of list) {
79
+ s.ownedByWorkspace = live.has(s.id);
80
+ s.focusCwd = focus[s.id] ?? null;
81
+ s.projectId = projects.projectForPath(focus[s.id] ?? s.firstCwd ?? s.cwd);
82
+ }
83
+ return list;
84
+ }
85
+
86
+ const withProjects = (task) => ({ ...task, projectIds: projects.projectsForTask(task) });
87
+
88
+ function workingElsewhere(sessionId) {
89
+ const live = new Set(chats.liveChatIds());
90
+ return sessions
91
+ .listSessions({ days: 1 })
92
+ .filter((s) => s.status === "working" && !live.has(s.id))
93
+ .map((s) => s.id)
94
+ .filter((id) => id === sessionId);
95
+ }
96
+
97
+ async function handleChats(res, method, parts, body) {
98
+ const [, sessionId, action, actionId] = parts;
99
+ if (!sessionId && method === "POST") {
100
+ const prompt = String(body.prompt ?? "").trim();
101
+ if (!prompt) throw new Error("Escribí un mensaje");
102
+ return send(res, 200, await chats.startChat({ taskId: body.taskId, cwd: body.cwd, prompt, model: body.model, effort: body.effort, permissionMode: body.permissionMode, attachments: body.attachments ?? [] }));
103
+ }
104
+ if (sessionId && !action && method === "GET") return send(res, 200, chats.chatState(sessionId));
105
+ if (action === "messages" && method === "POST") {
106
+ const text = String(body.text ?? "").trim();
107
+ if (!text) throw new Error("Escribí un mensaje");
108
+ return send(res, 200, await chats.sendMessage(sessionId, text, { workingSessions: workingElsewhere(sessionId), model: body.model, effort: body.effort, permissionMode: body.permissionMode, attachments: body.attachments ?? [] }));
109
+ }
110
+ if (action === "interrupt" && method === "POST") return send(res, 200, await chats.interrupt(sessionId));
111
+ if (action === "config" && method === "POST") return send(res, 200, await chats.configure(sessionId, body));
112
+ if (action === "permissions" && actionId && method === "POST") return send(res, 200, chats.answerPermission(sessionId, actionId, body));
113
+ return send(res, 404, { error: "not found" });
114
+ }
115
+
116
+ function handleGroups(res, method, parts, body) {
117
+ const [, id, sub, sessionId] = parts;
118
+ if (!id && method === "GET") return send(res, 200, groups.listGroups());
119
+ if (!id && method === "POST") return send(res, 200, groups.createGroup(body));
120
+ if (id && !sub && method === "PUT") return send(res, 200, groups.updateGroup(id, body));
121
+ if (id && !sub && method === "DELETE") {
122
+ groups.deleteGroup(id);
123
+ return send(res, 200, { ok: true });
124
+ }
125
+ if (sub === "sessions" && !sessionId && method === "POST") return send(res, 200, groups.addSession(id, body.sessionId, body.title));
126
+ if (sub === "sessions" && sessionId && method === "DELETE") return send(res, 200, groups.removeSession(id, sessionId));
127
+ return send(res, 404, { error: "not found" });
128
+ }
129
+
130
+ /** Creates the request handler plus file watchers that broadcast `ws:data`, `ws:sessions`, `ws:groups` and `ws:chat` events. */
131
+ export function createApi(broadcast) {
132
+ fs.mkdirSync(store.TASKS_DIR, { recursive: true });
133
+
134
+ let timer;
135
+ const pending = new Set();
136
+ fs.watch(store.TASKS_DIR, { recursive: true }, (_event, filename) => {
137
+ if (!filename || filename.endsWith(".tmp")) return;
138
+ pending.add(filename.split(path.sep)[0]);
139
+ clearTimeout(timer);
140
+ timer = setTimeout(() => {
141
+ broadcast("ws:data", { tasks: [...pending] });
142
+ pending.clear();
143
+ }, 150);
144
+ });
145
+
146
+ let sessionTimer;
147
+ const changedSessions = new Set();
148
+ if (fs.existsSync(sessions.PROJECTS_DIR)) {
149
+ fs.watch(sessions.PROJECTS_DIR, { recursive: true }, (_event, filename) => {
150
+ if (!filename?.endsWith(".jsonl")) return;
151
+ const id = sessions.sessionIdFromPath(filename);
152
+ if (!id) return;
153
+ changedSessions.add(id);
154
+ clearTimeout(sessionTimer);
155
+ sessionTimer = setTimeout(() => {
156
+ broadcast("ws:sessions", { ids: [...changedSessions] });
157
+ changedSessions.clear();
158
+ }, 300);
159
+ });
160
+ }
161
+
162
+ let groupsTimer;
163
+ fs.watch(store.DATA_DIR, (_event, filename) => {
164
+ if (filename !== path.basename(groups.GROUPS_FILE)) return;
165
+ clearTimeout(groupsTimer);
166
+ groupsTimer = setTimeout(() => broadcast("ws:groups", {}), 100);
167
+ });
168
+
169
+ chats.onChatChange((sessionId) => broadcast("ws:chat", { sessionId, state: chats.chatState(sessionId) }));
170
+
171
+ return async function handle(req, res, next) {
172
+ if (!isTrusted(req)) return send(res, 403, { error: "forbidden" });
173
+ if (req.url.startsWith("/files/")) {
174
+ const rel = decodeURIComponent(req.url.replace(/^\/files\//, "").split("?")[0]);
175
+ return serveStatic(res, store.TASKS_DIR, rel) || next();
176
+ }
177
+ if (!req.url.startsWith("/api/")) return next();
178
+
179
+ const url = new URL(req.url, "http://localhost");
180
+ const parts = url.pathname.split("/").filter(Boolean).slice(1).map(decodeURIComponent);
181
+ const method = req.method;
182
+ try {
183
+ const body = ["PUT", "POST"].includes(method) ? JSON.parse((await readBody(req)) || "{}") : undefined;
184
+ const [resource, id, sub, subId] = parts;
185
+
186
+ if (resource === "health") return send(res, 200, { app: "mono", ok: true });
187
+ if (resource === "chats") return await handleChats(res, method, parts, body);
188
+ if (resource === "groups") return handleGroups(res, method, parts, body);
189
+ if (resource === "models" && method === "GET") return send(res, 200, chats.listModels());
190
+ if (resource === "commands" && method === "GET") return send(res, 200, listCommands(url.searchParams.get("cwd") ? chats.expandHome(url.searchParams.get("cwd")) : null));
191
+ if (resource === "uploads" && method === "POST") return send(res, 200, chats.saveUpload(body));
192
+ if (resource === "uploads" && method === "GET" && id === "file") {
193
+ const file = path.resolve(url.searchParams.get("path") ?? "");
194
+ return file.startsWith(chats.UPLOADS_DIR + path.sep) && serveStatic(res, path.dirname(file), path.basename(file)) ? undefined : send(res, 404, { error: "not found" });
195
+ }
196
+ if (resource === "projects") {
197
+ if (!id && method === "GET") return send(res, 200, projects.listProjects({ fresh: url.searchParams.has("fresh") }).map(({ favicon, ...p }) => ({ ...p, hasIcon: Boolean(favicon) })));
198
+ if (id && sub === "icon" && method === "GET") {
199
+ const icon = projects.projectIcon(id);
200
+ return icon && serveStatic(res, path.dirname(icon), path.basename(icon)) ? undefined : send(res, 404, { error: "not found" });
201
+ }
202
+ if (id && !sub && method === "PUT") return send(res, 200, projects.saveOverride(id, { title: body.title || undefined, color: body.color || undefined }));
203
+ if (id && sub === "worktrees" && method === "GET") {
204
+ return projects.listProjects().some((p) => p.id === id) ? send(res, 200, chats.worktreesForProject(id)) : send(res, 404, { error: "not found" });
205
+ }
206
+ return send(res, 404, { error: "not found" });
207
+ }
208
+ if (resource === "sessions") {
209
+ if (!id) return send(res, 200, sessionList(Number(url.searchParams.get("days") ?? 3)));
210
+ const since = url.searchParams.get("since");
211
+ const detail = sessions.readSessionEvents(id, { since: since === null ? undefined : Number(since), agent: url.searchParams.get("agent") ?? undefined });
212
+ return detail ? send(res, 200, detail) : send(res, 404, { error: "not found" });
213
+ }
214
+ if (resource !== "tasks") return send(res, 404, { error: "not found" });
215
+
216
+ if (id && sub === "worktrees" && method === "GET") {
217
+ const task = store.getTask(id);
218
+ const roots = projects.listProjects().filter((p) => projects.projectsForTask(task ?? {}).includes(p.id)).map((p) => p.root);
219
+ return send(res, 200, chats.worktreesForTask(task, roots));
220
+ }
221
+ if (id && sub === "refresh") {
222
+ if (method === "POST") return send(res, 200, startRefresh(id));
223
+ if (method === "GET") return send(res, 200, refreshStatus(id));
224
+ }
225
+
226
+ if (!id) {
227
+ if (method === "GET") return send(res, 200, store.listTasks().map(withProjects));
228
+ if (method === "POST") {
229
+ store.assertSafe(body.id);
230
+ return send(res, 200, store.saveTask(body.id, body));
231
+ }
232
+ }
233
+
234
+ if (id && !sub) {
235
+ if (method === "GET") {
236
+ const task = store.getTask(id);
237
+ return task ? send(res, 200, withProjects(task)) : send(res, 404, { error: "not found" });
238
+ }
239
+ if (method === "PUT") return send(res, 200, withProjects(store.saveTask(id, body)));
240
+ if (method === "DELETE") {
241
+ store.trashTask(id);
242
+ return send(res, 200, { ok: true });
243
+ }
244
+ }
245
+
246
+ if (sub === "boards" && subId && parts[4] === "undo" && method === "POST") {
247
+ const restored = store.restoreBoard(id, subId);
248
+ return send(res, 200, { restored, remaining: store.boardHistory(id, subId).length });
249
+ }
250
+ if (sub === "boards" && subId && parts[4] === "history" && method === "GET") return send(res, 200, store.boardHistory(id, subId));
251
+ if (sub === "boards" && subId) {
252
+ if (method === "GET") {
253
+ const board = store.getBoard(id, subId);
254
+ return board ? send(res, 200, board) : send(res, 404, { error: "not found" });
255
+ }
256
+ if (method === "PUT") return send(res, 200, store.saveBoard(id, subId, body));
257
+ if (method === "DELETE") {
258
+ store.deleteBoard(id, subId);
259
+ return send(res, 200, { ok: true });
260
+ }
261
+ }
262
+
263
+ if (sub === "docs" && subId) {
264
+ if (method === "GET") {
265
+ const doc = store.getDoc(id, subId);
266
+ return doc === null ? send(res, 404, { error: "not found" }) : send(res, 200, { name: subId, content: doc });
267
+ }
268
+ if (method === "PUT") {
269
+ store.saveDoc(id, subId, body.content ?? "");
270
+ return send(res, 200, { ok: true });
271
+ }
272
+ }
273
+
274
+ return send(res, 404, { error: "not found" });
275
+ } catch (error) {
276
+ return send(res, 400, { error: String(error.message ?? error) });
277
+ }
278
+ };
279
+ }