acuvo-code 0.2.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 (154) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/ENTERPRISE.md +927 -0
  3. package/LICENSE +120 -0
  4. package/README.md +1245 -0
  5. package/ROADMAP.md +556 -0
  6. package/bin/acuvo-mcp.mjs +208 -0
  7. package/bin/acuvo.mjs +3108 -0
  8. package/lib/acceptance-consent.mjs +168 -0
  9. package/lib/acceptance.mjs +859 -0
  10. package/lib/account.mjs +226 -0
  11. package/lib/acuvo-dir.mjs +72 -0
  12. package/lib/acuvo-models.mjs +141 -0
  13. package/lib/apply-patch.mjs +570 -0
  14. package/lib/ask-user.mjs +173 -0
  15. package/lib/audit.mjs +530 -0
  16. package/lib/auto-lease.mjs +174 -0
  17. package/lib/background.mjs +842 -0
  18. package/lib/best-of.mjs +334 -0
  19. package/lib/board.mjs +232 -0
  20. package/lib/breaker.mjs +93 -0
  21. package/lib/budget.mjs +1375 -0
  22. package/lib/builtin-skills.mjs +135 -0
  23. package/lib/cache-floor.mjs +204 -0
  24. package/lib/chain.mjs +303 -0
  25. package/lib/changed-paths.mjs +84 -0
  26. package/lib/chat.mjs +434 -0
  27. package/lib/checkpoint.mjs +637 -0
  28. package/lib/child-lifetime.mjs +71 -0
  29. package/lib/cli-args.mjs +1255 -0
  30. package/lib/code-review.mjs +1382 -0
  31. package/lib/colour.mjs +82 -0
  32. package/lib/command.mjs +2847 -0
  33. package/lib/compact.mjs +1151 -0
  34. package/lib/completion.mjs +515 -0
  35. package/lib/creative-engines.mjs +779 -0
  36. package/lib/db-inspect.mjs +1624 -0
  37. package/lib/delete.mjs +111 -0
  38. package/lib/design-loop.mjs +570 -0
  39. package/lib/diff-preview.mjs +1044 -0
  40. package/lib/doctor.mjs +2139 -0
  41. package/lib/dropped.mjs +216 -0
  42. package/lib/edit-diagnostics.mjs +277 -0
  43. package/lib/edit.mjs +460 -0
  44. package/lib/env-file.mjs +250 -0
  45. package/lib/escalate.mjs +702 -0
  46. package/lib/evaluate.mjs +284 -0
  47. package/lib/fetch-text.mjs +952 -0
  48. package/lib/fleet-budget.mjs +256 -0
  49. package/lib/gh.mjs +1536 -0
  50. package/lib/git.mjs +1341 -0
  51. package/lib/github.mjs +261 -0
  52. package/lib/h2.mjs +194 -0
  53. package/lib/handoff.mjs +417 -0
  54. package/lib/hooks.mjs +626 -0
  55. package/lib/http-probe.mjs +907 -0
  56. package/lib/image-director.mjs +322 -0
  57. package/lib/image-edit.mjs +522 -0
  58. package/lib/imagegen.mjs +998 -0
  59. package/lib/interrupt.mjs +234 -0
  60. package/lib/learned.mjs +353 -0
  61. package/lib/lease-watch.mjs +115 -0
  62. package/lib/lease.mjs +868 -0
  63. package/lib/localize.mjs +834 -0
  64. package/lib/log-tail.mjs +1052 -0
  65. package/lib/login.mjs +157 -0
  66. package/lib/lsp.mjs +1613 -0
  67. package/lib/mcp-consent.mjs +377 -0
  68. package/lib/mcp-defaults.mjs +780 -0
  69. package/lib/mcp-server.mjs +1343 -0
  70. package/lib/mcp.mjs +1263 -0
  71. package/lib/media.mjs +1283 -0
  72. package/lib/memory-workspace.mjs +179 -0
  73. package/lib/model-json.mjs +132 -0
  74. package/lib/model-tier.mjs +171 -0
  75. package/lib/model.mjs +1445 -0
  76. package/lib/parallel.mjs +144 -0
  77. package/lib/perchance.mjs +210 -0
  78. package/lib/plan-coherence.mjs +1461 -0
  79. package/lib/plan-ledger.mjs +981 -0
  80. package/lib/plan.mjs +461 -0
  81. package/lib/policy.mjs +783 -0
  82. package/lib/prefix-order.mjs +38 -0
  83. package/lib/project-memory.mjs +127 -0
  84. package/lib/prompt.mjs +109 -0
  85. package/lib/python.mjs +862 -0
  86. package/lib/rcfile.mjs +853 -0
  87. package/lib/read-window.mjs +743 -0
  88. package/lib/refute-tools.mjs +34 -0
  89. package/lib/refute.mjs +806 -0
  90. package/lib/repl-driver.mjs +264 -0
  91. package/lib/repl.mjs +324 -0
  92. package/lib/replay.mjs +1218 -0
  93. package/lib/repo-map.mjs +1101 -0
  94. package/lib/report.mjs +419 -0
  95. package/lib/search-rank.mjs +99 -0
  96. package/lib/search.mjs +659 -0
  97. package/lib/secret-paths.mjs +54 -0
  98. package/lib/session.mjs +1017 -0
  99. package/lib/skills.mjs +703 -0
  100. package/lib/slash.mjs +356 -0
  101. package/lib/spawn-argv.mjs +1151 -0
  102. package/lib/spend.mjs +250 -0
  103. package/lib/steer.mjs +280 -0
  104. package/lib/stream.mjs +253 -0
  105. package/lib/stuck.mjs +712 -0
  106. package/lib/subagent.mjs +749 -0
  107. package/lib/terminal-graphics.mjs +171 -0
  108. package/lib/tool-prefix.mjs +226 -0
  109. package/lib/tool-shortlist.mjs +162 -0
  110. package/lib/tools.mjs +2333 -0
  111. package/lib/tsserver.mjs +423 -0
  112. package/lib/turn.mjs +5672 -0
  113. package/lib/untrusted-block.mjs +271 -0
  114. package/lib/verify-claim.mjs +299 -0
  115. package/lib/vision.mjs +330 -0
  116. package/lib/voice-task.mjs +561 -0
  117. package/lib/warm-provider.mjs +255 -0
  118. package/lib/websearch.mjs +401 -0
  119. package/lib/workspace.mjs +928 -0
  120. package/lib/write-approval.mjs +235 -0
  121. package/lib/write-many.mjs +162 -0
  122. package/package.json +62 -0
  123. package/scripts/bundle.mjs +768 -0
  124. package/scripts/cache-floor.mjs +176 -0
  125. package/scripts/machine.mjs +226 -0
  126. package/scripts/test.mjs +139 -0
  127. package/skills/accessibility.md +87 -0
  128. package/skills/acuvo-design-system.md +123 -0
  129. package/skills/animation.md +84 -0
  130. package/skills/api-design.md +82 -0
  131. package/skills/auth-and-sessions.md +78 -0
  132. package/skills/build-with-a-framework.md +101 -0
  133. package/skills/colour-and-contrast.md +112 -0
  134. package/skills/creative-engines.md +81 -0
  135. package/skills/css-layout.md +85 -0
  136. package/skills/data-and-charts.md +77 -0
  137. package/skills/debugging.md +76 -0
  138. package/skills/designing-by-looking.md +84 -0
  139. package/skills/error-handling.md +78 -0
  140. package/skills/forms-and-validation.md +93 -0
  141. package/skills/nextjs-app-router.md +75 -0
  142. package/skills/page-composition.md +103 -0
  143. package/skills/performance.md +77 -0
  144. package/skills/plan-before-building.md +52 -0
  145. package/skills/planning-and-delegating.md +72 -0
  146. package/skills/refactoring.md +70 -0
  147. package/skills/security-basics.md +76 -0
  148. package/skills/state-management.md +73 -0
  149. package/skills/supabase-multitenant.md +72 -0
  150. package/skills/typescript-strict.md +90 -0
  151. package/skills/typography.md +135 -0
  152. package/skills/verify-your-own-work.md +62 -0
  153. package/skills/web-app-quality.md +62 -0
  154. package/skills/working-in-the-background.md +64 -0
package/lib/stream.mjs ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * ── ⭐⭐ THE TWENTY SECONDS OF NOTHING ────────────────────────────────────────
3
+ *
4
+ * The CLI prints a round header and then goes silent for the length of a model
5
+ * call — measured at 8–40s depending on the task. Nothing moves. There is no way
6
+ * to tell "thinking" from "hung", and the only honest thing a user can do is
7
+ * wait and hope.
8
+ *
9
+ * ⭐ FOR A TERMINAL TOOL, PERCEIVED SPEED IS THE PRODUCT. The critique of this
10
+ * whole category got that right: developers will not switch to save a fraction
11
+ * of a cent, and they absolutely will switch for something that feels alive.
12
+ * Nothing here makes the model faster. It makes the wait legible, which is the
13
+ * thing people actually experience.
14
+ *
15
+ * ── ⚠️ THE TRAP THAT MAKES STREAMING HARDER THAN IT LOOKS ───────────────────
16
+ * Content streams as simple text deltas. **Tool calls do not.** They arrive as
17
+ * fragments keyed by an index, split at arbitrary byte boundaries:
18
+ *
19
+ * {index:0, id:"call_1", function:{name:"write_file", arguments:""}}
20
+ * {index:0, function:{arguments:"{\\"pa"}}
21
+ * {index:0, function:{arguments:"th\\":\\"src/a.js\\""}}
22
+ *
23
+ * A naive reader that JSON.parses each fragment sees garbage; one that keeps
24
+ * only the last delta loses the name. They must be ACCUMULATED PER INDEX and
25
+ * parsed only at the end — and because the JSON is incomplete until the final
26
+ * fragment, nothing downstream may look at it early.
27
+ *
28
+ * ⚠️ AND A DELTA CAN SPLIT AN SSE FRAME. A chunk from the socket is not a line;
29
+ * `data: {"cho` can arrive with the rest in the next read. The buffer below is
30
+ * the whole reason this is a module and not four lines in `model.mjs`.
31
+ */
32
+
33
+ /**
34
+ * Parse a Server-Sent Events byte stream into JSON payloads.
35
+ *
36
+ * Yields each `data:` object. Ignores comments, blank lines and `[DONE]`.
37
+ *
38
+ * ⚠️ THE BUFFER IS THE POINT. Splitting each chunk on newlines independently
39
+ * drops any line straddling a chunk boundary — which under load is most of the
40
+ * interesting ones, and produces a stream that works perfectly on a fast
41
+ * connection and corrupts on a slow one.
42
+ */
43
+ export async function* parseSse(stream) {
44
+ const decoder = new TextDecoder();
45
+ let buffer = '';
46
+ for await (const chunk of stream) {
47
+ buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
48
+ let nl;
49
+ while ((nl = buffer.indexOf('\n')) !== -1) {
50
+ const line = buffer.slice(0, nl).trim();
51
+ buffer = buffer.slice(nl + 1);
52
+ if (!line || line.startsWith(':')) continue; // keep-alive comment
53
+ if (!line.startsWith('data:')) continue;
54
+ const payload = line.slice(5).trim();
55
+ if (payload === '[DONE]') return;
56
+ try {
57
+ yield JSON.parse(payload);
58
+ } catch {
59
+ /**
60
+ * ⚠️ A MALFORMED FRAME IS SKIPPED, NOT THROWN. One bad line must not
61
+ * destroy a response that is 90% delivered — the user would lose real
62
+ * work to a provider's formatting hiccup.
63
+ */
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Accumulate streamed deltas into the same shape `extractReply` produces, so
71
+ * everything downstream is unchanged.
72
+ *
73
+ * `onText` is called with each content fragment as it arrives — that callback is
74
+ * the entire user-visible payoff.
75
+ */
76
+ export async function collectStream(stream, { onText = null } = {}) {
77
+ let content = '';
78
+ /** @type {Map<number, {id: string|null, name: string, args: string}>} */
79
+ const calls = new Map();
80
+ let finishReason = null;
81
+ let usage = null;
82
+ /**
83
+ * ── ⭐⭐ WHICH OF THE 28 UPSTREAMS ANSWERED ──────────────────────────────────
84
+ *
85
+ * MEASURED 2026-08-14: `deepseek/deepseek-v4-flash-0731` is served by **28
86
+ * upstream endpoints** on OpenRouter, a prompt cache lives on ONE of them, and
87
+ * real 4-round runs came in at 46.7% and 48.6% hit rate unpinned against 95.8%
88
+ * pinned. The entire variance is WHICH instance served the round — and
89
+ * OpenRouter states it, as `provider`, on the response body AND on every SSE
90
+ * frame. Both readers dropped it, so a collapsed hit rate was recorded and not
91
+ * attributable.
92
+ *
93
+ * ⚠️ THIS IS THE PATH THAT ALWAYS RUNS. `turn.mjs` passes `onText` on every
94
+ * round, so the streaming branch is the CLI's only branch — capturing it only
95
+ * in `extractReply` would have instrumented the path nothing takes.
96
+ */
97
+ let provider = null;
98
+ let sawAnything = false;
99
+
100
+ for await (const evt of parseSse(stream)) {
101
+ sawAnything = true;
102
+ // Usage arrives on its own frame at the end when `usage.include` is set.
103
+ if (evt.usage) usage = evt.usage;
104
+ // ⚠️ FIRST FRAME WINS IS WRONG AND LAST FRAME WINS IS RIGHT-BY-ACCIDENT:
105
+ // every frame of one completion carries the same name, so this is simply
106
+ // "whatever the provider kept telling us", and a stream that never mentions
107
+ // it stays null — unknown, never guessed.
108
+ if (typeof evt.provider === 'string' && evt.provider) provider = evt.provider;
109
+ const choice = evt.choices?.[0];
110
+ if (!choice) continue;
111
+ if (choice.finish_reason) finishReason = choice.finish_reason;
112
+
113
+ const delta = choice.delta ?? {};
114
+ if (typeof delta.content === 'string' && delta.content.length > 0) {
115
+ content += delta.content;
116
+ if (onText) onText(delta.content);
117
+ }
118
+
119
+ for (const tc of delta.tool_calls ?? []) {
120
+ /**
121
+ * ⚠️ KEYED BY `index`, NOT BY ORDER OF ARRIVAL. Fragments for two
122
+ * concurrent tool calls interleave, and appending to "the last one" would
123
+ * splice one call's arguments into another's — producing valid-looking
124
+ * JSON that writes the wrong file.
125
+ */
126
+ const i = tc.index ?? 0;
127
+ if (!calls.has(i)) calls.set(i, { id: null, name: '', args: '' });
128
+ const acc = calls.get(i);
129
+ if (tc.id) acc.id = tc.id;
130
+ if (tc.function?.name) acc.name += tc.function.name;
131
+ if (typeof tc.function?.arguments === 'string') acc.args += tc.function.arguments;
132
+ }
133
+ }
134
+
135
+ if (!sawAnything) {
136
+ return { ok: false, error: 'the stream closed without sending anything' };
137
+ }
138
+
139
+ const toolCalls = [...calls.entries()]
140
+ .sort((a, b) => a[0] - b[0])
141
+ .map(([i, c]) => ({
142
+ id: c.id ?? `call_${i}`,
143
+ type: 'function',
144
+ function: { name: c.name, arguments: c.args },
145
+ }))
146
+ .filter((c) => c.function.name);
147
+
148
+ /**
149
+ * ⚠️ AN EMPTY RESULT IS A FAILURE, AND THE CHAIN MUST BE ABLE TO SEE IT.
150
+ * Same trap as the non-streaming path: a reasoning budget can consume the
151
+ * whole reply and close the stream having sent only role frames. Reported in
152
+ * the exact words `chain.mjs` treats as retryable, so a fallback happens
153
+ * instead of the loop reporting "the model said nothing" as a result.
154
+ */
155
+ if (!content.trim() && toolCalls.length === 0) {
156
+ return { ok: false, error: 'the model returned an empty reply' };
157
+ }
158
+
159
+ return { ok: true, content: content || null, toolCalls, finishReason, usage, provider };
160
+ }
161
+
162
+ /**
163
+ * ── ⭐ THE LIVE LINE ─────────────────────────────────────────────────────────
164
+ *
165
+ * Prints streamed prose as it arrives, wrapped, indented, and truncated to a few
166
+ * lines — the reasoning is orientation, not the deliverable.
167
+ *
168
+ * ⚠️ NO CURSOR ADDRESSING, NO SPINNER, NO ALTERNATE SCREEN. The same rule the
169
+ * interactive session follows: output stays append-only so `>` redirection,
170
+ * piping and `tee` keep working, and a transcript remains a file you can read.
171
+ * A UI that only behaves in the terminal it was tested in is worse than plain
172
+ * text everywhere.
173
+ */
174
+ /**
175
+ * ── ⚠️ A CUT THAT LOSES A WORD, AND SAYS NOTHING ────────────────────────────
176
+ *
177
+ * `slice(0, width)` was used in two places and both produced the same defect on
178
+ * screen: a line ending "…implementa" with no marker, which reads as the model
179
+ * having stopped mid-word rather than as the printer having trimmed it.
180
+ *
181
+ * ⭐ THE MARKER IS THE HALF THAT MATTERS. Breaking on a space makes it tidy;
182
+ * the `…` is what makes it TRUE. Without it the reader cannot tell truncated
183
+ * output from truncated thinking, and those call for opposite reactions.
184
+ *
185
+ * ⚠️ AND AN UNBREAKABLE TOKEN MUST STILL PRINT. A minified line or a long URL
186
+ * has no space to break on, and "never cut a word" would silently become
187
+ * "print nothing" on exactly the input that most needs showing. A hard cut with
188
+ * a marker beats silence.
189
+ */
190
+ export function breakAt(text, width) {
191
+ const cut = text.lastIndexOf(' ', width);
192
+ // A boundary too near the start would throw away almost the whole line to
193
+ // save one fragment — past that point a hard cut shows more and lies no more.
194
+ return cut >= Math.min(20, width >> 1) ? cut : width;
195
+ }
196
+
197
+ export function clipLine(text, width) {
198
+ if (typeof text !== 'string' || text.length <= width) return text;
199
+ return `${text.slice(0, breakAt(text, width)).trimEnd()}…`;
200
+ }
201
+
202
+ export function createLivePrinter({ write, maxLines = 3, width = 88 } = {}) {
203
+ let buffer = '';
204
+ let printed = 0;
205
+ let stopped = false;
206
+
207
+ return {
208
+ onText(fragment) {
209
+ if (stopped) return;
210
+ buffer += fragment;
211
+ // Emit only complete lines, so a word is never split across two writes.
212
+ let nl;
213
+ while ((nl = buffer.indexOf('\n')) !== -1) {
214
+ const line = buffer.slice(0, nl).trim();
215
+ buffer = buffer.slice(nl + 1);
216
+ if (!line) continue;
217
+ if (printed >= maxLines) { stopped = true; return; }
218
+ write(` ${clipLine(line, width)}\n`);
219
+ printed += 1;
220
+ }
221
+ /**
222
+ * ⭐ A LONG UNBROKEN PARAGRAPH STILL SHOWS SOMETHING. Models often stream
223
+ * one enormous line, and a printer that waits for `\n` would sit silent
224
+ * through exactly the case this feature exists for.
225
+ */
226
+ if (buffer.length > width && printed < maxLines) {
227
+ /**
228
+ * ⚠️ THIS BRANCH LOOKED LIKE THE CORRECT ONE AND WAS NOT. It did break
229
+ * on a space — but only when that space fell PAST column 20, because
230
+ * `cut > 20` rejects a boundary landing exactly there. Feed it 20-letter
231
+ * words at width 30 and it hard-cuts every time. Same defect as the
232
+ * other two, hidden behind a magic number.
233
+ *
234
+ * ⭐ NO ELLIPSIS HERE, DELIBERATELY: the remainder stays in the buffer
235
+ * and prints next. This is a WRAP, not a truncation, and marking it as
236
+ * cut would be the opposite lie.
237
+ */
238
+ const take = breakAt(buffer, width);
239
+ write(` ${buffer.slice(0, take).trim()}\n`);
240
+ buffer = buffer.slice(take);
241
+ printed += 1;
242
+ }
243
+ },
244
+ /** Anything left that never got a newline. */
245
+ flush() {
246
+ if (stopped || printed >= maxLines) return;
247
+ const rest = buffer.trim();
248
+ if (rest) write(` ${clipLine(rest, width)}\n`);
249
+ buffer = '';
250
+ },
251
+ linesPrinted: () => printed,
252
+ };
253
+ }