flowviant 0.20.0 → 0.21.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/bin/lib/claude.mjs +115 -3
- package/bin/lib/config.mjs +1 -1
- package/bin/lib/fleet.mjs +60 -0
- package/package.json +1 -1
package/bin/lib/claude.mjs
CHANGED
|
@@ -199,13 +199,88 @@ export function mcpConfigFor(token, mcpUrl) {
|
|
|
199
199
|
return { dir, path: p };
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
// Shorten an absolute tool path to a repo-relative one for legible output.
|
|
203
|
+
const shortPath = (p, cwd) => {
|
|
204
|
+
if (typeof p !== 'string') return '';
|
|
205
|
+
let s = p;
|
|
206
|
+
if (cwd && s.startsWith(cwd)) s = s.slice(cwd.length).replace(/^\/+/, '');
|
|
207
|
+
return s;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Turn one Claude tool_use into a compact activity {kind, label}, or null for
|
|
211
|
+
// tools not worth surfacing. `kind:'read'` is what the file counter counts;
|
|
212
|
+
// an emit_wiki_node flips the phase to "writing". Used by wiki turns to stream
|
|
213
|
+
// exactly which files Claude is touching (daemon console + app cover).
|
|
214
|
+
export function humanizeToolUse(name, input = {}, cwd = '') {
|
|
215
|
+
switch (name) {
|
|
216
|
+
case 'Read':
|
|
217
|
+
return { kind: 'read', label: `read ${shortPath(input.file_path, cwd)}` };
|
|
218
|
+
case 'Grep':
|
|
219
|
+
return {
|
|
220
|
+
kind: 'search',
|
|
221
|
+
label: `grep ${JSON.stringify(input.pattern ?? '')}${input.path ? ` in ${shortPath(input.path, cwd)}` : ''}`,
|
|
222
|
+
};
|
|
223
|
+
case 'Glob':
|
|
224
|
+
return { kind: 'glob', label: `glob ${input.pattern ?? ''}` };
|
|
225
|
+
case 'LS':
|
|
226
|
+
return { kind: 'list', label: `ls ${shortPath(input.path ?? '.', cwd)}` };
|
|
227
|
+
case 'Bash':
|
|
228
|
+
return { kind: 'bash', label: `$ ${String(input.command ?? '').replace(/\s+/g, ' ').slice(0, 60)}` };
|
|
229
|
+
default:
|
|
230
|
+
if (typeof name !== 'string') return null;
|
|
231
|
+
if (name.includes('emit_wiki_node')) return { kind: 'write', label: `+ node ${input.id ?? ''}` };
|
|
232
|
+
if (name.includes('finish_wiki_generation')) return { kind: 'write', label: 'finalize wiki' };
|
|
233
|
+
if (name.includes('list_wiki_nodes')) return { kind: 'mcp', label: 'list wiki nodes' };
|
|
234
|
+
return null; // other tools: silent
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Parse ONE line of `--output-format stream-json` NDJSON. Pulls assistant text
|
|
239
|
+
// into `out` (so sentinel detection still works) and turns each tool_use into a
|
|
240
|
+
// streamed activity line. A line that isn't JSON (a stray warning) is treated as
|
|
241
|
+
// raw text so nothing is lost.
|
|
242
|
+
function handleStreamLine(line, { cwd, emit, onActivity, appendText }) {
|
|
243
|
+
let ev;
|
|
244
|
+
try {
|
|
245
|
+
ev = JSON.parse(line);
|
|
246
|
+
} catch {
|
|
247
|
+
appendText(line + '\n');
|
|
248
|
+
emit(line + '\n');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
|
|
252
|
+
for (const b of ev.message.content) {
|
|
253
|
+
if (b.type === 'text' && b.text) appendText(b.text + '\n');
|
|
254
|
+
else if (b.type === 'tool_use') {
|
|
255
|
+
const a = humanizeToolUse(b.name, b.input || {}, cwd);
|
|
256
|
+
if (a) {
|
|
257
|
+
emit(a.label + '\n');
|
|
258
|
+
onActivity?.(a);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} else if (ev.type === 'result' && typeof ev.result === 'string') {
|
|
263
|
+
// The final assistant text (carries WIKI_DONE / REGROUND_DONE).
|
|
264
|
+
appendText(ev.result + '\n');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
202
268
|
// One Claude Code turn. Output is captured (for sentinel detection) and streamed
|
|
203
269
|
// through, line-prefixed with the worker label so a fleet stays legible.
|
|
204
|
-
|
|
270
|
+
//
|
|
271
|
+
// `streamJson` switches to `--output-format stream-json` and parses the event
|
|
272
|
+
// stream: only the humanized tool activity reaches the console (a legible
|
|
273
|
+
// stream of `read …`, `grep …`, `+ node …`), assistant text is folded into the
|
|
274
|
+
// returned string for sentinel detection, and each activity is handed to
|
|
275
|
+
// `onActivity` so the caller can forward progress. Build-agent turns leave it
|
|
276
|
+
// off and keep the raw text passthrough + line sentinels.
|
|
277
|
+
export function runTurn({ prompt, resume, system, cwd, mcpConfig, label, onSpawn, streamJson, onActivity }) {
|
|
205
278
|
return new Promise((resolve) => {
|
|
206
279
|
const args = [];
|
|
207
280
|
if (resume) args.push('--continue');
|
|
208
|
-
args.push('-p', prompt, '--mcp-config', mcpConfig, '--append-system-prompt', system
|
|
281
|
+
args.push('-p', prompt, '--mcp-config', mcpConfig, '--append-system-prompt', system);
|
|
282
|
+
if (streamJson) args.push('--output-format', 'stream-json', '--verbose');
|
|
283
|
+
args.push(...PERM);
|
|
209
284
|
// Force the user's Claude Code subscription — never the API. A key exported in
|
|
210
285
|
// the shell would otherwise silently bill every poll-mode turn as raw API
|
|
211
286
|
// usage (same invariant live mode enforces on its SDK session env).
|
|
@@ -216,10 +291,47 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, label, onSpawn
|
|
|
216
291
|
onSpawn?.(child);
|
|
217
292
|
let out = '';
|
|
218
293
|
const pfx = label ? `${label} ` : '';
|
|
294
|
+
const emit = (s) => process.stdout.write(pfx ? s.replace(/\n/g, `\n${pfx}`) : s);
|
|
295
|
+
|
|
296
|
+
if (streamJson) {
|
|
297
|
+
let buf = '';
|
|
298
|
+
const appendText = (t) => {
|
|
299
|
+
out += t;
|
|
300
|
+
};
|
|
301
|
+
child.stdout.on('data', (d) => {
|
|
302
|
+
buf += d.toString();
|
|
303
|
+
let nl;
|
|
304
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
305
|
+
const line = buf.slice(0, nl);
|
|
306
|
+
buf = buf.slice(nl + 1);
|
|
307
|
+
if (line.trim()) handleStreamLine(line, { cwd, emit, onActivity, appendText });
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
// stderr is not JSON (warnings/errors) — pass through and keep for sentinels.
|
|
311
|
+
child.stderr.on('data', (d) => {
|
|
312
|
+
const s = d.toString();
|
|
313
|
+
out += s;
|
|
314
|
+
emit(s);
|
|
315
|
+
});
|
|
316
|
+
child.on('error', (e) => {
|
|
317
|
+
if (e.code === 'ENOENT') {
|
|
318
|
+
console.error("\nerror: 'claude' CLI not found on PATH. Install Claude Code first.");
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
console.error(e);
|
|
322
|
+
resolve(out);
|
|
323
|
+
});
|
|
324
|
+
child.on('close', () => {
|
|
325
|
+
if (buf.trim()) handleStreamLine(buf, { cwd, emit, onActivity, appendText });
|
|
326
|
+
resolve(out);
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
219
331
|
const onChunk = (d) => {
|
|
220
332
|
const s = d.toString();
|
|
221
333
|
out += s;
|
|
222
|
-
|
|
334
|
+
emit(s);
|
|
223
335
|
};
|
|
224
336
|
child.stdout.on('data', onChunk);
|
|
225
337
|
child.stderr.on('data', onChunk);
|
package/bin/lib/config.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
|
|
7
|
-
export const VERSION = '0.
|
|
7
|
+
export const VERSION = '0.21.0';
|
|
8
8
|
|
|
9
9
|
// Credential stored by `flowviant login` (device auth) — the no-token,
|
|
10
10
|
// no-env-var path. An explicit --fleet flag or FLOWVIANT_FLEET env still wins.
|
package/bin/lib/fleet.mjs
CHANGED
|
@@ -400,6 +400,7 @@ export async function runFleetDaemon() {
|
|
|
400
400
|
const wikiWt = join(baseDir, 'wiki');
|
|
401
401
|
const REGROUND_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/reground-done');
|
|
402
402
|
const WIKI_TOKEN_URL = FLEET_URL.replace(/\/agents\/?$/, '/wiki-token');
|
|
403
|
+
const WIKI_PROGRESS_URL = FLEET_URL.replace(/\/agents\/?$/, '/wiki-progress');
|
|
403
404
|
const wikiQueue = [];
|
|
404
405
|
let wikiBusy = false;
|
|
405
406
|
let lastSweepAt = null; // dedup: run each Regenerate request once
|
|
@@ -419,6 +420,30 @@ export async function runFleetDaemon() {
|
|
|
419
420
|
}
|
|
420
421
|
};
|
|
421
422
|
|
|
423
|
+
// Stream what the wiki turn is doing to the app (the canvas renders the read
|
|
424
|
+
// phase). Throttled to ~1/s — the FIRST activity of a run and the terminal
|
|
425
|
+
// `done` frame force-send so the cover appears fast and clears cleanly.
|
|
426
|
+
let lastProgressAt = 0;
|
|
427
|
+
const postWikiProgress = async (body, force = false) => {
|
|
428
|
+
const now = Date.now();
|
|
429
|
+
if (!force && now - lastProgressAt < 1000) return;
|
|
430
|
+
lastProgressAt = now;
|
|
431
|
+
try {
|
|
432
|
+
await fetch(WIKI_PROGRESS_URL, {
|
|
433
|
+
method: 'POST',
|
|
434
|
+
headers: {
|
|
435
|
+
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
436
|
+
'User-Agent': USER_AGENT,
|
|
437
|
+
'Content-Type': 'application/json',
|
|
438
|
+
},
|
|
439
|
+
signal: AbortSignal.timeout(15_000),
|
|
440
|
+
body: JSON.stringify(body),
|
|
441
|
+
});
|
|
442
|
+
} catch {
|
|
443
|
+
/* best-effort — a dropped frame is harmless, the next one supersedes it */
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
422
447
|
const enqueueSweep = (job) => {
|
|
423
448
|
if (!job || job.requestedAt === lastSweepAt) return;
|
|
424
449
|
lastSweepAt = job.requestedAt;
|
|
@@ -461,6 +486,24 @@ export async function runFleetDaemon() {
|
|
|
461
486
|
}
|
|
462
487
|
const task = wikiQueue.shift();
|
|
463
488
|
const { dir, path: mcpConfig } = mcpConfigFor(token, mcpUrl);
|
|
489
|
+
// Live progress for this turn: count the files Claude reads, flip to the
|
|
490
|
+
// "writing" phase once it starts emitting nodes, and stream each action
|
|
491
|
+
// to the app (throttled). elapsedSec is on the daemon's own clock.
|
|
492
|
+
const mode = task.type === 'sweep' ? 'sweep' : 'reground';
|
|
493
|
+
const startedAt = Date.now();
|
|
494
|
+
let filesRead = 0;
|
|
495
|
+
let phase = 'reading';
|
|
496
|
+
const onActivity = (a) => {
|
|
497
|
+
if (a.kind === 'read') filesRead++;
|
|
498
|
+
if (a.kind === 'write') phase = 'writing';
|
|
499
|
+
void postWikiProgress({
|
|
500
|
+
mode,
|
|
501
|
+
phase,
|
|
502
|
+
activity: a.label,
|
|
503
|
+
filesRead,
|
|
504
|
+
elapsedSec: Math.round((Date.now() - startedAt) / 1000),
|
|
505
|
+
});
|
|
506
|
+
};
|
|
464
507
|
try {
|
|
465
508
|
if (!existsSync(wikiWt)) {
|
|
466
509
|
try {
|
|
@@ -486,6 +529,8 @@ export async function runFleetDaemon() {
|
|
|
486
529
|
cwd: wikiWt,
|
|
487
530
|
mcpConfig,
|
|
488
531
|
label: c.cyan('[wiki]'),
|
|
532
|
+
streamJson: true,
|
|
533
|
+
onActivity,
|
|
489
534
|
});
|
|
490
535
|
if (sawSentinel(out, 'WIKI_DONE'))
|
|
491
536
|
ok(`${c.cyan('wiki')} ${c.dim('— regenerated from your code.')}`);
|
|
@@ -504,6 +549,8 @@ export async function runFleetDaemon() {
|
|
|
504
549
|
cwd: wikiWt,
|
|
505
550
|
mcpConfig,
|
|
506
551
|
label: c.cyan('[wiki]'),
|
|
552
|
+
streamJson: true,
|
|
553
|
+
onActivity,
|
|
507
554
|
});
|
|
508
555
|
if (sawSentinel(out, 'REGROUND_DONE'))
|
|
509
556
|
ok(`${c.cyan('wiki')} ${c.dim(`— wiki updated for "${task.title}".`)}`);
|
|
@@ -518,6 +565,19 @@ export async function runFleetDaemon() {
|
|
|
518
565
|
} catch (e) {
|
|
519
566
|
warn(`wiki ${task.type} failed: ${e.message}`);
|
|
520
567
|
} finally {
|
|
568
|
+
// Terminal frame so the app cover clears promptly (don't wait for the
|
|
569
|
+
// freshness window to lapse). force-sent past the throttle.
|
|
570
|
+
await postWikiProgress(
|
|
571
|
+
{
|
|
572
|
+
mode,
|
|
573
|
+
phase,
|
|
574
|
+
activity: '',
|
|
575
|
+
filesRead,
|
|
576
|
+
elapsedSec: Math.round((Date.now() - startedAt) / 1000),
|
|
577
|
+
done: true,
|
|
578
|
+
},
|
|
579
|
+
true
|
|
580
|
+
);
|
|
521
581
|
rmSync(dir, { recursive: true, force: true });
|
|
522
582
|
}
|
|
523
583
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Run your own Claude Code as headless build agents for Flowviant — on your own credentials. Claims dispatched work, opens PRs, captures review evidence, and routes questions back to you.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|