@c0mpute/code 0.6.0 → 0.6.1
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/cli.mjs +62 -24
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
// C0MPUTE_API_KEY=sk-... c0mpute-code # interactive
|
|
8
8
|
// C0MPUTE_API_KEY=sk-... c0mpute-code "task" # one task, then exit
|
|
9
9
|
import { spawnSync } from 'child_process';
|
|
10
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
|
|
11
11
|
import { createInterface } from 'readline';
|
|
12
12
|
import { stdin, stdout } from 'process';
|
|
13
|
-
import { homedir } from 'os';
|
|
13
|
+
import { homedir, tmpdir } from 'os';
|
|
14
14
|
import { resolve, isAbsolute, join, dirname } from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
|
|
@@ -35,7 +35,16 @@ const WS_JOURNAL = join(WS_DIR, 'journal.md');
|
|
|
35
35
|
|
|
36
36
|
// ── ansi ──
|
|
37
37
|
const e = (n) => (s) => `\x1b[${n}m${s}\x1b[0m`;
|
|
38
|
-
const c = { dim: e(2), bold: e(1), red: e(31), grn: e(32), yel: e(33), blu: e(34), cyn: e(36), mag: e(35), gry: e(90), b: (s) => `\x1b[1m${s}\x1b[0m` };
|
|
38
|
+
const c = { dim: e(2), bold: e(1), red: e(31), grn: e(32), yel: e(33), blu: e(34), cyn: e(36), mag: e(35), gry: e(90), it: e(3), b: (s) => `\x1b[1m${s}\x1b[0m` };
|
|
39
|
+
// Render a single line of the model's markdown prose to ANSI: headers, bullets,
|
|
40
|
+
// bold, italic, inline code. Applied per completed line (we buffer prose by line).
|
|
41
|
+
const mdLine = (s) => s
|
|
42
|
+
.replace(/^(\s*)#{1,6}\s+(.*)$/, (_, sp, t) => sp + c.b(t)) // # headers → bold
|
|
43
|
+
.replace(/^(\s*)([-*+])\s+/, (_, sp) => sp + c.grn('•') + ' ') // - bullets → •
|
|
44
|
+
.replace(/^(\s*)(\d+)\.\s+/, (_, sp, n) => sp + c.gry(n + '.') + ' ') // 1. numbered
|
|
45
|
+
.replace(/\*\*([^*]+)\*\*/g, (_, t) => c.b(t)) // **bold**
|
|
46
|
+
.replace(/`([^`]+)`/g, (_, t) => c.cyn(t)) // `code`
|
|
47
|
+
.replace(/(^|[\s(])[*_]([^*_\s][^*_]*?)[*_]([\s).,!?]|$)/g, (_, a, t, z) => a + c.it(t) + z); // *italic*
|
|
39
48
|
// c0mpute brand: pure black, green accent (#5af78e), pixel square marker (not Claude's round/orange dot).
|
|
40
49
|
const MARK = c.grn('▪');
|
|
41
50
|
const vlen = (s) => s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
@@ -64,9 +73,17 @@ const redact = (t) => { let s = String(t ?? ''); for (const rx of SECRET_RX) s =
|
|
|
64
73
|
// ── git / shell ──
|
|
65
74
|
const isGit = existsSync(`${CWD}/.git`);
|
|
66
75
|
const sh = (cmd) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
// Redirect the whole command's output to a temp file rather than capturing via a
|
|
77
|
+
// pipe. A backgrounded child (a server: `npm start & …`) inherits the pipe and
|
|
78
|
+
// holds it open, so a pipe-capturing spawnSync hangs until the 120s timeout even
|
|
79
|
+
// though the foreground finished. With a file, the bash process exits promptly,
|
|
80
|
+
// the server keeps running (orphaned, still serving), and we read what it printed.
|
|
81
|
+
const log = join(tmpdir(), `cc-${process.pid}-${Date.now()}.log`);
|
|
82
|
+
const r = spawnSync('/bin/bash', ['-c', `( ${cmd} ) >${shq(log)} 2>&1`], { cwd: CWD, timeout: 120000 });
|
|
83
|
+
let out = ''; try { out = readFileSync(log, 'utf8'); } catch {}
|
|
84
|
+
try { unlinkSync(log); } catch {}
|
|
85
|
+
if (out.length > (1 << 24)) out = out.slice(0, 1 << 24);
|
|
86
|
+
if (r.error) return `error: ${r.error.code === 'ETIMEDOUT' ? 'timed out after 120s' : r.error.message}` + (out ? `\n${out}` : '');
|
|
70
87
|
return (r.status ? `exit ${r.status}\n` : '') + out;
|
|
71
88
|
};
|
|
72
89
|
|
|
@@ -121,21 +138,30 @@ function recordWork(task, summary) {
|
|
|
121
138
|
// ── streaming over the network ──
|
|
122
139
|
const PULSE = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃', '▂']; // compute pulse
|
|
123
140
|
async function think(messages) {
|
|
124
|
-
let i = 0, tick = null
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
let i = 0, tick = null;
|
|
142
|
+
// The indicator stays alive whenever we're waiting (before the first prose line,
|
|
143
|
+
// between lines, and through the whole action-block generation) so there's never
|
|
144
|
+
// dead air — only paused while a prose line is actually being written.
|
|
145
|
+
const spin = () => { if (stdout.isTTY && !process.env.C0MPUTE_NO_SPINNER && !tick) tick = setInterval(() => process.stdout.write('\r' + c.grn(PULSE[i++ % PULSE.length]) + ' '), 80); };
|
|
146
|
+
const unspin = () => { if (tick) { clearInterval(tick); tick = null; if (stdout.isTTY) process.stdout.write('\r\x1b[K'); } };
|
|
147
|
+
spin();
|
|
127
148
|
currentAbort = new AbortController();
|
|
128
149
|
try {
|
|
129
150
|
const r = await fetch(API, { method: 'POST', signal: currentAbort.signal, headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: MODEL, messages, temperature: 0.2, max_tokens: 1024, stream: true }) });
|
|
130
151
|
if (!r.ok) throw new Error(`c0mpute API ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
131
152
|
const reader = r.body.getReader(), dec = new TextDecoder();
|
|
132
153
|
let buf = '', full = '', inCode = false, shown = false, prose = '', pp = 0;
|
|
133
|
-
//
|
|
134
|
-
//
|
|
154
|
+
// Emit prose a COMPLETE LINE at a time, rendered as markdown (bold/code/headers/
|
|
155
|
+
// bullets). Strip the model's "THOUGHT:" label; withhold a 9-char tail on the
|
|
156
|
+
// non-final pass so a half-arrived keyword never leaks to the screen.
|
|
135
157
|
const flush = (final) => {
|
|
136
158
|
const clean = prose.replace(/\bTHOUGHT:?\s*/gi, '');
|
|
137
|
-
const
|
|
138
|
-
|
|
159
|
+
const safeEnd = final ? clean.length : Math.max(pp, clean.length - 9);
|
|
160
|
+
let nl;
|
|
161
|
+
while ((nl = clean.indexOf('\n', pp)) !== -1 && nl < safeEnd) {
|
|
162
|
+
unspin(); process.stdout.write(mdLine(clean.slice(pp, nl)) + '\n'); pp = nl + 1; shown = true;
|
|
163
|
+
}
|
|
164
|
+
if (final && pp < clean.length) { unspin(); process.stdout.write(mdLine(clean.slice(pp))); pp = clean.length; shown = true; }
|
|
139
165
|
};
|
|
140
166
|
while (true) {
|
|
141
167
|
const { done, value } = await reader.read(); if (done) break;
|
|
@@ -144,18 +170,20 @@ async function think(messages) {
|
|
|
144
170
|
if (!ln.startsWith('data:')) continue; const p = ln.slice(5).trim(); if (p === '[DONE]') continue;
|
|
145
171
|
let tok = ''; try { tok = JSON.parse(p).choices?.[0]?.delta?.content || ''; } catch { continue; }
|
|
146
172
|
if (!tok) continue;
|
|
147
|
-
if (!first) { first = true; stop(); }
|
|
148
173
|
full += tok;
|
|
149
174
|
if (!inCode) {
|
|
150
|
-
if (full.includes('```')) {
|
|
151
|
-
|
|
175
|
+
if (full.includes('```')) {
|
|
176
|
+
inCode = true; flush(true); // emit any remaining prose
|
|
177
|
+
if (shown) process.stdout.write('\n'); // separate prose from the action below
|
|
178
|
+
spin(); // keep the indicator alive while the action generates
|
|
179
|
+
} else { prose += tok; flush(false); spin(); } // re-arm the indicator between prose lines
|
|
152
180
|
}
|
|
153
181
|
}
|
|
154
182
|
}
|
|
155
|
-
flush(true);
|
|
156
|
-
if (shown) process.stdout.write('\n');
|
|
183
|
+
flush(true); unspin();
|
|
184
|
+
if (shown && !inCode) process.stdout.write('\n');
|
|
157
185
|
return full;
|
|
158
|
-
} finally {
|
|
186
|
+
} finally { unspin(); }
|
|
159
187
|
}
|
|
160
188
|
// ── action protocol: model emits ONE fenced block per turn; first line is the command ──
|
|
161
189
|
const VERBS = new Set(['list', 'search', 'read', 'edit', 'write', 'run', 'done']);
|
|
@@ -321,7 +349,7 @@ const LABELS = { read: 'Read', list: 'List', search: 'Search', edit: 'Update', w
|
|
|
321
349
|
async function runTask(task, history) {
|
|
322
350
|
console.log('');
|
|
323
351
|
history.push({ role: 'user', content: task });
|
|
324
|
-
let ran = false, nudges = 0, lastRunFailed = false, doneNudges = 0, doneSummary = '';
|
|
352
|
+
let ran = false, nudges = 0, lastRunFailed = false, doneNudges = 0, doneSummary = '', doneBody = '';
|
|
325
353
|
busy = true; interrupted = false;
|
|
326
354
|
// is this a coding task (enforce actions) or chat/greeting (a prose reply is fine)?
|
|
327
355
|
const isCoding = /\b(fix|bug|error|fail|implement|add|refactor|test|debug|rename|update|create|build|install|broken|crash|exception|traceback|function|class|import|run)\b/i.test(task) || /[\w./-]+\.\w{1,5}\b/.test(task);
|
|
@@ -344,7 +372,7 @@ async function runTask(task, history) {
|
|
|
344
372
|
if (verb === 'done') {
|
|
345
373
|
// don't accept "done" while the last command was still failing — that's a false finish
|
|
346
374
|
if (lastRunFailed && doneNudges < 2) { doneNudges++; history.push({ role: 'user', content: 'The last command reported failures/errors, so the task is NOT verified. Keep fixing and re-run the test until it passes. If you are genuinely stuck, say plainly what is still broken instead of using `done`.' }); continue; }
|
|
347
|
-
ran = true;
|
|
375
|
+
ran = true; doneBody = act.body.trim(); doneSummary = doneBody.split('\n').map(s => s.trim()).filter(Boolean)[0] || ''; break;
|
|
348
376
|
}
|
|
349
377
|
const path0 = arg.split(/\s+/)[0] || '';
|
|
350
378
|
const shown = verb === 'search' ? arg : (verb === 'run' ? (arg || act.body.split('\n')[0]) : path0);
|
|
@@ -376,7 +404,13 @@ async function runTask(task, history) {
|
|
|
376
404
|
}
|
|
377
405
|
} finally { busy = false; }
|
|
378
406
|
if (interrupted) { interrupted = false; console.log(c.dim(' ⊘ stopped.') + '\n'); }
|
|
379
|
-
else if (ran) {
|
|
407
|
+
else if (ran) {
|
|
408
|
+
if (isCoding) recordWork(task, doneSummary);
|
|
409
|
+
// Closing summary: print the model's done message (what it built + how to run it),
|
|
410
|
+
// markdown-rendered, instead of a bare "done".
|
|
411
|
+
if (doneBody) console.log(MARK + ' ' + c.b('done') + '\n' + doneBody.split('\n').map(l => ' ' + mdLine(l)).join('\n') + '\n');
|
|
412
|
+
else console.log(MARK + ' ' + c.dim('done') + '\n');
|
|
413
|
+
}
|
|
380
414
|
else console.log('');
|
|
381
415
|
}
|
|
382
416
|
|
|
@@ -528,11 +562,15 @@ Create a new file or fully overwrite one. Prefer edit for existing files.
|
|
|
528
562
|
\`\`\`
|
|
529
563
|
run python3 -m pytest -q
|
|
530
564
|
\`\`\`
|
|
531
|
-
Run a shell command (tests, build, repro).
|
|
565
|
+
Run a shell command (tests, build, repro). To start a long-running server, background
|
|
566
|
+
it and probe it, e.g. \`npm start & sleep 3 && curl -s localhost:3000\` — never run a
|
|
567
|
+
server in the foreground; it would block.
|
|
532
568
|
|
|
533
569
|
\`\`\`
|
|
534
570
|
done
|
|
535
|
-
|
|
571
|
+
A short summary for the user: what you built or changed. If you started an app or a
|
|
572
|
+
server, say exactly how to run and view it (e.g. "Run: npm start, then open
|
|
573
|
+
http://localhost:3000"). A few lines is fine.
|
|
536
574
|
\`\`\`
|
|
537
575
|
Finish — ONLY after you verified the fix (ran the test/repro and it passed).
|
|
538
576
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c0mpute/code",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Decentralized coding agent — thinking runs on the c0mpute network, file edits and commands run locally under your approval. Claude Code-style UX, no single provider.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|