@zhihaohong52/sonata 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +437 -0
  3. package/dist/adapters/claude.js +83 -0
  4. package/dist/adapters/claude.js.map +1 -0
  5. package/dist/adapters/codex.js +382 -0
  6. package/dist/adapters/codex.js.map +1 -0
  7. package/dist/adapters/index.js +21 -0
  8. package/dist/adapters/index.js.map +1 -0
  9. package/dist/adapters/opencode.js +104 -0
  10. package/dist/adapters/opencode.js.map +1 -0
  11. package/dist/adapters/pi.js +152 -0
  12. package/dist/adapters/pi.js.map +1 -0
  13. package/dist/adapters/reasonix.js +358 -0
  14. package/dist/adapters/reasonix.js.map +1 -0
  15. package/dist/adapters/types.js +2 -0
  16. package/dist/adapters/types.js.map +1 -0
  17. package/dist/agent-markers.js +3 -0
  18. package/dist/agent-markers.js.map +1 -0
  19. package/dist/aipricing.js +109 -0
  20. package/dist/aipricing.js.map +1 -0
  21. package/dist/catalog.js +274 -0
  22. package/dist/catalog.js.map +1 -0
  23. package/dist/cli.js +766 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/commands/approve.js +23 -0
  26. package/dist/commands/approve.js.map +1 -0
  27. package/dist/commands/auth.js +41 -0
  28. package/dist/commands/auth.js.map +1 -0
  29. package/dist/commands/catalog.js +172 -0
  30. package/dist/commands/catalog.js.map +1 -0
  31. package/dist/commands/code.js +113 -0
  32. package/dist/commands/code.js.map +1 -0
  33. package/dist/commands/dispatch.js +148 -0
  34. package/dist/commands/dispatch.js.map +1 -0
  35. package/dist/commands/doctor.js +545 -0
  36. package/dist/commands/doctor.js.map +1 -0
  37. package/dist/commands/gc.js +25 -0
  38. package/dist/commands/gc.js.map +1 -0
  39. package/dist/commands/init-log.js +83 -0
  40. package/dist/commands/init-log.js.map +1 -0
  41. package/dist/commands/init.js +157 -0
  42. package/dist/commands/init.js.map +1 -0
  43. package/dist/commands/log.js +24 -0
  44. package/dist/commands/log.js.map +1 -0
  45. package/dist/commands/route.js +660 -0
  46. package/dist/commands/route.js.map +1 -0
  47. package/dist/commands/run.js +199 -0
  48. package/dist/commands/run.js.map +1 -0
  49. package/dist/commands/runs.js +37 -0
  50. package/dist/commands/runs.js.map +1 -0
  51. package/dist/commands/serve.js +818 -0
  52. package/dist/commands/serve.js.map +1 -0
  53. package/dist/commands/status.js +14 -0
  54. package/dist/commands/status.js.map +1 -0
  55. package/dist/commands/sync.js +258 -0
  56. package/dist/commands/sync.js.map +1 -0
  57. package/dist/commands/tail.js +229 -0
  58. package/dist/commands/tail.js.map +1 -0
  59. package/dist/commands/usage.js +85 -0
  60. package/dist/commands/usage.js.map +1 -0
  61. package/dist/commands/verify.js +69 -0
  62. package/dist/commands/verify.js.map +1 -0
  63. package/dist/commands/wait.js +38 -0
  64. package/dist/commands/wait.js.map +1 -0
  65. package/dist/config.js +588 -0
  66. package/dist/config.js.map +1 -0
  67. package/dist/detect.js +445 -0
  68. package/dist/detect.js.map +1 -0
  69. package/dist/filelock.js +83 -0
  70. package/dist/filelock.js.map +1 -0
  71. package/dist/init/apply.js +96 -0
  72. package/dist/init/apply.js.map +1 -0
  73. package/dist/init/candidates.js +115 -0
  74. package/dist/init/candidates.js.map +1 -0
  75. package/dist/init/discover.js +216 -0
  76. package/dist/init/discover.js.map +1 -0
  77. package/dist/init/helpers.js +429 -0
  78. package/dist/init/helpers.js.map +1 -0
  79. package/dist/init/interactive-state.js +128 -0
  80. package/dist/init/interactive-state.js.map +1 -0
  81. package/dist/init/plan.js +225 -0
  82. package/dist/init/plan.js.map +1 -0
  83. package/dist/init/scripted-state.js +130 -0
  84. package/dist/init/scripted-state.js.map +1 -0
  85. package/dist/init/toml.js +83 -0
  86. package/dist/init/toml.js.map +1 -0
  87. package/dist/init/validate.js +149 -0
  88. package/dist/init/validate.js.map +1 -0
  89. package/dist/ledger.js +123 -0
  90. package/dist/ledger.js.map +1 -0
  91. package/dist/mcp/protocol.js +64 -0
  92. package/dist/mcp/protocol.js.map +1 -0
  93. package/dist/mcp/server.js +96 -0
  94. package/dist/mcp/server.js.map +1 -0
  95. package/dist/mcp/tools.js +210 -0
  96. package/dist/mcp/tools.js.map +1 -0
  97. package/dist/mode.js +64 -0
  98. package/dist/mode.js.map +1 -0
  99. package/dist/native/claude-shim.js +228 -0
  100. package/dist/native/claude-shim.js.map +1 -0
  101. package/dist/native/codex-auth.js +177 -0
  102. package/dist/native/codex-auth.js.map +1 -0
  103. package/dist/native/copilot-auth.js +105 -0
  104. package/dist/native/copilot-auth.js.map +1 -0
  105. package/dist/native/credentials.js +83 -0
  106. package/dist/native/credentials.js.map +1 -0
  107. package/dist/native/hosts.js +60 -0
  108. package/dist/native/hosts.js.map +1 -0
  109. package/dist/native/litellm.js +103 -0
  110. package/dist/native/litellm.js.map +1 -0
  111. package/dist/native/models.js +96 -0
  112. package/dist/native/models.js.map +1 -0
  113. package/dist/native/oauth-login.js +112 -0
  114. package/dist/native/oauth-login.js.map +1 -0
  115. package/dist/native/router.js +469 -0
  116. package/dist/native/router.js.map +1 -0
  117. package/dist/native/tls.js +73 -0
  118. package/dist/native/tls.js.map +1 -0
  119. package/dist/native/usage.js +119 -0
  120. package/dist/native/usage.js.map +1 -0
  121. package/dist/normalize.js +98 -0
  122. package/dist/normalize.js.map +1 -0
  123. package/dist/pricing.js +86 -0
  124. package/dist/pricing.js.map +1 -0
  125. package/dist/roles.js +49 -0
  126. package/dist/roles.js.map +1 -0
  127. package/dist/sessions.js +62 -0
  128. package/dist/sessions.js.map +1 -0
  129. package/dist/settings.js +131 -0
  130. package/dist/settings.js.map +1 -0
  131. package/dist/store.js +70 -0
  132. package/dist/store.js.map +1 -0
  133. package/dist/tmux.js +89 -0
  134. package/dist/tmux.js.map +1 -0
  135. package/dist/tui-ink/app-state.js +340 -0
  136. package/dist/tui-ink/app-state.js.map +1 -0
  137. package/dist/tui-ink/app.js +156 -0
  138. package/dist/tui-ink/app.js.map +1 -0
  139. package/dist/tui-ink/components/byok-step.js +102 -0
  140. package/dist/tui-ink/components/byok-step.js.map +1 -0
  141. package/dist/tui-ink/components/login-screen.js +80 -0
  142. package/dist/tui-ink/components/login-screen.js.map +1 -0
  143. package/dist/tui-ink/components/models-step.js +92 -0
  144. package/dist/tui-ink/components/models-step.js.map +1 -0
  145. package/dist/tui-ink/components/multi-select-state.js +56 -0
  146. package/dist/tui-ink/components/multi-select-state.js.map +1 -0
  147. package/dist/tui-ink/components/multi-select.js +69 -0
  148. package/dist/tui-ink/components/multi-select.js.map +1 -0
  149. package/dist/tui-ink/components/providers-step.js +285 -0
  150. package/dist/tui-ink/components/providers-step.js.map +1 -0
  151. package/dist/tui-ink/components/ranked-select-state.js +41 -0
  152. package/dist/tui-ink/components/ranked-select-state.js.map +1 -0
  153. package/dist/tui-ink/components/ranked-select.js +61 -0
  154. package/dist/tui-ink/components/ranked-select.js.map +1 -0
  155. package/dist/tui-ink/components/search-select.js +47 -0
  156. package/dist/tui-ink/components/search-select.js.map +1 -0
  157. package/dist/tui-ink/components/text-input-state.js +42 -0
  158. package/dist/tui-ink/components/text-input-state.js.map +1 -0
  159. package/dist/tui-ink/components/text-input.js +48 -0
  160. package/dist/tui-ink/components/text-input.js.map +1 -0
  161. package/dist/tui-ink/run.js +38 -0
  162. package/dist/tui-ink/run.js.map +1 -0
  163. package/dist/tui-ink/test-compile.js +4 -0
  164. package/dist/tui-ink/test-compile.js.map +1 -0
  165. package/dist/tui-ink/types.js +2 -0
  166. package/dist/tui-ink/types.js.map +1 -0
  167. package/dist/tui.js +330 -0
  168. package/dist/tui.js.map +1 -0
  169. package/dist/types.js +2 -0
  170. package/dist/types.js.map +1 -0
  171. package/dist/watchdog.js +85 -0
  172. package/dist/watchdog.js.map +1 -0
  173. package/hooks/capture-mode.mjs +56 -0
  174. package/hooks/ensure-serve.mjs +140 -0
  175. package/hooks/hooks.json +12 -0
  176. package/hooks/route-session.mjs +47 -0
  177. package/hooks/route-subagent.mjs +54 -0
  178. package/package.json +64 -0
  179. package/roles/code.md +13 -0
  180. package/roles/explore.md +9 -0
  181. package/roles/plan.md +10 -0
  182. package/roles/review.md +11 -0
  183. package/skills/loop/SKILL.md +43 -0
package/dist/ledger.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The append-only record of what the router did.
3
+ *
4
+ * One JSON line per completed request, in a file per UTC day. Global rather
5
+ * than per-project because the router is a daemon shared across every project;
6
+ * per-project reporting comes from the session map in `src/sessions.ts`.
7
+ *
8
+ * Daily files exist so retention is a file deletion rather than a rewrite.
9
+ * "opencode's event table grows without bound — 6.5 GB across 140k rows" is
10
+ * already a documented limitation caused by another tool doing this
11
+ * carelessly, and sonata does not get to repeat it in its own store.
12
+ */
13
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ export const LEDGER_RETENTION_DAYS = 30;
16
+ const FILE_PATTERN = /^(\d{4}-\d{2}-\d{2})\.jsonl$/;
17
+ export function ledgerDir(home) {
18
+ return join(home, '.config', 'sonata', 'usage');
19
+ }
20
+ /** UTC date, never local: a local date would roll the file over at the wrong moment. */
21
+ export function ledgerPathFor(home, at) {
22
+ return join(ledgerDir(home), `${at.toISOString().slice(0, 10)}.jsonl`);
23
+ }
24
+ export function appendRow(home, row) {
25
+ const at = new Date(row.ts);
26
+ mkdirSync(ledgerDir(home), { recursive: true });
27
+ // O_APPEND plus one write keeps concurrent appends as whole lines.
28
+ appendFileSync(ledgerPathFor(home, at), `${JSON.stringify(row)}\n`);
29
+ }
30
+ export function readRows(home, sinceMs, now = Date.now()) {
31
+ const dir = ledgerDir(home);
32
+ if (!existsSync(dir))
33
+ return [];
34
+ const out = [];
35
+ for (const name of readdirSync(dir).sort()) {
36
+ if (!FILE_PATTERN.test(name))
37
+ continue;
38
+ let raw;
39
+ try {
40
+ raw = readFileSync(join(dir, name), 'utf8');
41
+ }
42
+ catch {
43
+ continue;
44
+ }
45
+ for (const line of raw.split('\n')) {
46
+ if (line === '')
47
+ continue;
48
+ let row;
49
+ try {
50
+ const parsed = JSON.parse(line);
51
+ if (parsed === null || typeof parsed !== 'object' || typeof parsed.ts !== 'string')
52
+ continue;
53
+ row = parsed;
54
+ if (!Number.isFinite(Date.parse(row.ts)))
55
+ continue;
56
+ if (!hasRequiredFields(row))
57
+ continue;
58
+ }
59
+ catch {
60
+ // A torn final line (a crash mid-append) must not cost the whole report.
61
+ continue;
62
+ }
63
+ const ts = Date.parse(row.ts);
64
+ if (!Number.isFinite(ts) || ts < sinceMs || ts > now)
65
+ continue;
66
+ out.push(row);
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ function priceIsValid(price) {
72
+ if (price === null || typeof price !== 'object' || Array.isArray(price))
73
+ return false;
74
+ if (price.source === 'none')
75
+ return true;
76
+ if (price.source === 'model' || price.source === 'gateway' || price.source === 'ai-pricing') {
77
+ return typeof price.totalUsd === 'number' && Number.isFinite(price.totalUsd);
78
+ }
79
+ return false;
80
+ }
81
+ /**
82
+ * A persisted row is untrusted input. Every downstream reader (`aggregate`,
83
+ * `recentRoutes`) reaches into `tokens.input`/`tokens.output`, `attempts.length`
84
+ * and `price.source`/`price.totalUsd`, so a parseable-but-incomplete row must be
85
+ * dropped here rather than crash those readers later. `aggregate` dereferences
86
+ * `price.source` directly, so a `null` or otherwise malformed price must be
87
+ * rejected too, not just an absent `tokens`/`attempts`.
88
+ */
89
+ function hasRequiredFields(row) {
90
+ const tokens = row.tokens;
91
+ if (tokens === null || typeof tokens !== 'object' || Array.isArray(tokens))
92
+ return false;
93
+ if (typeof tokens.input !== 'number' || !Number.isFinite(tokens.input))
94
+ return false;
95
+ if (typeof tokens.output !== 'number' || !Number.isFinite(tokens.output))
96
+ return false;
97
+ if (!priceIsValid(row.price))
98
+ return false;
99
+ return Array.isArray(row.attempts);
100
+ }
101
+ /** Deletes whole day-files older than the window. Returns how many were removed. */
102
+ export function pruneLedger(home, retentionDays, now = new Date()) {
103
+ const dir = ledgerDir(home);
104
+ if (!existsSync(dir))
105
+ return 0;
106
+ const cutoff = Math.floor((now.getTime() - retentionDays * 24 * 3600 * 1000) / (24 * 3600 * 1000)) * (24 * 3600 * 1000);
107
+ let removed = 0;
108
+ for (const name of readdirSync(dir)) {
109
+ const match = FILE_PATTERN.exec(name);
110
+ if (match === null)
111
+ continue; // never delete a file this module did not write
112
+ const day = Date.parse(`${match[1]}T00:00:00.000Z`);
113
+ if (!Number.isFinite(day) || day >= cutoff)
114
+ continue;
115
+ try {
116
+ rmSync(join(dir, name), { force: true });
117
+ removed += 1;
118
+ }
119
+ catch { /* a file we cannot remove is not worth failing serve over */ }
120
+ }
121
+ return removed;
122
+ }
123
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.js","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The MCP surface sonata needs, as a pure function.
3
+ *
4
+ * MCP is JSON-RPC 2.0 over newline-delimited stdio. Only four messages matter
5
+ * here, so the protocol lives in one testable function and the transport is a
6
+ * thin loop around it — the same split as parseKey/runList in tui.ts.
7
+ */
8
+ /**
9
+ * Used only when a client omits its version; the real one is echoed back.
10
+ * Captured from Claude Code 2.1.228 on 2026-08-12 — see
11
+ * tests/fixtures/mcp-handshake.jsonl. It was written as 2025-06-18 from
12
+ * documentation, which the capture disproved.
13
+ */
14
+ const FALLBACK_PROTOCOL_VERSION = '2025-11-25';
15
+ export async function handle(req, deps, onLines) {
16
+ // Notifications have no id and take no response.
17
+ if (req.id === undefined)
18
+ return null;
19
+ const id = req.id;
20
+ switch (req.method) {
21
+ case 'initialize':
22
+ return {
23
+ jsonrpc: '2.0',
24
+ id,
25
+ result: {
26
+ protocolVersion: req.params?.protocolVersion ?? FALLBACK_PROTOCOL_VERSION,
27
+ capabilities: { tools: {} },
28
+ serverInfo: { name: 'sonata', version: '0.0.1' },
29
+ },
30
+ };
31
+ case 'tools/list':
32
+ return { jsonrpc: '2.0', id, result: { tools: deps.tools } };
33
+ case 'tools/call': {
34
+ const name = req.params?.name;
35
+ const args = req.params?.arguments ?? {};
36
+ try {
37
+ if (!deps.tools.some((t) => t.name === name)) {
38
+ throw new Error(`unknown tool "${name}"`);
39
+ }
40
+ const text = await deps.call(name, args, onLines);
41
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }] } };
42
+ }
43
+ catch (err) {
44
+ // A refused dispatch must reach the wrapper as text it can relay,
45
+ // never as a dropped call it reports nothing about.
46
+ return {
47
+ jsonrpc: '2.0',
48
+ id,
49
+ result: {
50
+ content: [{ type: 'text', text: err.message }],
51
+ isError: true,
52
+ },
53
+ };
54
+ }
55
+ }
56
+ default:
57
+ return {
58
+ jsonrpc: '2.0',
59
+ id,
60
+ error: { code: -32601, message: `method not found: ${req.method}` },
61
+ };
62
+ }
63
+ }
64
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/mcp/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA6BH;;;;;GAKG;AACH,MAAM,yBAAyB,GAAG,YAAY,CAAC;AAE/C,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,GAAmB,EACnB,IAAU,EACV,OAAmC;IAEnC,iDAAiD;IACjD,IAAI,GAAG,CAAC,EAAE,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,CAAC;IAElB,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC;QACnB,KAAK,YAAY;YACf,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,EAAE;gBACF,MAAM,EAAE;oBACN,eAAe,EAAG,GAAG,CAAC,MAAM,EAAE,eAA0B,IAAI,yBAAyB;oBACrF,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;oBAC3B,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE;iBACjD;aACF,CAAC;QAEJ,KAAK,YAAY;YACf,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;QAE/D,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,IAAc,CAAC;YACxC,MAAM,IAAI,GAAI,GAAG,CAAC,MAAM,EAAE,SAAqC,IAAI,EAAE,CAAC;YACtE,IAAI,CAAC;gBACH,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;oBAC7C,MAAM,IAAI,KAAK,CAAC,iBAAiB,IAAI,GAAG,CAAC,CAAC;gBAC5C,CAAC;gBACD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;gBAClD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/E,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,kEAAkE;gBAClE,oDAAoD;gBACpD,OAAO;oBACL,OAAO,EAAE,KAAK;oBACd,EAAE;oBACF,MAAM,EAAE;wBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC;wBACzD,OAAO,EAAE,IAAI;qBACd;iBACF,CAAC;YACJ,CAAC;QACH,CAAC;QAED;YACE,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,EAAE;gBACF,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,qBAAqB,GAAG,CAAC,MAAM,EAAE,EAAE;aACpE,CAAC;IACN,CAAC;AACH,CAAC"}
@@ -0,0 +1,96 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { appendFileSync } from 'node:fs';
3
+ import { handle } from './protocol.js';
4
+ import { TOOL_DEFS, callTool } from './tools.js';
5
+ /**
6
+ * Records every incoming request verbatim when `SONATA_MCP_DEBUG` names a file.
7
+ *
8
+ * The client's own framing is otherwise invisible: sonata never sees whether
9
+ * Claude Code supplies a `_meta.progressToken`, which is what decides whether
10
+ * the server may push `notifications/progress` at all. Off unless the variable
11
+ * is set, and it never touches stdout — anything written there is protocol.
12
+ */
13
+ function debugLog(line) {
14
+ const path = process.env.SONATA_MCP_DEBUG;
15
+ if (!path)
16
+ return;
17
+ try {
18
+ appendFileSync(path, `${new Date().toISOString()} ${line}\n`);
19
+ }
20
+ catch {
21
+ // Debug logging must never take the session's dispatch capability down.
22
+ }
23
+ }
24
+ /**
25
+ * The stdio loop. Input is injected so the whole server is testable without a
26
+ * process, which is the same seam readKeys uses for stdin in tui.ts.
27
+ */
28
+ export async function serveMcp(input, write, env) {
29
+ for await (const raw of input) {
30
+ const line = raw.trim();
31
+ if (line.length === 0)
32
+ continue;
33
+ debugLog(line);
34
+ let req;
35
+ try {
36
+ req = JSON.parse(line);
37
+ }
38
+ catch {
39
+ // Never die on bad input: the session loses dispatch entirely.
40
+ write(JSON.stringify({
41
+ jsonrpc: '2.0', id: null,
42
+ error: { code: -32700, message: 'parse error' },
43
+ }));
44
+ continue;
45
+ }
46
+ // JSON.parse accepts `null`, `5`, `"x"` and `[1]`. Reading .id off any of
47
+ // them threw, which killed the loop — and a dead server takes the
48
+ // session's dispatch capability with it.
49
+ if (typeof req !== 'object' || req === null || Array.isArray(req)) {
50
+ write(JSON.stringify({
51
+ jsonrpc: '2.0', id: null,
52
+ error: { code: -32600, message: 'invalid request' },
53
+ }));
54
+ continue;
55
+ }
56
+ const token = req.method === 'tools/call'
57
+ && req.params?._meta
58
+ && typeof req.params._meta === 'object'
59
+ ? req.params._meta.progressToken
60
+ : undefined;
61
+ let progress = 0;
62
+ const onLines = typeof token === 'string' || typeof token === 'number'
63
+ ? (lines) => {
64
+ // One line per notification, deliberately.
65
+ //
66
+ // Sending a rolling window instead was tried and measured worse
67
+ // (2026-08-19, Claude Code 2.1.233): the client flattens the newlines,
68
+ // then clips the message head-first at roughly two wrapped rows. The
69
+ // newest line sits at the end of a window, so it is precisely what the
70
+ // clip removes — a screenshot showed four old commands and the live one
71
+ // truncated to `$ nod…`. One line always fits, and is always the line
72
+ // the run is on. History belongs in the transcript, not in a display
73
+ // that keeps only its head.
74
+ for (const message of lines) {
75
+ write(JSON.stringify({
76
+ jsonrpc: '2.0',
77
+ method: 'notifications/progress',
78
+ params: { progressToken: token, progress: ++progress, message },
79
+ }));
80
+ }
81
+ }
82
+ : undefined;
83
+ const res = await handle(req, {
84
+ tools: TOOL_DEFS,
85
+ call: (name, args, onLines) => callTool(name, args, env, onLines),
86
+ }, onLines);
87
+ if (res !== null)
88
+ write(JSON.stringify(res));
89
+ }
90
+ }
91
+ /** Wires the loop to the real stdio streams. */
92
+ export async function runMcpStdio(env) {
93
+ const rl = createInterface({ input: process.stdin });
94
+ await serveMcp(rl, (l) => process.stdout.write(`${l}\n`), env);
95
+ }
96
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,210 @@
1
+ import { writeFileSync, statSync, existsSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { join, resolve } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { cmdRun } from '../commands/run.js';
6
+ import { cmdWait } from '../commands/wait.js';
7
+ import { cmdApprove } from '../commands/approve.js';
8
+ import { readEvents } from '../store.js';
9
+ /**
10
+ * Ceiling on a report returned through MCP.
11
+ *
12
+ * Claude Code warns above 10k tokens of tool output and caps at 25k, and a
13
+ * result over its persist-to-disk threshold is replaced in the conversation by
14
+ * a file reference — which stops the report being the wrapper's final message,
15
+ * the one thing the orchestrator actually reads. 40k characters is roughly 10k
16
+ * tokens, so an ordinary report stays inline and under the warning.
17
+ */
18
+ export const MAX_REPORT_CHARS = 40_000;
19
+ /** Keeps the head of an oversized report and says where the whole thing is. */
20
+ export function truncateReport(report, id, max = MAX_REPORT_CHARS) {
21
+ if (report.length <= max)
22
+ return report;
23
+ return `${report.slice(0, max)}\n\n[truncated: full transcript at \`sonata log ${id}\`]`;
24
+ }
25
+ const MAX_RESULT_SIZE_CHARS = 200_000;
26
+ const REPORT_META = { 'anthropic/maxResultSizeChars': MAX_RESULT_SIZE_CHARS };
27
+ export const TOOL_DEFS = [
28
+ {
29
+ name: 'dispatch',
30
+ description: 'Run a task on a foreign model and return its report. Blocks until the run ' +
31
+ 'finishes, needs an approval, or stalls — so one call is usually the whole ' +
32
+ 'dispatch. Returns state DONE, PAUSED, STALLED or RUNNING.',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ role: { type: 'string', description: 'code | review | explore | plan' },
37
+ model: { type: 'string', description: 'a model key from sonata.toml' },
38
+ task: { type: 'string', description: 'The task text, verbatim and byte for byte. Never summarise, shorten, or rewrite it. If the caller gave you a file path instead, use task_file.' },
39
+ task_file: { type: 'string', description: 'Path to a file holding the task. Prefer this whenever the caller gives you one: a path cannot be paraphrased, and the model receives exactly what was written. Give either task or task_file, not both.' },
40
+ transcript: { type: 'boolean', description: 'Return the run\'s whole terminal transcript beside the report. Pass true when the caller asks to read what the model did, turn by turn; leave it out otherwise, since a transcript is far larger than a report.' },
41
+ cwd: { type: 'string', description: 'optional existing directory in which to launch the run' },
42
+ },
43
+ required: ['role', 'model'],
44
+ },
45
+ _meta: REPORT_META,
46
+ },
47
+ {
48
+ name: 'wait',
49
+ description: 'Resume waiting on a run already launched — after answering a PAUSED prompt, ' +
50
+ 'or when a previous call returned RUNNING. Same states as dispatch.',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ id: { type: 'string', description: 'the run id' },
55
+ cwd: { type: 'string', description: 'the same directory returned by dispatch' },
56
+ transcript: { type: 'boolean', description: 'Return the run\'s whole terminal transcript beside the report.' },
57
+ },
58
+ required: ['id'],
59
+ },
60
+ _meta: REPORT_META,
61
+ },
62
+ {
63
+ name: 'approve',
64
+ description: 'Answer a run that is PAUSED awaiting approval.',
65
+ inputSchema: {
66
+ type: 'object',
67
+ properties: {
68
+ id: { type: 'string' },
69
+ answer: { type: 'string', description: 'yes or no' },
70
+ cwd: { type: 'string', description: 'the same directory returned by dispatch' },
71
+ },
72
+ required: ['id', 'answer'],
73
+ },
74
+ },
75
+ ];
76
+ /** Trims the report in a result, leaving every other field alone. */
77
+ function withTrimmedReport(result) {
78
+ return result.report === undefined
79
+ ? result
80
+ : { ...result, report: truncateReport(result.report, result.id) };
81
+ }
82
+ /** The result payload, carrying the transcript only when it was asked for. */
83
+ function withTranscript(result, cwd, args) {
84
+ const payload = { ...result, cwd };
85
+ if (args.transcript !== true)
86
+ return payload;
87
+ const transcript = transcriptFor(cwd, result.id, (result.report ?? '').length);
88
+ if (transcript !== undefined)
89
+ payload.transcript = transcript;
90
+ return payload;
91
+ }
92
+ /**
93
+ * The run's whole pane transcript, for a caller who asked to read it.
94
+ *
95
+ * The events file has held every line all along — this is `sonata log`'s
96
+ * content delivered through MCP, so a wrapper can show its caller what the
97
+ * foreign model actually did rather than only what it concluded.
98
+ *
99
+ * Budgeted against the report beside it, because the two share one result and
100
+ * the report is the part that must never be pushed out. Head rather than tail:
101
+ * the end of a run is what the report already describes, so what is missing is
102
+ * how it got there.
103
+ */
104
+ export function transcriptFor(cwd, id, reportChars, max = MAX_REPORT_CHARS) {
105
+ const lines = readEvents(cwd, id);
106
+ if (lines.length === 0)
107
+ return undefined;
108
+ const budget = max - reportChars;
109
+ if (budget <= 0)
110
+ return `[omitted: the report used the whole result — \`sonata log ${id}\`]`;
111
+ const text = lines.join('\n');
112
+ return text.length <= budget
113
+ ? text
114
+ : `${text.slice(0, budget)}\n\n[truncated: whole transcript at \`sonata log ${id}\`]`;
115
+ }
116
+ /**
117
+ * Where the task text comes from: an inline string, or a file the caller wrote.
118
+ *
119
+ * `task_file` exists because the wrapper agent is a small model relaying
120
+ * arguments, and it paraphrases — a ~3K step-by-step spec once reached the
121
+ * harness as a one-line summary. A path cannot be paraphrased: it either
122
+ * arrives intact or the dispatch fails loudly. Prose asking a model not to
123
+ * summarise is a request; this is a mechanism.
124
+ *
125
+ * Exactly one of the two, because silently preferring one when both are given
126
+ * would let a paraphrased `task` win over the file the caller meant.
127
+ */
128
+ export function resolveTaskFile(args, cwd) {
129
+ const inline = typeof args.task === 'string' && args.task.length > 0;
130
+ const path = typeof args.task_file === 'string' && args.task_file.length > 0;
131
+ if (inline && path) {
132
+ throw new Error('sonata: give either "task" or "task_file", not both');
133
+ }
134
+ if (path) {
135
+ const resolved = resolve(cwd, args.task_file);
136
+ if (!existsSync(resolved)) {
137
+ throw new Error(`sonata: task_file "${args.task_file}" does not exist`);
138
+ }
139
+ // Copied rather than read-and-rewritten so the bytes the caller wrote are
140
+ // the bytes composeInstructions receives.
141
+ return resolved;
142
+ }
143
+ if (!inline) {
144
+ throw new Error('sonata: the "task" or "task_file" argument is required');
145
+ }
146
+ const tmp = join(tmpdir(), `sonata-task-${Date.now()}-${randomUUID().slice(0, 8)}.md`);
147
+ writeFileSync(tmp, args.task);
148
+ return tmp;
149
+ }
150
+ function need(args, key) {
151
+ const value = args[key];
152
+ if (typeof value !== 'string' || value.length === 0) {
153
+ throw new Error(`sonata: the "${key}" argument is required`);
154
+ }
155
+ return value;
156
+ }
157
+ export function resolveToolCwd(args, env) {
158
+ if (args.cwd === undefined)
159
+ return env.cwd;
160
+ if (typeof args.cwd !== 'string' || args.cwd.length === 0) {
161
+ throw new Error('sonata: the "cwd" argument must be a non-empty directory path');
162
+ }
163
+ const cwd = resolve(env.cwd, args.cwd);
164
+ try {
165
+ if (!statSync(cwd).isDirectory())
166
+ throw new Error('not a directory');
167
+ }
168
+ catch {
169
+ throw new Error(`sonata: requested cwd "${args.cwd}" does not exist or is not a directory`);
170
+ }
171
+ return cwd;
172
+ }
173
+ export async function callTool(name, args, env, onLines) {
174
+ switch (name) {
175
+ case 'dispatch': {
176
+ const role = need(args, 'role');
177
+ const model = need(args, 'model');
178
+ const cwd = resolveToolCwd(args, env);
179
+ const taskFile = resolveTaskFile(args, cwd);
180
+ const started = await (env.run ?? cmdRun)({
181
+ cwd,
182
+ role,
183
+ model,
184
+ taskFile,
185
+ rolesDir: env.rolesDir,
186
+ sessionId: env.sessionId,
187
+ });
188
+ const result = await (env.wait ?? cmdWait)({ cwd, id: started.id, onLines });
189
+ return JSON.stringify(withTranscript(withTrimmedReport(result), cwd, args));
190
+ }
191
+ case 'wait': {
192
+ const cwd = resolveToolCwd(args, env);
193
+ const result = await (env.wait ?? cmdWait)({ cwd, id: need(args, 'id'), onLines });
194
+ return JSON.stringify(withTranscript(withTrimmedReport(result), cwd, args));
195
+ }
196
+ case 'approve': {
197
+ const answer = need(args, 'answer');
198
+ if (answer !== 'yes' && answer !== 'no') {
199
+ throw new Error('sonata: the "answer" argument must be yes or no');
200
+ }
201
+ await (env.approve ?? cmdApprove)({
202
+ cwd: resolveToolCwd(args, env), id: need(args, 'id'), yes: answer === 'yes',
203
+ });
204
+ return 'answered';
205
+ }
206
+ default:
207
+ throw new Error(`sonata: unknown tool "${name}"`);
208
+ }
209
+ }
210
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/mcp/tools.ts"],"names":[],"mappings":"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"}
package/dist/mode.js ADDED
@@ -0,0 +1,64 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const VALID = ['plan', 'default', 'acceptEdits', 'bypassPermissions'];
5
+ /**
6
+ * Claude Code modes that sonata does not model directly, mapped to the closest
7
+ * mode it can actually enforce on a foreign harness.
8
+ *
9
+ * `auto` is Claude Code's current default: it runs tool calls its classifier
10
+ * judges lower-risk without prompting, and blocks the rest. Sonata cannot
11
+ * reproduce that classifier in another harness, so the honest equivalent is
12
+ * `acceptEdits` — work proceeds without prompting the user, which is what the
13
+ * parent session is doing too.
14
+ *
15
+ * Treating it as `default` instead — which is what an unrecognised value used
16
+ * to do — is wrong in both directions: it claims the parent prompts for every
17
+ * action when it does not, and since opencode cannot honour `default` at all,
18
+ * it silently made every opencode dispatch refuse.
19
+ *
20
+ * The residual gap is real and worth stating: the foreign harness has no
21
+ * classifier, so it will run things auto mode would have blocked. Dispatch in
22
+ * `plan` for a read-only run when that matters.
23
+ */
24
+ const ALIASES = {
25
+ auto: 'acceptEdits',
26
+ };
27
+ /**
28
+ * Claude Code does not expose the permission mode as an environment variable.
29
+ * A PreToolUse hook writes it to .sonata/session-<id>.json; this reads it back.
30
+ * Falls back to the safest mode when unknown.
31
+ */
32
+ /**
33
+ * `home` mirrors `loadConfig`: it is optional so existing callers keep working,
34
+ * and always injected in tests, which must never read the real home directory.
35
+ *
36
+ * The two locations match where the hook writes. A project with its own sonata
37
+ * setup keeps its mode beside it; a project relying on the machine config keeps
38
+ * its mode beside that, because the hook must not scatter `.sonata/`
39
+ * directories across every repository on the machine.
40
+ */
41
+ export function readPermissionMode(cwd, sessionId, home = homedir()) {
42
+ if (!sessionId)
43
+ return 'default';
44
+ const candidates = [
45
+ join(cwd, '.sonata', `session-${sessionId}.json`),
46
+ join(home, '.config', 'sonata', `session-${sessionId}.json`),
47
+ ];
48
+ const path = candidates.find((p) => existsSync(p));
49
+ if (path === undefined)
50
+ return 'default';
51
+ try {
52
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
53
+ const raw = parsed.permissionMode;
54
+ if (!raw)
55
+ return 'default';
56
+ if (ALIASES[raw])
57
+ return ALIASES[raw];
58
+ return VALID.includes(raw) ? raw : 'default';
59
+ }
60
+ catch {
61
+ return 'default';
62
+ }
63
+ }
64
+ //# sourceMappingURL=mode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode.js","sourceRoot":"","sources":["../src/mode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,MAAM,KAAK,GAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,mBAAmB,CAAC,CAAC;AAExF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,GAAmC;IAC9C,IAAI,EAAE,aAAa;CACpB,CAAC;AAEF;;;;GAIG;AACH;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAW,EACX,SAA6B,EAC7B,OAAe,OAAO,EAAE;IAExB,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACjC,MAAM,UAAU,GAAG;QACjB,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,WAAW,SAAS,OAAO,CAAC;QACjD,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,SAAS,OAAO,CAAC;KAC7D,CAAC;IACF,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAgC,CAAC;QACrF,MAAM,GAAG,GAAG,MAAM,CAAC,cAAc,CAAC;QAClC,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAC3B,IAAI,OAAO,CAAC,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;QACtC,OAAO,KAAK,CAAC,QAAQ,CAAC,GAAqB,CAAC,CAAC,CAAC,CAAE,GAAsB,CAAC,CAAC,CAAC,SAAS,CAAC;IACrF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}