claude-flow 3.32.2 → 3.32.9

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.
Files changed (50) hide show
  1. package/.claude/helpers/statusline.cjs +93 -39
  2. package/.claude-plugin/hooks/hooks.json +3 -1
  3. package/.claude-plugin/scripts/ruflo-hook.cjs +2 -2
  4. package/.claude-plugin/scripts/ruflo-hook.sh +17 -17
  5. package/package.json +9 -3
  6. package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
  7. package/v3/@claude-flow/cli/dist/src/auth/client.d.ts +89 -0
  8. package/v3/@claude-flow/cli/dist/src/auth/client.js +242 -0
  9. package/v3/@claude-flow/cli/dist/src/auth/constants.d.ts +7 -0
  10. package/v3/@claude-flow/cli/dist/src/auth/constants.js +7 -0
  11. package/v3/@claude-flow/cli/dist/src/auth/scopes.d.ts +14 -0
  12. package/v3/@claude-flow/cli/dist/src/auth/scopes.js +21 -0
  13. package/v3/@claude-flow/cli/dist/src/auth/security-bridge.d.ts +36 -0
  14. package/v3/@claude-flow/cli/dist/src/auth/security-bridge.js +42 -0
  15. package/v3/@claude-flow/cli/dist/src/auth/session.d.ts +20 -0
  16. package/v3/@claude-flow/cli/dist/src/auth/session.js +32 -0
  17. package/v3/@claude-flow/cli/dist/src/auth/state.d.ts +19 -0
  18. package/v3/@claude-flow/cli/dist/src/auth/state.js +53 -0
  19. package/v3/@claude-flow/cli/dist/src/auth/types.d.ts +27 -0
  20. package/v3/@claude-flow/cli/dist/src/auth/types.js +11 -0
  21. package/v3/@claude-flow/cli/dist/src/commands/auth.d.ts +15 -0
  22. package/v3/@claude-flow/cli/dist/src/commands/auth.js +244 -0
  23. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +478 -24
  24. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +43 -1
  25. package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
  26. package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.d.ts +20 -0
  27. package/v3/@claude-flow/cli/dist/src/commands/proxy-lifecycle.js +277 -0
  28. package/v3/@claude-flow/cli/dist/src/commands/proxy.js +97 -4
  29. package/v3/@claude-flow/cli/dist/src/funnel/message-transport.d.ts +11 -4
  30. package/v3/@claude-flow/cli/dist/src/funnel/message-transport.js +11 -4
  31. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +11 -3
  32. package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.d.ts +9 -0
  33. package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js +13 -0
  34. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +32 -5
  35. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +72 -0
  36. package/v3/@claude-flow/cli/dist/src/proxy/install.d.ts +29 -0
  37. package/v3/@claude-flow/cli/dist/src/proxy/install.js +135 -0
  38. package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.d.ts +61 -0
  39. package/v3/@claude-flow/cli/dist/src/proxy/lifecycle.js +249 -0
  40. package/v3/@claude-flow/cli/dist/src/proxy/paths.d.ts +34 -0
  41. package/v3/@claude-flow/cli/dist/src/proxy/paths.js +70 -0
  42. package/v3/@claude-flow/cli/dist/src/proxy/release.d.ts +47 -0
  43. package/v3/@claude-flow/cli/dist/src/proxy/release.js +138 -0
  44. package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.d.ts +5 -0
  45. package/v3/@claude-flow/cli/dist/src/proxy/token-bridge.js +61 -0
  46. package/v3/@claude-flow/cli/dist/src/proxy/verify.d.ts +44 -0
  47. package/v3/@claude-flow/cli/dist/src/proxy/verify.js +68 -0
  48. package/v3/@claude-flow/cli/package.json +6 -4
  49. package/.claude/.proven-config-version +0 -1
  50. package/.claude/proven-config.json +0 -42
@@ -5,7 +5,7 @@
5
5
  * Created with ruv.io
6
6
  */
7
7
  import { output } from '../output.js';
8
- import { existsSync, readFileSync, statSync } from 'fs';
8
+ import { existsSync, readFileSync, statSync, openSync, readSync, closeSync } from 'fs';
9
9
  import { join, dirname } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { createHash } from 'crypto';
@@ -216,6 +216,16 @@ async function checkDaemonStatus() {
216
216
  }
217
217
  }
218
218
  // Check memory database
219
+ //
220
+ // #2737: renamed the reported row from "Memory Database" to "Memory Database
221
+ // Presence" — this check is existsSync()+statSync() ONLY, it never opens the
222
+ // file or queries it, so it PASSES for any file that exists and can be
223
+ // stat'd, corrupt or not. It was the ONLY memory probe in the default
224
+ // `allChecks` run (the real integrity checks were --component memory only),
225
+ // so a corrupt .swarm/memory.db could sail through a bare `doctor` run as
226
+ // "healthy". The name change makes the check's actual scope honest; the
227
+ // behavior below is unchanged. See checkMemoryStructuralIntegrity below for
228
+ // the new default-run check that actually opens the file.
219
229
  async function checkMemoryDatabase() {
220
230
  // Authoritative path comes from `getMemoryRoot()` (honors
221
231
  // `CLAUDE_FLOW_MEMORY_PATH`, claude-flow.config.json's `memory.persistPath`,
@@ -238,14 +248,14 @@ async function checkMemoryDatabase() {
238
248
  try {
239
249
  const stats = statSync(dbPath);
240
250
  const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
241
- return { name: 'Memory Database', status: 'pass', message: `${dbPath} (${sizeMB} MB)` };
251
+ return { name: 'Memory Database Presence', status: 'pass', message: `${dbPath} (${sizeMB} MB) — existence/size only, not a health check (see Memory Structural Integrity)` };
242
252
  }
243
253
  catch {
244
- return { name: 'Memory Database', status: 'warn', message: `${dbPath} (unable to stat)` };
254
+ return { name: 'Memory Database Presence', status: 'warn', message: `${dbPath} (unable to stat)` };
245
255
  }
246
256
  }
247
257
  }
248
- return { name: 'Memory Database', status: 'warn', message: 'Not initialized', fix: 'claude-flow memory configure --backend hybrid' };
258
+ return { name: 'Memory Database Presence', status: 'warn', message: 'Not initialized', fix: 'claude-flow memory configure --backend hybrid' };
249
259
  }
250
260
  // ═══════════════════════════════════════════════════════════════════════════
251
261
  // #2677 — memory doctor: functional checks (stuinfla)
@@ -305,46 +315,282 @@ async function tryOpenSqlJs(dbPath) {
305
315
  return null;
306
316
  }
307
317
  }
308
- // Check 1 sql.js can open it AND PRAGMA integrity_check returns 'ok'.
309
- // Two fail modes handled distinctly per "UNKNOWN is never PASS":
310
- // - Open fails: warn ("cannot open; encrypted DB or corrupt — doctor
311
- // can't distinguish from this side")
312
- // - Open succeeds but pragma != 'ok': fail (definite corruption)
318
+ // ── #2737 shared helpers: encryption-at-rest carve-out ─────────────────────
319
+ //
320
+ // "file is not a database" / "malformed" from sql.js or better-sqlite3 is
321
+ // EXACTLY the signal a legitimately RFE1-encrypted-at-rest memory.db also
322
+ // produces when opened without decrypting (ADR-096). Before either the new
323
+ // default-run check or the strengthened checkMemoryIntegrity below concludes
324
+ // "malformed = fail", they sniff for the RFE1 magic using the SAME detector
325
+ // checkEncryptionAtRest uses (isEncryptedBlob), so the two checks can never
326
+ // disagree about what counts as "encrypted".
327
+ /** Read just the first `len` bytes of a file without loading the whole file
328
+ * into memory — a multi-GB memory.db shouldn't get fully buffered just to
329
+ * check a 4-byte magic. Returns fewer bytes (or empty) when the file is
330
+ * shorter than `len`; isEncryptedBlob() correctly treats a too-short blob
331
+ * as "not encrypted", so callers don't need to special-case that. */
332
+ function readHeaderBytes(path, len) {
333
+ const fd = openSync(path, 'r');
334
+ try {
335
+ const buf = Buffer.alloc(len);
336
+ const bytesRead = readSync(fd, buf, 0, len, 0);
337
+ return buf.subarray(0, bytesRead);
338
+ }
339
+ finally {
340
+ closeSync(fd);
341
+ }
342
+ }
343
+ // RFE1 wire format is magic(4) + iv(12) + ciphertext(N) + tag(16); the
344
+ // shortest possible blob is 32 bytes. 64 gives headroom without importing
345
+ // vault.ts's private MIN_BLOB_LEN constant.
346
+ const ENCRYPTION_SNIFF_LEN = 64;
347
+ /** True iff `dbPath` is a legitimately RFE1-encrypted-at-rest file. A read
348
+ * failure (permissions, races) is treated as "not encrypted" — callers
349
+ * still hit their own open/query error handling right after, so nothing
350
+ * gets silently swallowed. */
351
+ function isMemoryDbEncryptedAtRest(dbPath) {
352
+ try {
353
+ return isEncryptedBlob(readHeaderBytes(dbPath, ENCRYPTION_SNIFF_LEN));
354
+ }
355
+ catch {
356
+ return false;
357
+ }
358
+ }
359
+ // #2737 part 1 — bare `doctor` (no --component flag) never actually opened
360
+ // memory.db: checkMemoryDatabase above is existsSync()+statSync() only, so
361
+ // it PASSES on any file that exists and can be stat'd, corrupt or not, and
362
+ // it was the ONLY memory probe `allChecks` ran. The real integrity checks
363
+ // (checkMemoryIntegrity/Content/EmbeddingCoverage below) were, and remain,
364
+ // --component memory only.
365
+ //
366
+ // This is a NEW, cheaper, native check added to the default `allChecks` run
367
+ // (not a replacement for the deep trio):
368
+ // - better-sqlite3 (native) instead of sql.js: it's WAL-aware because
369
+ // SQLite itself resolves any sibling -wal file next to the main image;
370
+ // sql.js is WAL-blind — tryOpenSqlJs() above only readFileSync()s the
371
+ // main file and hands the raw bytes to the WASM build, so any
372
+ // committed-but-not-checkpointed rows sitting in a -wal file are
373
+ // invisible to it.
374
+ // - PRAGMA quick_check, not integrity_check: per sqlite.org, quick_check
375
+ // "is like integrity_check except that it does not verify UNIQUE
376
+ // constraints and does not verify that index content matches table
377
+ // content" — bounded enough to run unconditionally on every bare
378
+ // `doctor` call. The full integrity_check (with those extra
379
+ // cross-checks) is what the strengthened checkMemoryIntegrity below
380
+ // runs when better-sqlite3 is available.
381
+ // Explicitly labeled "structural-only" in both the row name and every
382
+ // message so nobody mistakes this cheap default-run probe for the deeper
383
+ // --component memory one.
384
+ async function checkMemoryStructuralIntegrity() {
385
+ const NAME = 'Memory Structural Integrity (quick_check)';
386
+ const dbPath = await resolveMemoryDbPath();
387
+ if (!dbPath) {
388
+ // Not-yet-initialized project — Memory Database Presence above already
389
+ // surfaces this; "UNKNOWN is never PASS" still applies, so this stays a
390
+ // warn rather than a silent skip, but doesn't pile on a second red for
391
+ // the same root cause.
392
+ return {
393
+ name: NAME,
394
+ status: 'warn',
395
+ message: 'no memory.db found (see Memory Database Presence above) — structural-only check skipped',
396
+ };
397
+ }
398
+ // Encryption-at-rest carve-out (#2737 part 3) — see helpers above.
399
+ if (isMemoryDbEncryptedAtRest(dbPath)) {
400
+ return {
401
+ name: NAME,
402
+ status: 'warn',
403
+ message: `${dbPath} — RFE1-encrypted at rest; structural-only check can't run without decrypting (expected, not corruption — see Encryption at Rest)`,
404
+ };
405
+ }
406
+ let Database;
407
+ try {
408
+ Database = (await import('better-sqlite3')).default;
409
+ }
410
+ catch {
411
+ return {
412
+ name: NAME,
413
+ status: 'warn',
414
+ message: `${dbPath} — better-sqlite3 not installed; structural-only check skipped (optional native module)`,
415
+ fix: 'npm install better-sqlite3 (enables WAL-aware structural checks on every `doctor` run)',
416
+ };
417
+ }
418
+ let db;
419
+ try {
420
+ db = new Database(dbPath, { readonly: true, fileMustExist: true });
421
+ }
422
+ catch (e) {
423
+ const msg = e.message || String(e);
424
+ // Encryption already ruled out above — an unencrypted file
425
+ // better-sqlite3 can't even open is definitive corruption.
426
+ // #2737 part 3: fail, not warn.
427
+ return {
428
+ name: NAME,
429
+ status: 'fail',
430
+ message: `${dbPath} — better-sqlite3 failed to open: ${msg} (unencrypted; definitively malformed) [structural-only]`,
431
+ fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
432
+ };
433
+ }
434
+ try {
435
+ const rows = db.pragma('quick_check');
436
+ const values = rows.map((r) => String(Object.values(r)[0]));
437
+ if (values.length === 1 && values[0] === 'ok') {
438
+ return {
439
+ name: NAME,
440
+ status: 'pass',
441
+ message: `${dbPath} — PRAGMA quick_check: ok [structural-only, native + WAL-aware]`,
442
+ };
443
+ }
444
+ return {
445
+ name: NAME,
446
+ status: 'fail',
447
+ message: `${dbPath} — PRAGMA quick_check: ${values.slice(0, 3).join('; ')}${values.length > 3 ? ` (+${values.length - 3} more)` : ''} [structural-only]`,
448
+ fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
449
+ };
450
+ }
451
+ catch (e) {
452
+ const msg = e.message || String(e);
453
+ // Encryption ruled out above; DB opened but the pragma itself threw —
454
+ // still definitive, unencrypted breakage. Fail, not warn.
455
+ return {
456
+ name: NAME,
457
+ status: 'fail',
458
+ message: `${dbPath} — quick_check probe threw: ${msg} (unencrypted) [structural-only]`,
459
+ fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
460
+ };
461
+ }
462
+ finally {
463
+ try {
464
+ db.close();
465
+ }
466
+ catch { /* best-effort */ }
467
+ }
468
+ }
469
+ // Check 1 (--component memory, deep path) — #2737 part 2 strengthens this:
470
+ // prefer native better-sqlite3 PRAGMA integrity_check (adds index↔table
471
+ // cross-checking + UNIQUE verification that quick_check above deliberately
472
+ // skips, and is WAL-aware — see checkMemoryStructuralIntegrity's comment)
473
+ // when better-sqlite3 is available, falling back to the original sql.js
474
+ // probe when it's not. The sql.js fallback is main-image-only (WAL-blind);
475
+ // its message says so explicitly so operators know its limits.
476
+ //
477
+ // #2737 part 3: with the encryption carve-out applied up front on BOTH
478
+ // paths, a "file is not a database" / "malformed" result is now definitive,
479
+ // unencrypted corruption in every branch below — fail, not warn (the
480
+ // previous version treated this as an ambiguous warn because it couldn't
481
+ // tell corruption from encryption; that ambiguity is what the carve-out
482
+ // resolves).
313
483
  async function checkMemoryIntegrity() {
314
484
  const dbPath = await resolveMemoryDbPath();
315
485
  if (!dbPath)
316
- return { name: 'Memory Integrity', status: 'warn', message: 'no memory.db found (see Memory Database check above)' };
486
+ return { name: 'Memory Integrity', status: 'warn', message: 'no memory.db found (see Memory Database Presence check above)' };
487
+ if (isMemoryDbEncryptedAtRest(dbPath)) {
488
+ return {
489
+ name: 'Memory Integrity',
490
+ status: 'warn',
491
+ message: `${dbPath} — RFE1-encrypted at rest; integrity check can't run without decrypting (expected, not corruption — see Encryption at Rest)`,
492
+ };
493
+ }
494
+ // Prefer native better-sqlite3 (WAL-aware, full integrity_check). Module
495
+ // load is isolated in its own try/catch so ONLY "not installed" falls
496
+ // through to the sql.js fallback below — once the module loaded, any
497
+ // open/query failure is resolved (fail) right here, not silently
498
+ // reclassified as "sql.js fallback, main-image-only" by an outer catch.
499
+ let Database;
500
+ try {
501
+ Database = (await import('better-sqlite3')).default;
502
+ }
503
+ catch {
504
+ Database = null; // not installed — fall through to the sql.js fallback below.
505
+ }
506
+ if (Database) {
507
+ let db;
508
+ try {
509
+ db = new Database(dbPath, { readonly: true, fileMustExist: true });
510
+ }
511
+ catch (e) {
512
+ const msg = e.message || String(e);
513
+ return {
514
+ name: 'Memory Integrity',
515
+ status: 'fail',
516
+ message: `${dbPath} — better-sqlite3 failed to open: ${msg} (unencrypted; definitively malformed) [native, WAL-aware]`,
517
+ fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
518
+ };
519
+ }
520
+ try {
521
+ const rows = db.pragma('integrity_check');
522
+ const values = rows.map((r) => String(Object.values(r)[0]));
523
+ if (values.length === 1 && values[0] === 'ok') {
524
+ return { name: 'Memory Integrity', status: 'pass', message: `${dbPath} — PRAGMA integrity_check: ok [native, WAL-aware]` };
525
+ }
526
+ return {
527
+ name: 'Memory Integrity',
528
+ status: 'fail',
529
+ message: `${dbPath} — PRAGMA integrity_check: ${values.slice(0, 3).join('; ')}${values.length > 3 ? ` (+${values.length - 3} more)` : ''} [native, WAL-aware]`,
530
+ fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
531
+ };
532
+ }
533
+ catch (e) {
534
+ // DB opened but the pragma itself threw — still definitive,
535
+ // unencrypted breakage (encryption ruled out above). Fail, not warn,
536
+ // and NOT a fall-through to the sql.js fallback: better-sqlite3 IS
537
+ // installed and just gave us the answer.
538
+ const msg = e.message || String(e);
539
+ return {
540
+ name: 'Memory Integrity',
541
+ status: 'fail',
542
+ message: `${dbPath} — integrity_check probe threw: ${msg} (unencrypted; definitively malformed) [native, WAL-aware]`,
543
+ fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
544
+ };
545
+ }
546
+ finally {
547
+ try {
548
+ db.close();
549
+ }
550
+ catch { /* best-effort */ }
551
+ }
552
+ }
553
+ // Fallback: sql.js — main-image-only (WAL-blind). tryOpenSqlJs()
554
+ // readFileSync()s the base file directly, so any committed-but-not-
555
+ // checkpointed rows sitting in a sibling -wal file are invisible here.
556
+ // Install better-sqlite3 for the WAL-aware path above.
317
557
  const db = await tryOpenSqlJs(dbPath);
318
558
  if (!db) {
559
+ // Encryption already ruled out above — an unencrypted file sql.js can't
560
+ // even open is definitive corruption too.
319
561
  return {
320
562
  name: 'Memory Integrity',
321
- status: 'warn',
322
- message: `${dbPath} — sql.js can't open (encrypted DB or corrupt; doctor can't tell which from outside)`,
323
- fix: 'if encrypted: expected. if not: back up + `claude-flow memory init --force` to rebuild',
563
+ status: 'fail',
564
+ message: `${dbPath} — sql.js can't open (unencrypted; definitively malformed) [main-image-only fallback install better-sqlite3 for WAL-aware checking]`,
565
+ fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
324
566
  };
325
567
  }
326
568
  try {
327
569
  const res = db.exec('PRAGMA integrity_check');
328
570
  const rows = res[0]?.values?.map((v) => String(v[0])) ?? [];
329
571
  if (rows.length === 1 && rows[0] === 'ok') {
330
- return { name: 'Memory Integrity', status: 'pass', message: `${dbPath} — PRAGMA integrity_check: ok` };
572
+ return { name: 'Memory Integrity', status: 'pass', message: `${dbPath} — PRAGMA integrity_check: ok [main-image-only fallback — sql.js can't see WAL-only data; install better-sqlite3 for full coverage]` };
331
573
  }
332
574
  return {
333
575
  name: 'Memory Integrity',
334
576
  status: 'fail',
335
- message: `${dbPath} — PRAGMA integrity_check: ${rows.slice(0, 3).join('; ')}${rows.length > 3 ? ` (+${rows.length - 3} more)` : ''}`,
577
+ message: `${dbPath} — PRAGMA integrity_check: ${rows.slice(0, 3).join('; ')}${rows.length > 3 ? ` (+${rows.length - 3} more)` : ''} [main-image-only fallback]`,
336
578
  fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
337
579
  };
338
580
  }
339
581
  catch (e) {
340
582
  const msg = e.message || String(e);
341
- const encryptedOrCorrupt = msg.includes('file is not a database') || msg.includes('malformed');
583
+ const definitivelyMalformed = msg.includes('file is not a database') || msg.includes('malformed');
584
+ // Encryption already ruled out above, so — matched pattern or not —
585
+ // this is a query refusal on a file we KNOW isn't RFE1-encrypted.
586
+ // #2737 part 3: fail, not warn.
342
587
  return {
343
588
  name: 'Memory Integrity',
344
- status: 'warn',
345
- message: encryptedOrCorrupt
346
- ? `${dbPath} — DB refused query: ${msg} (encrypted DB or corruption; see Memory Integrity above)`
347
- : `${dbPath} — probe threw: ${msg}`,
589
+ status: 'fail',
590
+ message: definitivelyMalformed
591
+ ? `${dbPath} — DB refused query: ${msg} (unencrypted; definitively malformed) [main-image-only fallback]`
592
+ : `${dbPath} — probe threw: ${msg} (unencrypted — encryption ruled out above) [main-image-only fallback]`,
593
+ fix: 'back up .swarm/memory.db then `claude-flow memory init --force`',
348
594
  };
349
595
  }
350
596
  finally {
@@ -978,7 +1224,7 @@ async function checkFunnel() {
978
1224
  }
979
1225
  }
980
1226
  /** Meta LLM Proxy — sponsored-downtime health (ADR-313). */
981
- async function checkProxy() {
1227
+ async function checkProxySponsoredConsent() {
982
1228
  try {
983
1229
  const { funnelStateDir, hasConsent, readRateLimitStatus, lastRecordedEvent } = await import('../funnel/index.js');
984
1230
  const dir = funnelStateDir();
@@ -1019,6 +1265,208 @@ async function checkProxy() {
1019
1265
  };
1020
1266
  }
1021
1267
  }
1268
+ /**
1269
+ * Binary presence + tamper check (ADR-307). Deliberately NEVER spawns the
1270
+ * binary to probe a version — confirmed empirically (2026-07-16) that
1271
+ * `meta-proxy` has no `--version`/`--help` flag and starts the live server
1272
+ * as a side effect of ANY invocation, which a doctor health check must never
1273
+ * do. Version info instead comes from install-manifest.json (written at
1274
+ * install time) and, once running, the proxy's own `/status` endpoint via
1275
+ * checkProxyProcess below.
1276
+ */
1277
+ async function checkProxyBinary() {
1278
+ const NAME = 'Meta LLM Proxy binary (ADR-307)';
1279
+ try {
1280
+ const { proxyBinaryPath, proxyInstallManifestPath } = await import('../proxy/paths.js');
1281
+ const binPath = proxyBinaryPath();
1282
+ if (!existsSync(binPath)) {
1283
+ return { name: NAME, status: 'warn', message: 'not installed', fix: 'ruflo proxy install' };
1284
+ }
1285
+ const manifestPath = proxyInstallManifestPath();
1286
+ if (!existsSync(manifestPath)) {
1287
+ return {
1288
+ name: NAME,
1289
+ status: 'warn',
1290
+ message: 'binary present but no install-manifest.json — provenance unknown (installed outside `ruflo proxy install`?)',
1291
+ fix: 'ruflo proxy update --release <x.y.z>',
1292
+ };
1293
+ }
1294
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
1295
+ const liveSha = createHash('sha256').update(readFileSync(binPath)).digest('hex');
1296
+ if (liveSha !== manifest.sha256) {
1297
+ return {
1298
+ name: NAME,
1299
+ status: 'fail',
1300
+ message: `binary sha256 does not match the recorded install manifest — possible tampering or a manual overwrite (expected ${manifest.sha256.slice(0, 12)}…, got ${liveSha.slice(0, 12)}…)`,
1301
+ fix: 'ruflo proxy update --release <x.y.z>',
1302
+ };
1303
+ }
1304
+ return { name: NAME, status: 'pass', message: `v${manifest.version}, signature-verified at install (${manifest.verifiedAt})` };
1305
+ }
1306
+ catch (err) {
1307
+ return { name: NAME, status: 'warn', message: `check failed: ${err instanceof Error ? err.message : String(err)}` };
1308
+ }
1309
+ }
1310
+ /** PID-file liveness (ADR-307) — mirrors daemon.ts's signal-0 probe pattern. */
1311
+ async function checkProxyProcess() {
1312
+ const NAME = 'Meta LLM Proxy process (ADR-307)';
1313
+ try {
1314
+ const { proxyPidFilePath } = await import('../proxy/paths.js');
1315
+ const pidPath = proxyPidFilePath();
1316
+ if (!existsSync(pidPath)) {
1317
+ return { name: NAME, status: 'warn', message: 'not running (no PID file)', fix: 'ruflo proxy start' };
1318
+ }
1319
+ const pidRaw = readFileSync(pidPath, 'utf-8').trim();
1320
+ const pid = parseInt(pidRaw, 10);
1321
+ if (!Number.isFinite(pid)) {
1322
+ return { name: NAME, status: 'warn', message: `PID file is malformed: ${JSON.stringify(pidRaw)}`, fix: 'ruflo proxy stop && ruflo proxy start' };
1323
+ }
1324
+ try {
1325
+ process.kill(pid, 0); // signal-0 liveness probe — throws if the process is dead
1326
+ }
1327
+ catch {
1328
+ return { name: NAME, status: 'warn', message: `PID file points at ${pid}, which is not running — stale PID file`, fix: 'ruflo proxy start' };
1329
+ }
1330
+ // Live version-compat + data-plane info, ONLY once PID liveness already
1331
+ // confirmed a process is running (never spawns anything — see
1332
+ // checkProxyBinary's comment on why probing via process launch is unsafe).
1333
+ // GET /status shape confirmed against the real v0.1.0 binary:
1334
+ // {"version","data_plane","bind","sponsored_available","proxy_token_valid"}.
1335
+ const { proxyConfigPath, proxyTokenPath, proxyInstallManifestPath } = await import('../proxy/paths.js');
1336
+ const bindMatch = existsSync(proxyConfigPath())
1337
+ ? readFileSync(proxyConfigPath(), 'utf-8').match(/^bind\s*=\s*"([^"]*)"/m)
1338
+ : null;
1339
+ const bind = bindMatch ? bindMatch[1] : '127.0.0.1:11435';
1340
+ let token;
1341
+ try {
1342
+ token = readFileSync(proxyTokenPath(), 'utf-8').trim();
1343
+ }
1344
+ catch {
1345
+ return { name: NAME, status: 'pass', message: `running (pid ${pid}); no proxy-token to query /status` };
1346
+ }
1347
+ try {
1348
+ const controller = new AbortController();
1349
+ const timer = setTimeout(() => controller.abort(), 2000);
1350
+ const resp = await fetch(`http://${bind}/status`, {
1351
+ headers: { authorization: `Bearer ${token}` },
1352
+ signal: controller.signal,
1353
+ });
1354
+ clearTimeout(timer);
1355
+ if (!resp.ok) {
1356
+ return { name: NAME, status: 'warn', message: `running (pid ${pid}); /status returned HTTP ${resp.status}` };
1357
+ }
1358
+ const body = (await resp.json());
1359
+ let versionNote = '';
1360
+ if (existsSync(proxyInstallManifestPath())) {
1361
+ const manifest = JSON.parse(readFileSync(proxyInstallManifestPath(), 'utf-8'));
1362
+ if (manifest.version && body.version && manifest.version !== body.version) {
1363
+ return {
1364
+ name: NAME,
1365
+ status: 'warn',
1366
+ message: `running (pid ${pid}) reports v${body.version}, but the installed binary is v${manifest.version} — a stale process from a previous version?`,
1367
+ fix: 'ruflo proxy stop && ruflo proxy start',
1368
+ };
1369
+ }
1370
+ versionNote = body.version ? ` v${body.version}` : '';
1371
+ }
1372
+ return {
1373
+ name: NAME,
1374
+ status: 'pass',
1375
+ message: `running (pid ${pid})${versionNote}; data plane: ${body.data_plane ?? 'unknown'}`,
1376
+ };
1377
+ }
1378
+ catch (err) {
1379
+ // Live process but /status unreachable (still starting up, or a
1380
+ // network hiccup) — not a failure, PID liveness already passed.
1381
+ return {
1382
+ name: NAME,
1383
+ status: 'pass',
1384
+ message: `running (pid ${pid}); /status unreachable (${err instanceof Error ? err.message : String(err)})`,
1385
+ };
1386
+ }
1387
+ }
1388
+ catch (err) {
1389
+ return { name: NAME, status: 'warn', message: `check failed: ${err instanceof Error ? err.message : String(err)}` };
1390
+ }
1391
+ }
1392
+ /** Non-loopback bind exposure warning (ADR-307's mandated startup warning, surfaced in doctor too). */
1393
+ async function checkProxyBindAddress() {
1394
+ const NAME = 'Meta LLM Proxy bind address (ADR-307)';
1395
+ try {
1396
+ const { proxyConfigPath, isLoopbackBind } = await import('../proxy/paths.js');
1397
+ const cfgPath = proxyConfigPath();
1398
+ if (!existsSync(cfgPath)) {
1399
+ return { name: NAME, status: 'pass', message: 'no config file yet — defaults to loopback-only (127.0.0.1:11435)' };
1400
+ }
1401
+ const raw = readFileSync(cfgPath, 'utf-8');
1402
+ const match = raw.match(/^bind\s*=\s*"([^"]*)"/m);
1403
+ const bind = match ? match[1] : '127.0.0.1:11435';
1404
+ if (!isLoopbackBind(bind)) {
1405
+ return {
1406
+ name: NAME,
1407
+ status: 'warn',
1408
+ message: `bound to non-loopback address ${bind} — this exposes the proxy to your network`,
1409
+ fix: 'Set bind back to 127.0.0.1:<port> in proxy-config.toml unless external exposure is intended',
1410
+ };
1411
+ }
1412
+ return { name: NAME, status: 'pass', message: `loopback-only (${bind})` };
1413
+ }
1414
+ catch (err) {
1415
+ return { name: NAME, status: 'warn', message: `check failed: ${err instanceof Error ? err.message : String(err)}` };
1416
+ }
1417
+ }
1418
+ /** `ruflo auth` health (ADR-306). Warn (never fail) on absence — auth is never required for core functionality. */
1419
+ async function checkAuth() {
1420
+ const NAME = 'Cognitum identity (ADR-306)';
1421
+ try {
1422
+ const { listProfiles } = await import('../auth/state.js');
1423
+ const { domainForScope } = await import('../auth/scopes.js');
1424
+ const { hasConsent } = await import('../funnel/index.js');
1425
+ const { profiles } = listProfiles();
1426
+ if (profiles.length === 0) {
1427
+ return { name: NAME, status: 'warn', message: 'not logged in', fix: 'ruflo auth login' };
1428
+ }
1429
+ let keychainAvailable = 'unknown';
1430
+ try {
1431
+ const sec = await import('@claude-flow/security');
1432
+ keychainAvailable = await (await sec.createKeychainAdapter()).isAvailable();
1433
+ }
1434
+ catch {
1435
+ keychainAvailable = 'unknown'; // security package unavailable — surfaced by checkProxyBinary's sibling concerns, not duplicated here
1436
+ }
1437
+ // Scope-vs-receipt consistency check (ADR-306: "fail-closed... reports").
1438
+ // Unlike every other check in this file, a violation here is a FAIL, not
1439
+ // a warn — a scope present without a matching consent receipt is exactly
1440
+ // the condition ADR-306 says must never silently pass.
1441
+ const violations = [];
1442
+ for (const p of profiles) {
1443
+ for (const scope of p.scopes) {
1444
+ const domain = domainForScope(scope);
1445
+ if (domain && !hasConsent(domain))
1446
+ violations.push(`${p.profile}: ${scope}`);
1447
+ }
1448
+ }
1449
+ if (violations.length > 0) {
1450
+ return {
1451
+ name: NAME,
1452
+ status: 'fail',
1453
+ message: `scope granted without a matching consent receipt: ${violations.join(', ')}`,
1454
+ fix: 'ruflo auth logout && ruflo auth login',
1455
+ };
1456
+ }
1457
+ const names = profiles.map((p) => p.profile).join(', ');
1458
+ const sessionOnly = profiles.filter((p) => !p.keychainRef).map((p) => p.profile);
1459
+ const parts = [`profiles: ${names}`];
1460
+ if (keychainAvailable === false)
1461
+ parts.push('keychain backend unreachable — falling back to session-only tokens');
1462
+ if (sessionOnly.length > 0)
1463
+ parts.push(`session-only (no persisted refresh token): ${sessionOnly.join(', ')}`);
1464
+ return { name: NAME, status: 'pass', message: parts.join('; ') };
1465
+ }
1466
+ catch (err) {
1467
+ return { name: NAME, status: 'warn', message: `check failed: ${err instanceof Error ? err.message : String(err)}` };
1468
+ }
1469
+ }
1022
1470
  async function checkMetaharnessIntegration() {
1023
1471
  // Locate plugins dir.
1024
1472
  //
@@ -1395,7 +1843,7 @@ export const doctorCommand = {
1395
1843
  {
1396
1844
  name: 'component',
1397
1845
  short: 'c',
1398
- description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, metaharness)',
1846
+ description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, agentic-flow, encryption, federation, funnel, proxy, auth, metaharness)',
1399
1847
  type: 'string'
1400
1848
  },
1401
1849
  {
@@ -1513,6 +1961,7 @@ export const doctorCommand = {
1513
1961
  checkStaleSettingsNpx, // #2448 — runaway `npx @latest` in statusLine/hooks
1514
1962
  checkDaemonStatus,
1515
1963
  checkMemoryDatabase,
1964
+ checkMemoryStructuralIntegrity, // #2737 — bounded, native quick_check on every default run
1516
1965
  checkLearningBridge, // #2545 — can the auto-memory hook actually load @claude-flow/memory?
1517
1966
  checkApiKeys,
1518
1967
  checkMcpServers,
@@ -1525,7 +1974,8 @@ export const doctorCommand = {
1525
1974
  checkMetaharness, // ADR-150 — MetaHarness upstream package
1526
1975
  checkMetaharnessIntegration, // iter 45 — ruflo-side integration layer
1527
1976
  checkFunnel, // ADR-305 — effective funnel state + deciding precedence source
1528
- checkProxy, // ADR-313 — Meta LLM Proxy sponsored-downtime health
1977
+ checkProxySponsoredConsent, // ADR-313 — Meta LLM Proxy sponsored-downtime health
1978
+ checkAuth, // ADR-306 — Cognitum identity (warn-only; never fails bare `ruflo doctor`)
1529
1979
  ];
1530
1980
  // #2677: `--component memory` now runs the whole memory-health suite,
1531
1981
  // not just the existence check. Values can be a single check or an
@@ -1563,7 +2013,11 @@ export const doctorCommand = {
1563
2013
  'metaharness': checkMetaharness, // ADR-150 — upstream package
1564
2014
  'metaharness-integration': checkMetaharnessIntegration, // iter 45 — ruflo-side
1565
2015
  'funnel': checkFunnel, // ADR-305
1566
- 'proxy': checkProxy, // ADR-313
2016
+ // ADR-307 — deep-dive array, same pattern as 'memory' above: the cheap
2017
+ // sponsored-consent check first, then binary/process/bind in the order
2018
+ // a user would actually debug them (is it installed? running? exposed?).
2019
+ 'proxy': [checkProxySponsoredConsent, checkProxyBinary, checkProxyProcess, checkProxyBindAddress],
2020
+ 'auth': checkAuth, // ADR-306
1567
2021
  };
1568
2022
  let checksToRun = allChecks;
1569
2023
  if (component && componentMap[component]) {
@@ -3485,12 +3485,54 @@ const statuslineCommand = {
3485
3485
  const contextPct = Math.min(100, Math.floor(learning.sessions * 5));
3486
3486
  return { memoryMB, contextPct, intelligencePct, subAgents };
3487
3487
  }
3488
+ // #2733: Claude Code pipes session JSON (incl. the real active model) on
3489
+ // stdin to statusline commands. Read it synchronously the same way
3490
+ // .claude/helpers/statusline.cjs's getModelFromStdin() does, so this CLI
3491
+ // subcommand is correct standalone — not just when the generated helper
3492
+ // happens to override its output. Read once, memoized per invocation
3493
+ // (mirrors statusline.cjs's _stdinData cache).
3494
+ let _stdinData;
3495
+ let _stdinRead = false;
3496
+ function getStdinData() {
3497
+ if (_stdinRead)
3498
+ return _stdinData;
3499
+ _stdinRead = true;
3500
+ try {
3501
+ if (process.stdin.isTTY) {
3502
+ _stdinData = null;
3503
+ return null;
3504
+ }
3505
+ const chunks = [];
3506
+ const buf = Buffer.alloc(4096);
3507
+ let bytesRead;
3508
+ try {
3509
+ while ((bytesRead = fs.readSync(0, buf, 0, buf.length, null)) > 0) {
3510
+ chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
3511
+ }
3512
+ }
3513
+ catch { /* EOF or read error */ }
3514
+ const raw = Buffer.concat(chunks).toString('utf-8').trim();
3515
+ _stdinData = (raw && raw.startsWith('{')) ? JSON.parse(raw) : null;
3516
+ }
3517
+ catch {
3518
+ _stdinData = null;
3519
+ }
3520
+ return _stdinData;
3521
+ }
3522
+ function getModelFromStdin() {
3523
+ const data = getStdinData();
3524
+ return (data && data.model && typeof data.model.display_name === 'string') ? data.model.display_name : null;
3525
+ }
3488
3526
  // Get user info
3489
3527
  function getUserInfo() {
3490
3528
  const identityMode = (process.env.RUFLO_STATUSLINE_IDENTITY || 'project').toLowerCase();
3491
3529
  let name = path.basename(process.cwd()) || 'project';
3492
3530
  let gitBranch = '';
3493
- const modelName = 'Opus 4.6 (1M context)';
3531
+ // Real active model from Claude Code's stdin payload when available;
3532
+ // this string is only a last-resort label for direct/manual CLI
3533
+ // invocation with no stdin (e.g. a bare terminal run) — never shown
3534
+ // when Claude Code itself is the caller.
3535
+ const modelName = getModelFromStdin() || 'Claude Code';
3494
3536
  const isWindows = process.platform === 'win32';
3495
3537
  try {
3496
3538
  const rootCmd = isWindows
@@ -79,6 +79,8 @@ const commandLoaders = {
79
79
  // User-facing preferences wrapper (ADR-311 copy discipline — no "funnel" in
80
80
  // the user surface). Forwards to the funnel primitives internally.
81
81
  settings: () => import('./settings.js'),
82
+ // Cognitum identity — login/logout/status (ADR-306)
83
+ auth: () => import('./auth.js'),
82
84
  // Meta LLM Proxy — sponsored downtime capacity (ADR-304/307/313)
83
85
  proxy: () => import('./proxy.js'),
84
86
  // Fable co-pilot advisor tip in the statusline insight ticker (ADR-316)