auxilo-mcp 0.9.1 → 0.9.3
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 +130 -149
- package/bin/auxilo-cli.js +435 -99
- package/lib/installer.js +397 -13
- package/lib/review.js +170 -3
- package/mcp-server.js +247 -150
- package/package.json +15 -3
- package/scripts/extract-local.js +204 -0
- package/scripts/review-notice.js +145 -0
- package/scripts/runner.js +171 -52
- package/scripts/sources/cline.js +160 -0
- package/scripts/sources/continue.js +140 -0
- package/scripts/sources/roo-code.js +35 -0
package/bin/auxilo-cli.js
CHANGED
|
@@ -90,14 +90,25 @@ 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
|
-
every run in ~/.auxilo/extract.log. Saying No installs the MCP server only
|
|
109
|
+
every run in ~/.auxilo/extract.log. Saying No installs the MCP server only —
|
|
110
|
+
no session-end capture hook is written into any client config unless you say
|
|
111
|
+
Yes, and any capture hooks left by an earlier install are removed.
|
|
101
112
|
`;
|
|
102
113
|
|
|
103
114
|
async function cmdSetup(flags) {
|
|
@@ -185,7 +196,11 @@ async function cmdSetup(flags) {
|
|
|
185
196
|
}
|
|
186
197
|
}
|
|
187
198
|
|
|
188
|
-
// 4. Runner install +
|
|
199
|
+
// 4. Runner install + review notice -----------------------------------------
|
|
200
|
+
// UC-1a (GOV-3): NO capture hook is written into any third-party client
|
|
201
|
+
// config here — only files under our own ~/.auxilo/bin, plus the
|
|
202
|
+
// SessionStart review notice (a count-only account-status surface, not a
|
|
203
|
+
// capture hook). Capture hooks are written ONLY after consent=Yes (step 5).
|
|
189
204
|
console.log('');
|
|
190
205
|
try {
|
|
191
206
|
const { binRoot } = installer.installRunner(HOME);
|
|
@@ -197,54 +212,95 @@ async function cmdSetup(flags) {
|
|
|
197
212
|
|
|
198
213
|
const claudeCode = chosen.find((c) => c.id === 'claude-code');
|
|
199
214
|
if (claudeCode) {
|
|
215
|
+
// LW-18: SessionStart held-count notice ("N learnings held for your
|
|
216
|
+
// review") — fail-silent, ≤1 per 4h, count-only. Installed regardless of
|
|
217
|
+
// extraction consent: the review queue also holds MCP contributions.
|
|
200
218
|
try {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
console.log(` (replaced legacy hook entr${hook.removedLegacy.length === 1 ? 'y' : 'ies'}: ${hook.removedLegacy.join(', ')})`);
|
|
206
|
-
}
|
|
207
|
-
} else {
|
|
208
|
-
console.log(' ✓ Claude Code SessionEnd hook already registered (no changes)');
|
|
209
|
-
}
|
|
219
|
+
const notice = installer.registerClaudeCodeSessionStartNotice(HOME);
|
|
220
|
+
console.log(notice.changed
|
|
221
|
+
? ` ✓ Claude Code SessionStart review notice registered (${notice.hookCmd})`
|
|
222
|
+
: ' ✓ Claude Code SessionStart review notice already registered (no changes)');
|
|
210
223
|
} catch (err) {
|
|
211
|
-
console.error(` ✗
|
|
224
|
+
console.error(` ✗ Review notice SKIPPED — ${err.message}`);
|
|
212
225
|
}
|
|
213
226
|
}
|
|
214
227
|
|
|
215
|
-
//
|
|
216
|
-
try {
|
|
217
|
-
for (const r of installer.installCaptureHooks(HOME, chosen)) {
|
|
218
|
-
if (r.error) {
|
|
219
|
-
console.error(` ✗ ${r.name}: SKIPPED — ${r.error}`);
|
|
220
|
-
} else if (r.changed) {
|
|
221
|
-
console.log(` ✓ ${r.name}: capture hook registered (${r.event})`);
|
|
222
|
-
if (r.notes) console.log(` ${r.notes}`);
|
|
223
|
-
} else {
|
|
224
|
-
console.log(` ✓ ${r.name}: capture hook already registered`);
|
|
225
|
-
if (r.notes) console.log(` ${r.notes}`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} catch (err) {
|
|
229
|
-
console.error(` ✗ Capture hooks SKIPPED — ${err.message}`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// 5. Explicit consent (default No) ------------------------------------------
|
|
228
|
+
// 5. Explicit consent (default No), THEN capture hooks ----------------------
|
|
233
229
|
console.log(CONSENT_TEXT);
|
|
234
230
|
const consented = await askYesNo(' Enable background extraction?', false);
|
|
231
|
+
let extractionArmed = false;
|
|
235
232
|
if (consented) {
|
|
236
233
|
try {
|
|
237
234
|
await installer.recordConsent({ action: 'grant', apiKey: creds.api_key, baseUrl });
|
|
238
235
|
installer.enableSentinel(HOME);
|
|
236
|
+
extractionArmed = true;
|
|
239
237
|
console.log(' ✓ Consent recorded; background extraction ENABLED (~/.auxilo/autonomous-enabled)');
|
|
240
238
|
} catch (err) {
|
|
241
239
|
console.error(` ✗ Could not record consent on server: ${err.message}`);
|
|
242
|
-
console.error(' Background extraction NOT enabled (no sentinel created). Re-run `auxilo setup` to retry.');
|
|
240
|
+
console.error(' Background extraction NOT enabled (no sentinel created, no hooks written). Re-run `auxilo setup` to retry.');
|
|
243
241
|
}
|
|
244
242
|
} else {
|
|
245
243
|
console.log(' ✓ Background extraction left OFF (MCP-only install). Enable later by re-running `auxilo setup`.');
|
|
246
244
|
}
|
|
247
245
|
|
|
246
|
+
if (extractionArmed) {
|
|
247
|
+
// Consent recorded — NOW write the capture hooks (UC-1a ordering).
|
|
248
|
+
if (claudeCode) {
|
|
249
|
+
try {
|
|
250
|
+
const hook = installer.registerClaudeCodeHook(HOME);
|
|
251
|
+
if (hook.changed) {
|
|
252
|
+
console.log(` ✓ Claude Code SessionEnd hook registered (${hook.hookCmd})`);
|
|
253
|
+
if (hook.removedLegacy.length > 0) {
|
|
254
|
+
console.log(` (replaced legacy hook entr${hook.removedLegacy.length === 1 ? 'y' : 'ies'}: ${hook.removedLegacy.join(', ')})`);
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
console.log(' ✓ Claude Code SessionEnd hook already registered (no changes)');
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error(` ✗ Hook registration SKIPPED — ${err.message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// UC-1: capture hooks for every other chosen client that supports one.
|
|
265
|
+
try {
|
|
266
|
+
for (const r of installer.installCaptureHooks(HOME, chosen)) {
|
|
267
|
+
if (r.error) {
|
|
268
|
+
console.error(` ✗ ${r.name}: SKIPPED — ${r.error}`);
|
|
269
|
+
} else if (r.changed) {
|
|
270
|
+
console.log(` ✓ ${r.name}: capture hook registered (${r.event})`);
|
|
271
|
+
if (r.notes) console.log(` ${r.notes}`);
|
|
272
|
+
} else {
|
|
273
|
+
console.log(` ✓ ${r.name}: capture hook already registered`);
|
|
274
|
+
if (r.notes) console.log(` ${r.notes}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.error(` ✗ Capture hooks SKIPPED — ${err.message}`);
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
// UC-1a: consent not granted (No, or consent record failed) — ensure NO
|
|
282
|
+
// capture-hook artifact remains in any client config, including ones a
|
|
283
|
+
// pre-UC-1a install wrote before the consent step.
|
|
284
|
+
try {
|
|
285
|
+
const removedHook = installer.removeClaudeCodeHook(HOME);
|
|
286
|
+
if (removedHook.changed) {
|
|
287
|
+
console.log(` ✓ Removed pre-existing Claude Code SessionEnd capture hook (${removedHook.removed.join(', ')})`);
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error(` ✗ Claude Code hook cleanup SKIPPED — ${err.message}`);
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const allClients = installer.detectClients(HOME); // clean beyond the chosen set
|
|
294
|
+
for (const r of installer.removeCaptureHooks(HOME, allClients)) {
|
|
295
|
+
if (r.error) console.error(` ✗ ${r.name}: capture-hook cleanup SKIPPED — ${r.error}`);
|
|
296
|
+
else if (r.changed) console.log(` ✓ ${r.name}: removed pre-existing capture hook`);
|
|
297
|
+
}
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.error(` ✗ Capture-hook cleanup SKIPPED — ${err.message}`);
|
|
300
|
+
}
|
|
301
|
+
console.log(' ✓ No capture hooks are present in any client config.');
|
|
302
|
+
}
|
|
303
|
+
|
|
248
304
|
// 6. Rules-file snippet (UC-0 agent-prompted contribution; opt-in, default No)
|
|
249
305
|
const rulesClients = chosen.filter((c) => c.rulesPath);
|
|
250
306
|
if (rulesClients.length > 0) {
|
|
@@ -275,6 +331,119 @@ async function cmdSetup(flags) {
|
|
|
275
331
|
console.log('\nDone. Check `npx auxilo status` any time. Restart your client(s) to pick up the MCP server.\n');
|
|
276
332
|
}
|
|
277
333
|
|
|
334
|
+
// ─── auxilo init (NF-3, Wave 3.4) ───────────────────────────────────────────
|
|
335
|
+
//
|
|
336
|
+
// Non-interactive builder onboarding: mint a SCOPED key for CI or a second
|
|
337
|
+
// machine WITHOUT re-running full setup (no client detection, no hooks, no
|
|
338
|
+
// consent flow, and — unless --save — no touch of ~/.auxilo/credentials.json).
|
|
339
|
+
// ZERO prompts by design: every input is a flag with a default, so piped/
|
|
340
|
+
// scripted stdin can never hang (LW-17 lesson class; init never opens the
|
|
341
|
+
// shared readline at all). The only human step is the device-code approval
|
|
342
|
+
// in a browser, which can happen on any machine.
|
|
343
|
+
|
|
344
|
+
const INIT_SCOPES = ['read', 'earnings-read', 'contribute'];
|
|
345
|
+
|
|
346
|
+
async function cmdInit(flags) {
|
|
347
|
+
const baseUrl = resolveBaseUrl(flags);
|
|
348
|
+
|
|
349
|
+
const scope = flags.scope === undefined ? 'contribute' : String(flags.scope);
|
|
350
|
+
if (!INIT_SCOPES.includes(scope)) {
|
|
351
|
+
console.error(`Invalid --scope '${scope}'. Must be one of: ${INIT_SCOPES.join(', ')}.`);
|
|
352
|
+
console.error('(admin keys cannot be created via the API or device flow.)');
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
const label = (flags.label === undefined || flags.label === true
|
|
356
|
+
? `init-${os.hostname()}`
|
|
357
|
+
: String(flags.label)).trim().slice(0, 50);
|
|
358
|
+
|
|
359
|
+
if (!flags.json) {
|
|
360
|
+
console.log('\nAuxilo init — mint a scoped API key');
|
|
361
|
+
console.log('===================================');
|
|
362
|
+
console.log(`Server: ${baseUrl}`);
|
|
363
|
+
console.log(`Scope: ${scope} Label: ${label}\n`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let result;
|
|
367
|
+
try {
|
|
368
|
+
result = await installer.deviceLogin({
|
|
369
|
+
baseUrl,
|
|
370
|
+
scope,
|
|
371
|
+
label,
|
|
372
|
+
onCode: (code, url) => {
|
|
373
|
+
if (!flags.json) {
|
|
374
|
+
console.log(` Your device code: ${code}`);
|
|
375
|
+
console.log(` Approve at: ${url}`);
|
|
376
|
+
console.log(' Waiting for authorization (Ctrl-C to abort)...\n');
|
|
377
|
+
} else {
|
|
378
|
+
// --json keeps stdout machine-readable; the human-step info goes to stderr.
|
|
379
|
+
console.error(`device_code=${code} approve_at=${url}`);
|
|
380
|
+
}
|
|
381
|
+
if (!flags['no-browser']) openBrowser(url);
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.error(`✗ init failed: ${err.message}`);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Pre-Wave-3.4 servers ignore the requested scope and mint 'contribute'
|
|
390
|
+
// (and don't echo). Detect and WARN — never silently mislabel a credential.
|
|
391
|
+
const grantedScope = result.scope || 'contribute';
|
|
392
|
+
const grantedLabel = result.label || label;
|
|
393
|
+
if (grantedScope !== scope) {
|
|
394
|
+
console.error(` ! Server granted scope '${grantedScope}' (requested '${scope}') — older server version. The key below carries '${grantedScope}'.`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (flags['env-file']) {
|
|
398
|
+
const envPath = String(flags['env-file']);
|
|
399
|
+
try {
|
|
400
|
+
const w = installer.writeEnvFile(envPath, { api_key: result.api_key, base_url: baseUrl });
|
|
401
|
+
if (!flags.json) {
|
|
402
|
+
console.log(` ✓ ${w.created ? 'Created' : 'Updated'} ${w.path} (AUXILO_API_KEY, mode 0600)`);
|
|
403
|
+
}
|
|
404
|
+
} catch (err) {
|
|
405
|
+
// The key was already minted — surface it rather than losing it.
|
|
406
|
+
console.error(` ✗ Could not write ${envPath}: ${err.message}`);
|
|
407
|
+
console.error(' Your key (shown once — store it now):');
|
|
408
|
+
console.error(` ${result.api_key}`);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (flags.save) {
|
|
414
|
+
installer.writeCredentials(HOME, {
|
|
415
|
+
api_key: result.api_key,
|
|
416
|
+
base_url: baseUrl,
|
|
417
|
+
email: result.email,
|
|
418
|
+
account_id: result.account_id,
|
|
419
|
+
});
|
|
420
|
+
if (!flags.json) console.log(' ✓ Saved to ~/.auxilo/credentials.json (mode 0600) — this machine now uses this key.');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (flags.json) {
|
|
424
|
+
console.log(JSON.stringify({
|
|
425
|
+
api_key: result.api_key,
|
|
426
|
+
scope: grantedScope,
|
|
427
|
+
label: grantedLabel,
|
|
428
|
+
account_id: result.account_id,
|
|
429
|
+
email: result.email,
|
|
430
|
+
base_url: baseUrl,
|
|
431
|
+
env_file: flags['env-file'] ? String(flags['env-file']) : null,
|
|
432
|
+
saved_credentials: !!flags.save,
|
|
433
|
+
}));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
console.log(` ✓ Key minted for ${result.email || result.account_id} (scope: ${grantedScope}, label: ${grantedLabel})`);
|
|
438
|
+
if (!flags['env-file']) {
|
|
439
|
+
console.log('\n Your API key (shown ONCE — store it now, e.g. in your CI secret store):');
|
|
440
|
+
console.log(` ${result.api_key}\n`);
|
|
441
|
+
console.log(' Use it as the X-API-Key header, or write it to an env file next time with --env-file <path>.');
|
|
442
|
+
}
|
|
443
|
+
console.log(' Rotate later: POST /account/api-keys/rotate with this key (self-rotate).');
|
|
444
|
+
console.log(' Revoke later: from the dashboard, or DELETE /account/api-keys/:label with this key.\n');
|
|
445
|
+
}
|
|
446
|
+
|
|
278
447
|
// ─── auxilo status ──────────────────────────────────────────────────────────
|
|
279
448
|
|
|
280
449
|
async function cmdStatus() {
|
|
@@ -338,20 +507,108 @@ async function cmdDisable(flags) {
|
|
|
338
507
|
// The human gate that makes background extraction safe to re-enable:
|
|
339
508
|
// extract -> pending_review -> `auxilo review` -> approve the safe ones.
|
|
340
509
|
// Operates ONLY on the caller's own pending learnings (account-scoped API key).
|
|
510
|
+
//
|
|
511
|
+
// Review-seamless (2026-07-18): the default view is now a triage summary table
|
|
512
|
+
// (quality desc, clean before flagged), with bulk modes that batch through
|
|
513
|
+
// POST /account/pending/bulk. HARD RULE for every bulk APPROVE path: print the
|
|
514
|
+
// exact list and count first, then require the operator to TYPE THE COUNT.
|
|
515
|
+
// There is no --yes bypass on any approve path; publishing always costs one
|
|
516
|
+
// typed number (2026-06-10 mass-publish lesson).
|
|
517
|
+
|
|
518
|
+
/** Truncate + pad a string to an exact width (for the triage table). */
|
|
519
|
+
function fit(s, width) {
|
|
520
|
+
const str = String(s == null ? '' : s);
|
|
521
|
+
if (str.length > width) return str.slice(0, Math.max(0, width - 1)) + '…';
|
|
522
|
+
return str.padEnd(width);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Short flag codes for a summary row: 'clean' or e.g. 'inj+sens'. */
|
|
526
|
+
function shortFlags(row) {
|
|
527
|
+
if (row.screens_passed) return 'clean';
|
|
528
|
+
const map = { injection: 'inj', content_sensitivity: 'sens', near_duplicate: 'dup' };
|
|
529
|
+
return (row.flags || []).map((f) => map[f] || f).join('+') || 'flagged';
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** One compact triage line (shared by the table and rapid mode). */
|
|
533
|
+
function triageLine(row, n, total) {
|
|
534
|
+
const q = row.quality == null ? ' --' : String(row.quality).padStart(3);
|
|
535
|
+
return ` ${String(n).padStart(String(total).length)}. q=${q} ${fit(shortFlags(row), 9)} ${fit(row.category || '', 20)} ${fit(row.title || '(no title)', 52)}`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Print the triage summary: counts, then clean rows, then flagged rows. */
|
|
539
|
+
function printSummaryTable(summary) {
|
|
540
|
+
const items = summary.items || [];
|
|
541
|
+
const clean = items.filter((r) => r.screens_passed);
|
|
542
|
+
const flagged = items.filter((r) => !r.screens_passed);
|
|
543
|
+
|
|
544
|
+
console.log(`\n${summary.pending_count} learning(s) pending your review: ${clean.length} passed every platform screen, ${flagged.length} flagged.`);
|
|
545
|
+
const bands = summary.counts && summary.counts.by_quality_band;
|
|
546
|
+
if (bands) {
|
|
547
|
+
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}`);
|
|
548
|
+
}
|
|
549
|
+
if (Array.isArray(summary.near_dup_clusters) && summary.near_dup_clusters.length > 0) {
|
|
550
|
+
console.log(`Near-duplicate clusters among your pending items: ${summary.near_dup_clusters.length} (review these together)`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (clean.length > 0) {
|
|
554
|
+
console.log(`\nCLEAN (passed every screen) - sorted by quality:`);
|
|
555
|
+
clean.forEach((r, i) => console.log(triageLine(r, i + 1, clean.length)));
|
|
556
|
+
}
|
|
557
|
+
if (flagged.length > 0) {
|
|
558
|
+
console.log(`\nFLAGGED (a platform screen raised a signal) - review individually:`);
|
|
559
|
+
flagged.forEach((r, i) => console.log(triageLine(r, i + 1, flagged.length)));
|
|
560
|
+
}
|
|
561
|
+
console.log('');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* The counted confirmation: print the exact selection, then require the
|
|
566
|
+
* operator to type the exact count. Returns true ONLY on an exact match.
|
|
567
|
+
* No flag bypasses this for approve paths.
|
|
568
|
+
*/
|
|
569
|
+
async function confirmByTypedCount(rows, actionLabel) {
|
|
570
|
+
console.log(`\n${actionLabel} the following ${rows.length} learning(s):`);
|
|
571
|
+
rows.forEach((r) => {
|
|
572
|
+
const q = r.quality == null ? '--' : r.quality;
|
|
573
|
+
console.log(` ${r.id} q=${q} [${shortFlags(r)}] ${r.title || '(no title)'}`);
|
|
574
|
+
});
|
|
575
|
+
console.log(`\nCount: ${rows.length}.`);
|
|
576
|
+
const answer = await ask(`Type the count (${rows.length}) to confirm, anything else aborts: `);
|
|
577
|
+
if (answer !== String(rows.length)) {
|
|
578
|
+
console.log('Aborted. Nothing changed.');
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
341
583
|
|
|
342
|
-
/**
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
584
|
+
/** Run a confirmed selection through the bulk endpoint in chunks, with progress. */
|
|
585
|
+
async function runBulk({ apiKey, baseUrl, rows, decision, reason }) {
|
|
586
|
+
const decisions = rows.map((r) => (decision === 'reject' && reason)
|
|
587
|
+
? { id: r.id, decision, reason }
|
|
588
|
+
: { id: r.id, decision });
|
|
589
|
+
const totals = await review.submitBulkChunked({
|
|
590
|
+
apiKey,
|
|
591
|
+
baseUrl,
|
|
592
|
+
decisions,
|
|
593
|
+
onChunk: ({ chunkIndex, chunkCount, response }) => {
|
|
594
|
+
console.log(` chunk ${chunkIndex + 1}/${chunkCount}: approved ${response.approved || 0}, rejected ${response.rejected || 0}, already done ${response.idempotent || 0}, failed ${response.failed || 0}`);
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
for (const r of totals.results) {
|
|
598
|
+
if (!r.ok) console.error(` ! ${r.id || `entry #${r.index}`}: ${r.error || r.code}`);
|
|
599
|
+
}
|
|
600
|
+
return totals;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Parse --min-quality into an integer 0-20 (default DEFAULT_QUALITY_THRESHOLD). */
|
|
604
|
+
function parseMinQuality(flags) {
|
|
605
|
+
if (flags['min-quality'] === undefined) return review.DEFAULT_QUALITY_THRESHOLD;
|
|
606
|
+
const n = parseInt(flags['min-quality'], 10);
|
|
607
|
+
if (!Number.isFinite(n) || n < 0 || n > 20) {
|
|
608
|
+
console.error('Invalid --min-quality (expected an integer 0-20).');
|
|
609
|
+
process.exit(1);
|
|
610
|
+
}
|
|
611
|
+
return n;
|
|
355
612
|
}
|
|
356
613
|
|
|
357
614
|
async function cmdReview(flags) {
|
|
@@ -363,80 +620,129 @@ async function cmdReview(flags) {
|
|
|
363
620
|
const baseUrl = resolveBaseUrl(flags);
|
|
364
621
|
const apiKey = creds.api_key;
|
|
365
622
|
|
|
366
|
-
|
|
623
|
+
// ── Summary FIRST, always (triage before any decision surface) ────────────
|
|
624
|
+
let summary;
|
|
367
625
|
try {
|
|
368
|
-
|
|
626
|
+
summary = await review.fetchPendingSummary({ apiKey, baseUrl });
|
|
369
627
|
} catch (err) {
|
|
370
|
-
console.error(`Could not fetch pending review
|
|
628
|
+
console.error(`Could not fetch pending review summary: ${err.message}`);
|
|
371
629
|
process.exit(1);
|
|
372
630
|
}
|
|
373
631
|
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (items.length === 0) {
|
|
632
|
+
const rows = summary.items || [];
|
|
633
|
+
if (rows.length === 0) {
|
|
378
634
|
console.log('No learnings pending your review. Nothing to do.');
|
|
379
635
|
return;
|
|
380
636
|
}
|
|
381
637
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
638
|
+
printSummaryTable(summary);
|
|
639
|
+
|
|
640
|
+
// ── --list: summary only, no mutations ────────────────────────────────────
|
|
641
|
+
if (flags.list) return;
|
|
642
|
+
|
|
643
|
+
// ── --approve-clean: bulk-approve items that passed EVERY platform screen
|
|
644
|
+
// AND clear the quality threshold. Typed-count confirmation, no bypass. ──
|
|
645
|
+
if (flags['approve-clean']) {
|
|
646
|
+
const minQuality = parseMinQuality(flags);
|
|
647
|
+
const sel = review.selectForBulkApprove(rows, { mode: 'clean', minQuality });
|
|
648
|
+
console.log(`approve-clean selection: ${sel.selected.length} of ${rows.length} pending (threshold: quality >= ${minQuality}).`);
|
|
649
|
+
if (sel.excluded_flagged.length) console.log(` excluded ${sel.excluded_flagged.length} screen-flagged item(s) (review those individually).`);
|
|
650
|
+
if (sel.excluded_low_quality.length) console.log(` excluded ${sel.excluded_low_quality.length} below the quality threshold.`);
|
|
651
|
+
if (sel.excluded_unscored.length) console.log(` excluded ${sel.excluded_unscored.length} with no quality score (pass --min-quality 0 to include).`);
|
|
652
|
+
if (sel.selected.length === 0) { console.log('Nothing qualifies. No changes made.'); return; }
|
|
653
|
+
|
|
654
|
+
if (!await confirmByTypedCount(sel.selected, 'APPROVE and PUBLISH')) return;
|
|
655
|
+
const totals = await runBulk({ apiKey, baseUrl, rows: sel.selected, decision: 'approve' });
|
|
656
|
+
console.log(`\nDone. Approved ${totals.approved} (already approved earlier: ${totals.idempotent}, failed: ${totals.failed}). Flagged and below-threshold items stay pending.`);
|
|
390
657
|
return;
|
|
391
658
|
}
|
|
392
659
|
|
|
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
|
-
}
|
|
660
|
+
// ── --all: bulk-approve everything EXCEPT screen-flagged items unless
|
|
661
|
+
// --include-flagged. Typed-count confirmation, no bypass. ───────────────
|
|
662
|
+
if (flags.all) {
|
|
663
|
+
const includeFlagged = flags['include-flagged'] === true;
|
|
664
|
+
const sel = review.selectForBulkApprove(rows, { mode: 'all', includeFlagged });
|
|
665
|
+
if (includeFlagged && sel.selected.some((r) => !r.screens_passed)) {
|
|
666
|
+
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.');
|
|
667
|
+
} else if (sel.excluded_flagged.length) {
|
|
668
|
+
console.log(`Excluding ${sel.excluded_flagged.length} screen-flagged item(s) (pass --include-flagged to include them; safer to review those individually).`);
|
|
406
669
|
}
|
|
407
|
-
console.log(
|
|
670
|
+
if (sel.selected.length === 0) { console.log('Nothing qualifies. No changes made.'); return; }
|
|
671
|
+
|
|
672
|
+
if (!await confirmByTypedCount(sel.selected, 'APPROVE and PUBLISH')) return;
|
|
673
|
+
const totals = await runBulk({ apiKey, baseUrl, rows: sel.selected, decision: 'approve' });
|
|
674
|
+
console.log(`\nDone. Approved ${totals.approved} (already approved earlier: ${totals.idempotent}, failed: ${totals.failed}).`);
|
|
408
675
|
return;
|
|
409
676
|
}
|
|
410
677
|
|
|
411
|
-
// ──
|
|
412
|
-
|
|
413
|
-
|
|
678
|
+
// ── --all-reject: bulk reject the whole batch (incident escape hatch).
|
|
679
|
+
// Now batched through the bulk endpoint. --yes keeps its scripted-incident
|
|
680
|
+
// bypass because rejection is the SAFE direction (nothing goes public). ──
|
|
681
|
+
if (flags['all-reject']) {
|
|
682
|
+
const ok = flags.yes || await confirmByTypedCount(rows, 'REJECT (keep private)');
|
|
683
|
+
if (!ok) return;
|
|
684
|
+
const totals = await runBulk({ apiKey, baseUrl, rows, decision: 'reject' });
|
|
685
|
+
console.log(`\nDone. Rejected ${totals.rejected} of ${rows.length} (already rejected earlier: ${totals.idempotent}, failed: ${totals.failed}).`);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ── interactive rapid mode (default): y/n/s per item, one line each.
|
|
690
|
+
// [v] shows the full body before deciding; decisions post one at a time
|
|
691
|
+
// through the same single-item endpoints as before. ──────────────────────
|
|
692
|
+
const bodies = new Map();
|
|
693
|
+
try {
|
|
694
|
+
let offset = 0;
|
|
695
|
+
const pageLimit = 200;
|
|
696
|
+
for (;;) {
|
|
697
|
+
const page = await review.fetchPending({ apiKey, baseUrl, limit: pageLimit, offset });
|
|
698
|
+
for (const l of page.learnings || []) bodies.set(l.id, l);
|
|
699
|
+
offset += (page.learnings || []).length;
|
|
700
|
+
if (!page.learnings || page.learnings.length === 0 || offset >= (page.pending_count || 0)) break;
|
|
701
|
+
}
|
|
702
|
+
} catch (err) {
|
|
703
|
+
console.error(`Could not fetch pending bodies: ${err.message}`);
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
console.log('Rapid review. Per item: [y] approve (goes live) · [n] reject (stays private) · [v] view full body · [s] skip · [q] quit\n');
|
|
414
708
|
|
|
415
|
-
let approved = 0, rejected = 0, skipped = 0
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
709
|
+
let approved = 0, rejected = 0, skipped = 0;
|
|
710
|
+
const ordered = rows; // clean-first, quality desc (server order preserved by printSummaryTable groups)
|
|
711
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
712
|
+
const row = ordered[i];
|
|
713
|
+
console.log(triageLine(row, i + 1, ordered.length));
|
|
419
714
|
let choice = '';
|
|
420
|
-
|
|
421
|
-
choice = (await ask('
|
|
715
|
+
for (;;) {
|
|
716
|
+
choice = (await ask(' [y/n/v/s/q]? ')).toLowerCase().slice(0, 1);
|
|
717
|
+
if (choice === 'v') {
|
|
718
|
+
const full = bodies.get(row.id);
|
|
719
|
+
console.log(' ------------------------------------------------------------');
|
|
720
|
+
console.log(full && full.body ? full.body : '(body not loaded)');
|
|
721
|
+
if (full) {
|
|
722
|
+
const flagStr = review.formatFlags(full);
|
|
723
|
+
if (flagStr) console.log(` ⚠ flags: ${flagStr}`);
|
|
724
|
+
}
|
|
725
|
+
console.log(' ------------------------------------------------------------');
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (['y', 'n', 's', 'q'].includes(choice)) break;
|
|
422
729
|
}
|
|
423
730
|
if (choice === 'q') { console.log(' Stopping. Remaining items left pending.'); break; }
|
|
424
|
-
if (choice === 's') { skipped += 1;
|
|
731
|
+
if (choice === 's') { skipped += 1; continue; }
|
|
425
732
|
try {
|
|
426
|
-
if (choice === '
|
|
427
|
-
await review.submitDecision({ apiKey, baseUrl, id:
|
|
428
|
-
approved += 1; console.log('
|
|
733
|
+
if (choice === 'y') {
|
|
734
|
+
await review.submitDecision({ apiKey, baseUrl, id: row.id, decision: 'approve' });
|
|
735
|
+
approved += 1; console.log(' ✓ approved, now live');
|
|
429
736
|
} else {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
rejected += 1; console.log(' ✗ rejected — stays private');
|
|
737
|
+
await review.submitDecision({ apiKey, baseUrl, id: row.id, decision: 'reject' });
|
|
738
|
+
rejected += 1; console.log(' ✗ rejected, stays private');
|
|
433
739
|
}
|
|
434
740
|
} catch (err) {
|
|
435
|
-
console.error(`
|
|
741
|
+
console.error(` ! decision failed: ${err.message} (item left pending)`);
|
|
436
742
|
}
|
|
437
743
|
}
|
|
438
744
|
|
|
439
|
-
console.log(`\nReview complete: approved ${approved}, rejected ${rejected}, skipped ${skipped} of ${
|
|
745
|
+
console.log(`\nReview complete: approved ${approved}, rejected ${rejected}, skipped ${skipped} of ${ordered.length}.`);
|
|
440
746
|
}
|
|
441
747
|
|
|
442
748
|
// ─── Entry point ────────────────────────────────────────────────────────────
|
|
@@ -449,10 +755,39 @@ Commands:
|
|
|
449
755
|
setup Interactive install: client detection, MCP registration,
|
|
450
756
|
device-code login, extraction runner + hook, consent.
|
|
451
757
|
Flags: --re-auth, --base-url <url>
|
|
758
|
+
init Mint a SCOPED API key for CI or a second machine — no full setup,
|
|
759
|
+
no prompts (device-code approval in a browser is the only human
|
|
760
|
+
step). Does NOT touch ~/.auxilo/credentials.json unless --save.
|
|
761
|
+
Flags:
|
|
762
|
+
--scope <s> read | earnings-read | contribute (default:
|
|
763
|
+
contribute; admin is never issuable)
|
|
764
|
+
--label <name> key label (default: init-<hostname>)
|
|
765
|
+
--env-file <path> write AUXILO_API_KEY to an env file (0600,
|
|
766
|
+
updated in place) instead of printing the key
|
|
767
|
+
--save also write ~/.auxilo/credentials.json
|
|
768
|
+
--json machine-readable result on stdout
|
|
769
|
+
--no-browser don't try to open the approval URL
|
|
770
|
+
--base-url <url>
|
|
452
771
|
status Show install/auth/extraction status.
|
|
453
772
|
review Review YOUR pending-review learnings (from background extraction)
|
|
454
|
-
and approve/reject
|
|
455
|
-
|
|
773
|
+
and approve/reject before anything goes public. Default: triage
|
|
774
|
+
summary table first (quality desc, clean vs flagged), then rapid
|
|
775
|
+
y/n/s review.
|
|
776
|
+
Flags:
|
|
777
|
+
--list summary table only, no changes
|
|
778
|
+
--approve-clean bulk-approve items that passed EVERY platform
|
|
779
|
+
screen AND quality >= threshold (default 14;
|
|
780
|
+
tune with --min-quality N). Prints the exact
|
|
781
|
+
list, then requires typing the count.
|
|
782
|
+
--all bulk-approve everything EXCEPT screen-flagged
|
|
783
|
+
items (add --include-flagged to include them).
|
|
784
|
+
Same typed-count confirmation.
|
|
785
|
+
--min-quality N quality threshold for --approve-clean (0-20)
|
|
786
|
+
--all-reject reject the whole batch [--yes for scripted
|
|
787
|
+
incident response; rejects stay private]
|
|
788
|
+
--base-url <url>
|
|
789
|
+
Bulk approvals ALWAYS require typing the exact count. No flag
|
|
790
|
+
skips that step.
|
|
456
791
|
disable Turn off background extraction (local kill-switch; optional
|
|
457
792
|
server-side consent revoke).
|
|
458
793
|
|
|
@@ -465,6 +800,7 @@ async function main() {
|
|
|
465
800
|
const flags = parseFlags(process.argv);
|
|
466
801
|
switch (cmd) {
|
|
467
802
|
case 'setup': return cmdSetup(flags);
|
|
803
|
+
case 'init': return cmdInit(flags);
|
|
468
804
|
case 'status': return cmdStatus(flags);
|
|
469
805
|
case 'review': return cmdReview(flags);
|
|
470
806
|
case 'disable': return cmdDisable(flags);
|