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.
- package/CHANGELOG.md +328 -0
- package/ENTERPRISE.md +927 -0
- package/LICENSE +120 -0
- package/README.md +1245 -0
- package/ROADMAP.md +556 -0
- package/bin/acuvo-mcp.mjs +208 -0
- package/bin/acuvo.mjs +3108 -0
- package/lib/acceptance-consent.mjs +168 -0
- package/lib/acceptance.mjs +859 -0
- package/lib/account.mjs +226 -0
- package/lib/acuvo-dir.mjs +72 -0
- package/lib/acuvo-models.mjs +141 -0
- package/lib/apply-patch.mjs +570 -0
- package/lib/ask-user.mjs +173 -0
- package/lib/audit.mjs +530 -0
- package/lib/auto-lease.mjs +174 -0
- package/lib/background.mjs +842 -0
- package/lib/best-of.mjs +334 -0
- package/lib/board.mjs +232 -0
- package/lib/breaker.mjs +93 -0
- package/lib/budget.mjs +1375 -0
- package/lib/builtin-skills.mjs +135 -0
- package/lib/cache-floor.mjs +204 -0
- package/lib/chain.mjs +303 -0
- package/lib/changed-paths.mjs +84 -0
- package/lib/chat.mjs +434 -0
- package/lib/checkpoint.mjs +637 -0
- package/lib/child-lifetime.mjs +71 -0
- package/lib/cli-args.mjs +1255 -0
- package/lib/code-review.mjs +1382 -0
- package/lib/colour.mjs +82 -0
- package/lib/command.mjs +2847 -0
- package/lib/compact.mjs +1151 -0
- package/lib/completion.mjs +515 -0
- package/lib/creative-engines.mjs +779 -0
- package/lib/db-inspect.mjs +1624 -0
- package/lib/delete.mjs +111 -0
- package/lib/design-loop.mjs +570 -0
- package/lib/diff-preview.mjs +1044 -0
- package/lib/doctor.mjs +2139 -0
- package/lib/dropped.mjs +216 -0
- package/lib/edit-diagnostics.mjs +277 -0
- package/lib/edit.mjs +460 -0
- package/lib/env-file.mjs +250 -0
- package/lib/escalate.mjs +702 -0
- package/lib/evaluate.mjs +284 -0
- package/lib/fetch-text.mjs +952 -0
- package/lib/fleet-budget.mjs +256 -0
- package/lib/gh.mjs +1536 -0
- package/lib/git.mjs +1341 -0
- package/lib/github.mjs +261 -0
- package/lib/h2.mjs +194 -0
- package/lib/handoff.mjs +417 -0
- package/lib/hooks.mjs +626 -0
- package/lib/http-probe.mjs +907 -0
- package/lib/image-director.mjs +322 -0
- package/lib/image-edit.mjs +522 -0
- package/lib/imagegen.mjs +998 -0
- package/lib/interrupt.mjs +234 -0
- package/lib/learned.mjs +353 -0
- package/lib/lease-watch.mjs +115 -0
- package/lib/lease.mjs +868 -0
- package/lib/localize.mjs +834 -0
- package/lib/log-tail.mjs +1052 -0
- package/lib/login.mjs +157 -0
- package/lib/lsp.mjs +1613 -0
- package/lib/mcp-consent.mjs +377 -0
- package/lib/mcp-defaults.mjs +780 -0
- package/lib/mcp-server.mjs +1343 -0
- package/lib/mcp.mjs +1263 -0
- package/lib/media.mjs +1283 -0
- package/lib/memory-workspace.mjs +179 -0
- package/lib/model-json.mjs +132 -0
- package/lib/model-tier.mjs +171 -0
- package/lib/model.mjs +1445 -0
- package/lib/parallel.mjs +144 -0
- package/lib/perchance.mjs +210 -0
- package/lib/plan-coherence.mjs +1461 -0
- package/lib/plan-ledger.mjs +981 -0
- package/lib/plan.mjs +461 -0
- package/lib/policy.mjs +783 -0
- package/lib/prefix-order.mjs +38 -0
- package/lib/project-memory.mjs +127 -0
- package/lib/prompt.mjs +109 -0
- package/lib/python.mjs +862 -0
- package/lib/rcfile.mjs +853 -0
- package/lib/read-window.mjs +743 -0
- package/lib/refute-tools.mjs +34 -0
- package/lib/refute.mjs +806 -0
- package/lib/repl-driver.mjs +264 -0
- package/lib/repl.mjs +324 -0
- package/lib/replay.mjs +1218 -0
- package/lib/repo-map.mjs +1101 -0
- package/lib/report.mjs +419 -0
- package/lib/search-rank.mjs +99 -0
- package/lib/search.mjs +659 -0
- package/lib/secret-paths.mjs +54 -0
- package/lib/session.mjs +1017 -0
- package/lib/skills.mjs +703 -0
- package/lib/slash.mjs +356 -0
- package/lib/spawn-argv.mjs +1151 -0
- package/lib/spend.mjs +250 -0
- package/lib/steer.mjs +280 -0
- package/lib/stream.mjs +253 -0
- package/lib/stuck.mjs +712 -0
- package/lib/subagent.mjs +749 -0
- package/lib/terminal-graphics.mjs +171 -0
- package/lib/tool-prefix.mjs +226 -0
- package/lib/tool-shortlist.mjs +162 -0
- package/lib/tools.mjs +2333 -0
- package/lib/tsserver.mjs +423 -0
- package/lib/turn.mjs +5672 -0
- package/lib/untrusted-block.mjs +271 -0
- package/lib/verify-claim.mjs +299 -0
- package/lib/vision.mjs +330 -0
- package/lib/voice-task.mjs +561 -0
- package/lib/warm-provider.mjs +255 -0
- package/lib/websearch.mjs +401 -0
- package/lib/workspace.mjs +928 -0
- package/lib/write-approval.mjs +235 -0
- package/lib/write-many.mjs +162 -0
- package/package.json +62 -0
- package/scripts/bundle.mjs +768 -0
- package/scripts/cache-floor.mjs +176 -0
- package/scripts/machine.mjs +226 -0
- package/scripts/test.mjs +139 -0
- package/skills/accessibility.md +87 -0
- package/skills/acuvo-design-system.md +123 -0
- package/skills/animation.md +84 -0
- package/skills/api-design.md +82 -0
- package/skills/auth-and-sessions.md +78 -0
- package/skills/build-with-a-framework.md +101 -0
- package/skills/colour-and-contrast.md +112 -0
- package/skills/creative-engines.md +81 -0
- package/skills/css-layout.md +85 -0
- package/skills/data-and-charts.md +77 -0
- package/skills/debugging.md +76 -0
- package/skills/designing-by-looking.md +84 -0
- package/skills/error-handling.md +78 -0
- package/skills/forms-and-validation.md +93 -0
- package/skills/nextjs-app-router.md +75 -0
- package/skills/page-composition.md +103 -0
- package/skills/performance.md +77 -0
- package/skills/plan-before-building.md +52 -0
- package/skills/planning-and-delegating.md +72 -0
- package/skills/refactoring.md +70 -0
- package/skills/security-basics.md +76 -0
- package/skills/state-management.md +73 -0
- package/skills/supabase-multitenant.md +72 -0
- package/skills/typescript-strict.md +90 -0
- package/skills/typography.md +135 -0
- package/skills/verify-your-own-work.md +62 -0
- package/skills/web-app-quality.md +62 -0
- 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
|
+
}
|