auxilo-mcp 0.9.0 → 0.9.2
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/README.md +132 -135
- package/bin/auxilo-cli.js +230 -68
- package/lib/installer.js +5 -1
- package/lib/review.js +170 -3
- package/mcp-server.js +330 -138
- package/package.json +12 -6
- package/scripts/extract-local.js +119 -0
- package/scripts/runner.js +54 -30
- package/prompts/auxilo-learning-claude-code.md +0 -250
- package/prompts/auxilo-learning-generic.md +0 -327
- package/prompts/auxilo-learning-schema.json +0 -98
package/bin/auxilo-cli.js
CHANGED
|
@@ -90,12 +90,21 @@ const CONSENT_TEXT = `
|
|
|
90
90
|
--------------------------------
|
|
91
91
|
If you opt in, a session-end hook runs after each session in your wired
|
|
92
92
|
clients (Claude Code, Cursor, Gemini CLI, …) and a local runner:
|
|
93
|
-
• READS the session transcript
|
|
94
|
-
• SCRUBS it locally (sensitivity filter: API keys, tokens, emails, PII
|
|
95
|
-
redacted
|
|
96
|
-
•
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
• READS the session transcript on your machine,
|
|
94
|
+
• SCRUBS it locally (sensitivity filter: API keys, tokens, emails, PII
|
|
95
|
+
are redacted first),
|
|
96
|
+
• EXTRACTS reusable learnings locally using your own claude CLI (your
|
|
97
|
+
existing subscription). For this step your transcript is processed
|
|
98
|
+
only by your own model provider the same way your normal sessions
|
|
99
|
+
are, and is never sent to Auxilo, raw or scrubbed.
|
|
100
|
+
• UPLOADS only the finished learning drafts (title, body, category,
|
|
101
|
+
tags, task context, outcome) to Auxilo (${'POST /learn'}). A draft
|
|
102
|
+
that passes every screen publishes to the marketplace immediately
|
|
103
|
+
under your account, and you can retract it for 7 days. A draft that
|
|
104
|
+
any screen flags (sensitive, duplicate, uncertain quality) waits in
|
|
105
|
+
your private queue for \`auxilo review\`. Manual mode (approve first,
|
|
106
|
+
for everything) is available in your account settings. You earn 70%
|
|
107
|
+
of sales.
|
|
99
108
|
You can stop any time with \`auxilo disable\` (local kill-switch) and review
|
|
100
109
|
every run in ~/.auxilo/extract.log. Saying No installs the MCP server only.
|
|
101
110
|
`;
|
|
@@ -338,20 +347,108 @@ async function cmdDisable(flags) {
|
|
|
338
347
|
// The human gate that makes background extraction safe to re-enable:
|
|
339
348
|
// extract -> pending_review -> `auxilo review` -> approve the safe ones.
|
|
340
349
|
// Operates ONLY on the caller's own pending learnings (account-scoped API key).
|
|
350
|
+
//
|
|
351
|
+
// Review-seamless (2026-07-18): the default view is now a triage summary table
|
|
352
|
+
// (quality desc, clean before flagged), with bulk modes that batch through
|
|
353
|
+
// POST /account/pending/bulk. HARD RULE for every bulk APPROVE path: print the
|
|
354
|
+
// exact list and count first, then require the operator to TYPE THE COUNT.
|
|
355
|
+
// There is no --yes bypass on any approve path; publishing always costs one
|
|
356
|
+
// typed number (2026-06-10 mass-publish lesson).
|
|
357
|
+
|
|
358
|
+
/** Truncate + pad a string to an exact width (for the triage table). */
|
|
359
|
+
function fit(s, width) {
|
|
360
|
+
const str = String(s == null ? '' : s);
|
|
361
|
+
if (str.length > width) return str.slice(0, Math.max(0, width - 1)) + '…';
|
|
362
|
+
return str.padEnd(width);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Short flag codes for a summary row: 'clean' or e.g. 'inj+sens'. */
|
|
366
|
+
function shortFlags(row) {
|
|
367
|
+
if (row.screens_passed) return 'clean';
|
|
368
|
+
const map = { injection: 'inj', content_sensitivity: 'sens', near_duplicate: 'dup' };
|
|
369
|
+
return (row.flags || []).map((f) => map[f] || f).join('+') || 'flagged';
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** One compact triage line (shared by the table and rapid mode). */
|
|
373
|
+
function triageLine(row, n, total) {
|
|
374
|
+
const q = row.quality == null ? ' --' : String(row.quality).padStart(3);
|
|
375
|
+
return ` ${String(n).padStart(String(total).length)}. q=${q} ${fit(shortFlags(row), 9)} ${fit(row.category || '', 20)} ${fit(row.title || '(no title)', 52)}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Print the triage summary: counts, then clean rows, then flagged rows. */
|
|
379
|
+
function printSummaryTable(summary) {
|
|
380
|
+
const items = summary.items || [];
|
|
381
|
+
const clean = items.filter((r) => r.screens_passed);
|
|
382
|
+
const flagged = items.filter((r) => !r.screens_passed);
|
|
383
|
+
|
|
384
|
+
console.log(`\n${summary.pending_count} learning(s) pending your review: ${clean.length} passed every platform screen, ${flagged.length} flagged.`);
|
|
385
|
+
const bands = summary.counts && summary.counts.by_quality_band;
|
|
386
|
+
if (bands) {
|
|
387
|
+
console.log(`Quality bands: 18-20: ${bands['18-20'] || 0} · 14-17: ${bands['14-17'] || 0} · 10-13: ${bands['10-13'] || 0} · below 10: ${bands.below_10 || 0} · unscored: ${bands.unscored || 0}`);
|
|
388
|
+
}
|
|
389
|
+
if (Array.isArray(summary.near_dup_clusters) && summary.near_dup_clusters.length > 0) {
|
|
390
|
+
console.log(`Near-duplicate clusters among your pending items: ${summary.near_dup_clusters.length} (review these together)`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (clean.length > 0) {
|
|
394
|
+
console.log(`\nCLEAN (passed every screen) - sorted by quality:`);
|
|
395
|
+
clean.forEach((r, i) => console.log(triageLine(r, i + 1, clean.length)));
|
|
396
|
+
}
|
|
397
|
+
if (flagged.length > 0) {
|
|
398
|
+
console.log(`\nFLAGGED (a platform screen raised a signal) - review individually:`);
|
|
399
|
+
flagged.forEach((r, i) => console.log(triageLine(r, i + 1, flagged.length)));
|
|
400
|
+
}
|
|
401
|
+
console.log('');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* The counted confirmation: print the exact selection, then require the
|
|
406
|
+
* operator to type the exact count. Returns true ONLY on an exact match.
|
|
407
|
+
* No flag bypasses this for approve paths.
|
|
408
|
+
*/
|
|
409
|
+
async function confirmByTypedCount(rows, actionLabel) {
|
|
410
|
+
console.log(`\n${actionLabel} the following ${rows.length} learning(s):`);
|
|
411
|
+
rows.forEach((r) => {
|
|
412
|
+
const q = r.quality == null ? '--' : r.quality;
|
|
413
|
+
console.log(` ${r.id} q=${q} [${shortFlags(r)}] ${r.title || '(no title)'}`);
|
|
414
|
+
});
|
|
415
|
+
console.log(`\nCount: ${rows.length}.`);
|
|
416
|
+
const answer = await ask(`Type the count (${rows.length}) to confirm, anything else aborts: `);
|
|
417
|
+
if (answer !== String(rows.length)) {
|
|
418
|
+
console.log('Aborted. Nothing changed.');
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Run a confirmed selection through the bulk endpoint in chunks, with progress. */
|
|
425
|
+
async function runBulk({ apiKey, baseUrl, rows, decision, reason }) {
|
|
426
|
+
const decisions = rows.map((r) => (decision === 'reject' && reason)
|
|
427
|
+
? { id: r.id, decision, reason }
|
|
428
|
+
: { id: r.id, decision });
|
|
429
|
+
const totals = await review.submitBulkChunked({
|
|
430
|
+
apiKey,
|
|
431
|
+
baseUrl,
|
|
432
|
+
decisions,
|
|
433
|
+
onChunk: ({ chunkIndex, chunkCount, response }) => {
|
|
434
|
+
console.log(` chunk ${chunkIndex + 1}/${chunkCount}: approved ${response.approved || 0}, rejected ${response.rejected || 0}, already done ${response.idempotent || 0}, failed ${response.failed || 0}`);
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
for (const r of totals.results) {
|
|
438
|
+
if (!r.ok) console.error(` ! ${r.id || `entry #${r.index}`}: ${r.error || r.code}`);
|
|
439
|
+
}
|
|
440
|
+
return totals;
|
|
441
|
+
}
|
|
341
442
|
|
|
342
|
-
/**
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (flags) console.log(` ⚠ flags: ${flags}`);
|
|
352
|
-
console.log(` ----------------------------------------------------------`);
|
|
353
|
-
console.log(l.body || '(no body)');
|
|
354
|
-
console.log(` ----------------------------------------------------------`);
|
|
443
|
+
/** Parse --min-quality into an integer 0-20 (default DEFAULT_QUALITY_THRESHOLD). */
|
|
444
|
+
function parseMinQuality(flags) {
|
|
445
|
+
if (flags['min-quality'] === undefined) return review.DEFAULT_QUALITY_THRESHOLD;
|
|
446
|
+
const n = parseInt(flags['min-quality'], 10);
|
|
447
|
+
if (!Number.isFinite(n) || n < 0 || n > 20) {
|
|
448
|
+
console.error('Invalid --min-quality (expected an integer 0-20).');
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
return n;
|
|
355
452
|
}
|
|
356
453
|
|
|
357
454
|
async function cmdReview(flags) {
|
|
@@ -363,80 +460,129 @@ async function cmdReview(flags) {
|
|
|
363
460
|
const baseUrl = resolveBaseUrl(flags);
|
|
364
461
|
const apiKey = creds.api_key;
|
|
365
462
|
|
|
366
|
-
|
|
463
|
+
// ── Summary FIRST, always (triage before any decision surface) ────────────
|
|
464
|
+
let summary;
|
|
367
465
|
try {
|
|
368
|
-
|
|
466
|
+
summary = await review.fetchPendingSummary({ apiKey, baseUrl });
|
|
369
467
|
} catch (err) {
|
|
370
|
-
console.error(`Could not fetch pending review
|
|
468
|
+
console.error(`Could not fetch pending review summary: ${err.message}`);
|
|
371
469
|
process.exit(1);
|
|
372
470
|
}
|
|
373
471
|
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (items.length === 0) {
|
|
472
|
+
const rows = summary.items || [];
|
|
473
|
+
if (rows.length === 0) {
|
|
378
474
|
console.log('No learnings pending your review. Nothing to do.');
|
|
379
475
|
return;
|
|
380
476
|
}
|
|
381
477
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
478
|
+
printSummaryTable(summary);
|
|
479
|
+
|
|
480
|
+
// ── --list: summary only, no mutations ────────────────────────────────────
|
|
481
|
+
if (flags.list) return;
|
|
482
|
+
|
|
483
|
+
// ── --approve-clean: bulk-approve items that passed EVERY platform screen
|
|
484
|
+
// AND clear the quality threshold. Typed-count confirmation, no bypass. ──
|
|
485
|
+
if (flags['approve-clean']) {
|
|
486
|
+
const minQuality = parseMinQuality(flags);
|
|
487
|
+
const sel = review.selectForBulkApprove(rows, { mode: 'clean', minQuality });
|
|
488
|
+
console.log(`approve-clean selection: ${sel.selected.length} of ${rows.length} pending (threshold: quality >= ${minQuality}).`);
|
|
489
|
+
if (sel.excluded_flagged.length) console.log(` excluded ${sel.excluded_flagged.length} screen-flagged item(s) (review those individually).`);
|
|
490
|
+
if (sel.excluded_low_quality.length) console.log(` excluded ${sel.excluded_low_quality.length} below the quality threshold.`);
|
|
491
|
+
if (sel.excluded_unscored.length) console.log(` excluded ${sel.excluded_unscored.length} with no quality score (pass --min-quality 0 to include).`);
|
|
492
|
+
if (sel.selected.length === 0) { console.log('Nothing qualifies. No changes made.'); return; }
|
|
493
|
+
|
|
494
|
+
if (!await confirmByTypedCount(sel.selected, 'APPROVE and PUBLISH')) return;
|
|
495
|
+
const totals = await runBulk({ apiKey, baseUrl, rows: sel.selected, decision: 'approve' });
|
|
496
|
+
console.log(`\nDone. Approved ${totals.approved} (already approved earlier: ${totals.idempotent}, failed: ${totals.failed}). Flagged and below-threshold items stay pending.`);
|
|
390
497
|
return;
|
|
391
498
|
}
|
|
392
499
|
|
|
393
|
-
// ── --all
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
console.log(` ✗ rejected (${rejected}/${items.length}): ${l.title}`);
|
|
403
|
-
} catch (err) {
|
|
404
|
-
console.error(` ! failed to reject ${l.id}: ${err.message}`);
|
|
405
|
-
}
|
|
500
|
+
// ── --all: bulk-approve everything EXCEPT screen-flagged items unless
|
|
501
|
+
// --include-flagged. Typed-count confirmation, no bypass. ───────────────
|
|
502
|
+
if (flags.all) {
|
|
503
|
+
const includeFlagged = flags['include-flagged'] === true;
|
|
504
|
+
const sel = review.selectForBulkApprove(rows, { mode: 'all', includeFlagged });
|
|
505
|
+
if (includeFlagged && sel.selected.some((r) => !r.screens_passed)) {
|
|
506
|
+
console.log('WARNING: --include-flagged is set. This selection INCLUDES items a platform screen flagged (possible injection, sensitive content, or near-duplicates). Approving publishes them publicly.');
|
|
507
|
+
} else if (sel.excluded_flagged.length) {
|
|
508
|
+
console.log(`Excluding ${sel.excluded_flagged.length} screen-flagged item(s) (pass --include-flagged to include them; safer to review those individually).`);
|
|
406
509
|
}
|
|
407
|
-
console.log(
|
|
510
|
+
if (sel.selected.length === 0) { console.log('Nothing qualifies. No changes made.'); return; }
|
|
511
|
+
|
|
512
|
+
if (!await confirmByTypedCount(sel.selected, 'APPROVE and PUBLISH')) return;
|
|
513
|
+
const totals = await runBulk({ apiKey, baseUrl, rows: sel.selected, decision: 'approve' });
|
|
514
|
+
console.log(`\nDone. Approved ${totals.approved} (already approved earlier: ${totals.idempotent}, failed: ${totals.failed}).`);
|
|
408
515
|
return;
|
|
409
516
|
}
|
|
410
517
|
|
|
411
|
-
// ──
|
|
412
|
-
|
|
413
|
-
|
|
518
|
+
// ── --all-reject: bulk reject the whole batch (incident escape hatch).
|
|
519
|
+
// Now batched through the bulk endpoint. --yes keeps its scripted-incident
|
|
520
|
+
// bypass because rejection is the SAFE direction (nothing goes public). ──
|
|
521
|
+
if (flags['all-reject']) {
|
|
522
|
+
const ok = flags.yes || await confirmByTypedCount(rows, 'REJECT (keep private)');
|
|
523
|
+
if (!ok) return;
|
|
524
|
+
const totals = await runBulk({ apiKey, baseUrl, rows, decision: 'reject' });
|
|
525
|
+
console.log(`\nDone. Rejected ${totals.rejected} of ${rows.length} (already rejected earlier: ${totals.idempotent}, failed: ${totals.failed}).`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
414
528
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
529
|
+
// ── interactive rapid mode (default): y/n/s per item, one line each.
|
|
530
|
+
// [v] shows the full body before deciding; decisions post one at a time
|
|
531
|
+
// through the same single-item endpoints as before. ──────────────────────
|
|
532
|
+
const bodies = new Map();
|
|
533
|
+
try {
|
|
534
|
+
let offset = 0;
|
|
535
|
+
const pageLimit = 200;
|
|
536
|
+
for (;;) {
|
|
537
|
+
const page = await review.fetchPending({ apiKey, baseUrl, limit: pageLimit, offset });
|
|
538
|
+
for (const l of page.learnings || []) bodies.set(l.id, l);
|
|
539
|
+
offset += (page.learnings || []).length;
|
|
540
|
+
if (!page.learnings || page.learnings.length === 0 || offset >= (page.pending_count || 0)) break;
|
|
541
|
+
}
|
|
542
|
+
} catch (err) {
|
|
543
|
+
console.error(`Could not fetch pending bodies: ${err.message}`);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
console.log('Rapid review. Per item: [y] approve (goes live) · [n] reject (stays private) · [v] view full body · [s] skip · [q] quit\n');
|
|
548
|
+
|
|
549
|
+
let approved = 0, rejected = 0, skipped = 0;
|
|
550
|
+
const ordered = rows; // clean-first, quality desc (server order preserved by printSummaryTable groups)
|
|
551
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
552
|
+
const row = ordered[i];
|
|
553
|
+
console.log(triageLine(row, i + 1, ordered.length));
|
|
419
554
|
let choice = '';
|
|
420
|
-
|
|
421
|
-
choice = (await ask('
|
|
555
|
+
for (;;) {
|
|
556
|
+
choice = (await ask(' [y/n/v/s/q]? ')).toLowerCase().slice(0, 1);
|
|
557
|
+
if (choice === 'v') {
|
|
558
|
+
const full = bodies.get(row.id);
|
|
559
|
+
console.log(' ------------------------------------------------------------');
|
|
560
|
+
console.log(full && full.body ? full.body : '(body not loaded)');
|
|
561
|
+
if (full) {
|
|
562
|
+
const flagStr = review.formatFlags(full);
|
|
563
|
+
if (flagStr) console.log(` ⚠ flags: ${flagStr}`);
|
|
564
|
+
}
|
|
565
|
+
console.log(' ------------------------------------------------------------');
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (['y', 'n', 's', 'q'].includes(choice)) break;
|
|
422
569
|
}
|
|
423
570
|
if (choice === 'q') { console.log(' Stopping. Remaining items left pending.'); break; }
|
|
424
|
-
if (choice === 's') { skipped += 1;
|
|
571
|
+
if (choice === 's') { skipped += 1; continue; }
|
|
425
572
|
try {
|
|
426
|
-
if (choice === '
|
|
427
|
-
await review.submitDecision({ apiKey, baseUrl, id:
|
|
428
|
-
approved += 1; console.log('
|
|
573
|
+
if (choice === 'y') {
|
|
574
|
+
await review.submitDecision({ apiKey, baseUrl, id: row.id, decision: 'approve' });
|
|
575
|
+
approved += 1; console.log(' ✓ approved, now live');
|
|
429
576
|
} else {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
rejected += 1; console.log(' ✗ rejected — stays private');
|
|
577
|
+
await review.submitDecision({ apiKey, baseUrl, id: row.id, decision: 'reject' });
|
|
578
|
+
rejected += 1; console.log(' ✗ rejected, stays private');
|
|
433
579
|
}
|
|
434
580
|
} catch (err) {
|
|
435
|
-
console.error(`
|
|
581
|
+
console.error(` ! decision failed: ${err.message} (item left pending)`);
|
|
436
582
|
}
|
|
437
583
|
}
|
|
438
584
|
|
|
439
|
-
console.log(`\nReview complete: approved ${approved}, rejected ${rejected}, skipped ${skipped} of ${
|
|
585
|
+
console.log(`\nReview complete: approved ${approved}, rejected ${rejected}, skipped ${skipped} of ${ordered.length}.`);
|
|
440
586
|
}
|
|
441
587
|
|
|
442
588
|
// ─── Entry point ────────────────────────────────────────────────────────────
|
|
@@ -451,8 +597,24 @@ Commands:
|
|
|
451
597
|
Flags: --re-auth, --base-url <url>
|
|
452
598
|
status Show install/auth/extraction status.
|
|
453
599
|
review Review YOUR pending-review learnings (from background extraction)
|
|
454
|
-
and approve/reject
|
|
455
|
-
|
|
600
|
+
and approve/reject before anything goes public. Default: triage
|
|
601
|
+
summary table first (quality desc, clean vs flagged), then rapid
|
|
602
|
+
y/n/s review.
|
|
603
|
+
Flags:
|
|
604
|
+
--list summary table only, no changes
|
|
605
|
+
--approve-clean bulk-approve items that passed EVERY platform
|
|
606
|
+
screen AND quality >= threshold (default 14;
|
|
607
|
+
tune with --min-quality N). Prints the exact
|
|
608
|
+
list, then requires typing the count.
|
|
609
|
+
--all bulk-approve everything EXCEPT screen-flagged
|
|
610
|
+
items (add --include-flagged to include them).
|
|
611
|
+
Same typed-count confirmation.
|
|
612
|
+
--min-quality N quality threshold for --approve-clean (0-20)
|
|
613
|
+
--all-reject reject the whole batch [--yes for scripted
|
|
614
|
+
incident response; rejects stay private]
|
|
615
|
+
--base-url <url>
|
|
616
|
+
Bulk approvals ALWAYS require typing the exact count. No flag
|
|
617
|
+
skips that step.
|
|
456
618
|
disable Turn off background extraction (local kill-switch; optional
|
|
457
619
|
server-side consent revoke).
|
|
458
620
|
|
package/lib/installer.js
CHANGED
|
@@ -31,7 +31,7 @@ const path = require('path');
|
|
|
31
31
|
const MCP_ENTRY = Object.freeze({ command: 'npx', args: ['auxilo-mcp'] });
|
|
32
32
|
|
|
33
33
|
/** Default production API base (README.md / openapi.json servers[0]). */
|
|
34
|
-
const DEFAULT_BASE_URL = 'https://
|
|
34
|
+
const DEFAULT_BASE_URL = 'https://auxilo.io';
|
|
35
35
|
|
|
36
36
|
/** Root of the installed npm package (or the repo, when run from a clone). */
|
|
37
37
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
@@ -47,6 +47,10 @@ const RUNNER_STACK = Object.freeze([
|
|
|
47
47
|
// [package-relative source, bin-relative dest, mode]
|
|
48
48
|
['scripts/runner.js', 'scripts/runner.js', 0o755],
|
|
49
49
|
['scripts/capture-core.js', 'scripts/capture-core.js', 0o755], // UC-1 shim target
|
|
50
|
+
// Client-side extraction module: runner.js postExtract() requires './extract-local.js'
|
|
51
|
+
// at first extraction; omitting it from the installed stack is a MODULE_NOT_FOUND
|
|
52
|
+
// for every npm-installed user (test/runner-packaging-closure.test.js guards this).
|
|
53
|
+
['scripts/extract-local.js', 'scripts/extract-local.js', 0o644],
|
|
50
54
|
['scripts/sources/source.interface.js', 'scripts/sources/source.interface.js', 0o644],
|
|
51
55
|
['scripts/sources/claude-code.js', 'scripts/sources/claude-code.js', 0o644],
|
|
52
56
|
['scripts/sources/openclaw.js', 'scripts/sources/openclaw.js', 0o644],
|
package/lib/review.js
CHANGED
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
*
|
|
12
12
|
* extract -> pending_review -> `auxilo review` (this) -> approve/reject
|
|
13
13
|
*
|
|
14
|
-
* That loop is what
|
|
15
|
-
*
|
|
14
|
+
* That loop is what keeps background extraction safe to leave on: anything a
|
|
15
|
+
* platform screen flags stays 'pending_review' (out of the public catalog)
|
|
16
|
+
* until the contributor personally approves it here. Clean extractions publish
|
|
17
|
+
* seamlessly with a 7-day retraction window and never enter this queue; manual
|
|
18
|
+
* mode (account settings) routes every draft through it instead.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
|
-
const DEFAULT_BASE_URL = 'https://
|
|
21
|
+
const DEFAULT_BASE_URL = 'https://auxilo.io';
|
|
19
22
|
|
|
20
23
|
function normBase(baseUrl) {
|
|
21
24
|
return (baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
@@ -116,9 +119,173 @@ function formatFlags(l) {
|
|
|
116
119
|
return parts.join(' · ');
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
// ── Review-seamless additions (2026-07-18) ──────────────────────────────────
|
|
123
|
+
//
|
|
124
|
+
// Summary + bulk network calls, plus the pure approve-clean selection logic
|
|
125
|
+
// shared by the CLI, the MCP auxilo_review tool, and the triage report script.
|
|
126
|
+
// One selection implementation everywhere, so what a dry run PRINTS is exactly
|
|
127
|
+
// what a confirmed run DOES.
|
|
128
|
+
|
|
129
|
+
/** Server-enforced max decisions per bulk call; clients chunk at this size. */
|
|
130
|
+
const BULK_CHUNK = 100;
|
|
131
|
+
|
|
132
|
+
/** Default approve-clean quality threshold (repo quality gate: total >= 14/20). */
|
|
133
|
+
const DEFAULT_QUALITY_THRESHOLD = 14;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* GET /account/pending/summary: compact triage rows + counts (no bodies).
|
|
137
|
+
*
|
|
138
|
+
* @param {object} opts { apiKey, baseUrl?, fetchImpl? }
|
|
139
|
+
* @returns {Promise<object>} summary body (pending_count, clean_count, items, ...)
|
|
140
|
+
*/
|
|
141
|
+
async function fetchPendingSummary(opts = {}) {
|
|
142
|
+
const { apiKey } = opts;
|
|
143
|
+
if (!apiKey) throw new Error('fetchPendingSummary: apiKey is required');
|
|
144
|
+
const baseUrl = normBase(opts.baseUrl);
|
|
145
|
+
const fetchImpl = opts.fetchImpl || fetch;
|
|
146
|
+
|
|
147
|
+
const res = await fetchImpl(`${baseUrl}/account/pending/summary`, {
|
|
148
|
+
method: 'GET',
|
|
149
|
+
headers: { 'X-API-Key': apiKey },
|
|
150
|
+
});
|
|
151
|
+
let body = {};
|
|
152
|
+
try { body = await res.json(); } catch { /* non-JSON error body */ }
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
throw new Error(`Fetch pending summary failed (HTTP ${res.status}): ${body.error || 'unknown error'}`);
|
|
155
|
+
}
|
|
156
|
+
return body;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* POST /account/pending/bulk: submit ONE chunk of decisions (<= BULK_CHUNK).
|
|
161
|
+
* confirm_count is set to decisions.length here because by the time this is
|
|
162
|
+
* called, the human-facing layer has already shown the exact list and count
|
|
163
|
+
* and received the operator's counted confirmation. This function never
|
|
164
|
+
* invents decisions; it transmits what was confirmed.
|
|
165
|
+
*
|
|
166
|
+
* @param {object} opts { apiKey, decisions, baseUrl?, fetchImpl? }
|
|
167
|
+
* @returns {Promise<object>} server response (counts + per-id results)
|
|
168
|
+
*/
|
|
169
|
+
async function submitBulk(opts = {}) {
|
|
170
|
+
const { apiKey, decisions } = opts;
|
|
171
|
+
if (!apiKey) throw new Error('submitBulk: apiKey is required');
|
|
172
|
+
if (!Array.isArray(decisions) || decisions.length === 0) {
|
|
173
|
+
throw new Error('submitBulk: decisions must be a non-empty array');
|
|
174
|
+
}
|
|
175
|
+
if (decisions.length > BULK_CHUNK) {
|
|
176
|
+
throw new Error(`submitBulk: at most ${BULK_CHUNK} decisions per call (use chunkDecisions)`);
|
|
177
|
+
}
|
|
178
|
+
const baseUrl = normBase(opts.baseUrl);
|
|
179
|
+
const fetchImpl = opts.fetchImpl || fetch;
|
|
180
|
+
|
|
181
|
+
const res = await fetchImpl(`${baseUrl}/account/pending/bulk`, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
|
|
184
|
+
body: JSON.stringify({ decisions, confirm_count: decisions.length }),
|
|
185
|
+
});
|
|
186
|
+
let body = {};
|
|
187
|
+
try { body = await res.json(); } catch { /* non-JSON error body */ }
|
|
188
|
+
if (!res.ok) {
|
|
189
|
+
throw new Error(`Bulk review failed (HTTP ${res.status}): ${body.error || 'unknown error'}`);
|
|
190
|
+
}
|
|
191
|
+
return body;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Split an array into chunks of at most `size` (default BULK_CHUNK). */
|
|
195
|
+
function chunkDecisions(list, size = BULK_CHUNK) {
|
|
196
|
+
const n = Math.max(1, size);
|
|
197
|
+
const out = [];
|
|
198
|
+
for (let i = 0; i < list.length; i += n) out.push(list.slice(i, i + n));
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Submit decisions through the bulk endpoint in sequential chunks, aggregating
|
|
204
|
+
* counts and per-id results. onChunk (optional) is called after each chunk for
|
|
205
|
+
* progress rendering. A failed chunk stops the run (already-applied chunks
|
|
206
|
+
* stay applied; the endpoint is idempotent per id, so re-running is safe).
|
|
207
|
+
*
|
|
208
|
+
* @param {object} opts { apiKey, decisions, baseUrl?, fetchImpl?, onChunk? }
|
|
209
|
+
* @returns {Promise<{approved:number, rejected:number, idempotent:number, failed:number, results:Array<object>}>}
|
|
210
|
+
*/
|
|
211
|
+
async function submitBulkChunked(opts = {}) {
|
|
212
|
+
const { decisions } = opts;
|
|
213
|
+
const totals = { approved: 0, rejected: 0, idempotent: 0, failed: 0, results: [] };
|
|
214
|
+
const chunks = chunkDecisions(decisions || []);
|
|
215
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
216
|
+
const resp = await submitBulk({ ...opts, decisions: chunks[i] });
|
|
217
|
+
totals.approved += resp.approved || 0;
|
|
218
|
+
totals.rejected += resp.rejected || 0;
|
|
219
|
+
totals.idempotent += resp.idempotent || 0;
|
|
220
|
+
totals.failed += resp.failed || 0;
|
|
221
|
+
if (Array.isArray(resp.results)) totals.results.push(...resp.results);
|
|
222
|
+
if (typeof opts.onChunk === 'function') {
|
|
223
|
+
opts.onChunk({ chunkIndex: i, chunkCount: chunks.length, chunkSize: chunks[i].length, response: resp });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return totals;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** True when a summary row's quality clears the threshold (threshold <= 0 accepts unscored). */
|
|
230
|
+
function qualityClears(row, minQuality) {
|
|
231
|
+
if (minQuality <= 0) return true;
|
|
232
|
+
return row.quality != null && row.quality >= minQuality;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* THE approve-clean selection. Operates on summary rows (screens_passed,
|
|
237
|
+
* flags, quality) from GET /account/pending/summary.
|
|
238
|
+
*
|
|
239
|
+
* mode 'clean' (approve-clean): screens_passed AND quality >= minQuality.
|
|
240
|
+
* mode 'all': every row EXCEPT screens-flagged ones unless includeFlagged
|
|
241
|
+
* is set; no quality gate.
|
|
242
|
+
*
|
|
243
|
+
* Returns the selection plus what was excluded and why, so every confirmation
|
|
244
|
+
* surface can show the operator the exact consequences before anything runs.
|
|
245
|
+
*
|
|
246
|
+
* @param {Array<object>} rows
|
|
247
|
+
* @param {object} [opts] { mode: 'clean'|'all', minQuality, includeFlagged }
|
|
248
|
+
* @returns {{selected:Array<object>, excluded_flagged:Array<object>, excluded_low_quality:Array<object>, excluded_unscored:Array<object>, min_quality:number|null}}
|
|
249
|
+
*/
|
|
250
|
+
function selectForBulkApprove(rows, opts = {}) {
|
|
251
|
+
const mode = opts.mode === 'all' ? 'all' : 'clean';
|
|
252
|
+
const minQuality = mode === 'clean'
|
|
253
|
+
? (Number.isFinite(opts.minQuality) ? opts.minQuality : DEFAULT_QUALITY_THRESHOLD)
|
|
254
|
+
: null;
|
|
255
|
+
const includeFlagged = mode === 'all' && opts.includeFlagged === true;
|
|
256
|
+
|
|
257
|
+
const selected = [];
|
|
258
|
+
const excluded_flagged = [];
|
|
259
|
+
const excluded_low_quality = [];
|
|
260
|
+
const excluded_unscored = [];
|
|
261
|
+
|
|
262
|
+
for (const row of rows || []) {
|
|
263
|
+
if (!row || !row.id) continue;
|
|
264
|
+
if (!row.screens_passed && !includeFlagged) {
|
|
265
|
+
excluded_flagged.push(row);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (mode === 'clean' && !qualityClears(row, minQuality)) {
|
|
269
|
+
(row.quality == null ? excluded_unscored : excluded_low_quality).push(row);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
selected.push(row);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { selected, excluded_flagged, excluded_low_quality, excluded_unscored, min_quality: minQuality };
|
|
276
|
+
}
|
|
277
|
+
|
|
119
278
|
module.exports = {
|
|
120
279
|
DEFAULT_BASE_URL,
|
|
280
|
+
BULK_CHUNK,
|
|
281
|
+
DEFAULT_QUALITY_THRESHOLD,
|
|
121
282
|
fetchPending,
|
|
122
283
|
submitDecision,
|
|
123
284
|
formatFlags,
|
|
285
|
+
fetchPendingSummary,
|
|
286
|
+
submitBulk,
|
|
287
|
+
chunkDecisions,
|
|
288
|
+
submitBulkChunked,
|
|
289
|
+
qualityClears,
|
|
290
|
+
selectForBulkApprove,
|
|
124
291
|
};
|