@cachly-dev/mcp-server 0.10.116 → 0.10.118

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 (43) hide show
  1. package/README.md +8 -3
  2. package/dist/src/ambient-cli.d.ts +85 -0
  3. package/dist/src/ambient-cli.js +199 -0
  4. package/dist/src/ambient-cli.js.map +1 -0
  5. package/dist/src/ambient-hooks.d.ts +128 -0
  6. package/dist/src/ambient-hooks.js +269 -0
  7. package/dist/src/ambient-hooks.js.map +1 -0
  8. package/dist/src/ambient-ledger.d.ts +19 -0
  9. package/dist/src/ambient-ledger.js +95 -0
  10. package/dist/src/ambient-ledger.js.map +1 -0
  11. package/dist/src/ambient-recall.d.ts +59 -0
  12. package/dist/src/ambient-recall.js +96 -0
  13. package/dist/src/ambient-recall.js.map +1 -0
  14. package/dist/src/doctor.d.ts +47 -0
  15. package/dist/src/doctor.js +247 -0
  16. package/dist/src/doctor.js.map +1 -0
  17. package/dist/src/first-contact.d.ts +56 -0
  18. package/dist/src/first-contact.js +108 -0
  19. package/dist/src/first-contact.js.map +1 -0
  20. package/dist/src/handlers/brain.d.ts +1 -0
  21. package/dist/src/handlers/brain.js +37 -7
  22. package/dist/src/handlers/brain.js.map +1 -1
  23. package/dist/src/handlers/fedbrain.js +48 -16
  24. package/dist/src/handlers/fedbrain.js.map +1 -1
  25. package/dist/src/handlers/syndicate.js +91 -1
  26. package/dist/src/handlers/syndicate.js.map +1 -1
  27. package/dist/src/handlers/team.d.ts +10 -0
  28. package/dist/src/handlers/team.js +45 -1
  29. package/dist/src/handlers/team.js.map +1 -1
  30. package/dist/src/index.d.ts +8 -5
  31. package/dist/src/index.js +213 -33
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/tools.d.ts +5 -196
  34. package/dist/src/tools.js +12 -179
  35. package/dist/src/tools.js.map +1 -1
  36. package/dist/src/toolspecs.d.ts +1 -1
  37. package/dist/src/toolspecs.js +1 -1
  38. package/glama.json +1 -1
  39. package/package.json +14 -4
  40. package/server.json +4 -4
  41. package/dist/src/handlers/tco.d.ts +0 -16
  42. package/dist/src/handlers/tco.js +0 -304
  43. package/dist/src/handlers/tco.js.map +0 -1
package/dist/src/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { jwtExpiryMs, checkJwt, handleApiError, diagnoseAuth, planAuthHeal, readClientCredentialsFromEnv, buildClientCredentialsBody, clientCredentialsTokenUrl } from './auth.js';
3
- import { handleTcoTool } from './handlers/tco.js';
4
3
  import { notify } from './notifier.js';
5
4
  import { fileURLToPath } from 'node:url';
6
5
  import { createRequire } from 'node:module';
@@ -37,24 +36,12 @@ const _isMain = process.argv[1] != null &&
37
36
  * ── Auth & Status ────────────────────────────────────────────────────────────
38
37
  * • get_api_status – check API health + JWT auth info (Keycloak)
39
38
  *
40
- * ── Travel Chaos Organizer (TCO) ────────────────────────────────────────────
41
- * • tco_list_trips – list all trips
42
- * • tco_create_trip – create a new trip
43
- * • tco_get_timeline – get timeline items for a trip
44
- * • tco_delete_trip – delete a trip
45
- * • tco_inbox_list – list Chaos Inbox items
46
- * • tco_inbox_assign – assign inbox item to a trip
47
- * • tco_inbox_reject – reject/discard an inbox item
48
- * • tco_parse_url – fetch a URL and parse with Ollama
49
- * • tco_import_email – import raw email text
50
- * (Requires TCO_API_URL env var; auth forwarded from CACHLY_JWT — same Keycloak realm)
51
39
  *
52
40
  * Configuration (env vars):
53
41
  * CACHLY_API_URL – default https://api.cachly.dev
54
42
  * CACHLY_JWT – your JWT (Keycloak access token)
55
43
  * CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
56
44
  * CACHLY_EMBED_MODEL – override embedding model (optional)
57
- * TCO_API_URL – Travel Chaos Organizer backend URL (optional, enables tco_* tools)
58
45
  */
59
46
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
60
47
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -707,6 +694,10 @@ import { handleAdvancedTool } from './handlers/advanced.js';
707
694
  import { handleSyndicateTool } from './handlers/syndicate.js';
708
695
  import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
709
696
  import { installClsPostCommitHook, CLS_HOOK_VERSION } from './cls-hook.js';
697
+ import { installAmbientHooks, AMBIENT_HOOK_VERSION } from './ambient-hooks.js';
698
+ import { runAmbient, truncateToTokens, parseHookPayload, stopObservation } from './ambient-cli.js';
699
+ import { appendLedgerEntry, readLedger, defaultLedgerPath } from './ambient-ledger.js';
700
+ import { netBalance, shouldBackoff } from './ambient-recall.js';
710
701
  import { handleShareTool } from './handlers/share.js';
711
702
  import { handleVizTool } from './handlers/viz.js';
712
703
  // ── Tools (imported from tools.ts) ─────────────────────────────────────────
@@ -761,6 +752,21 @@ function sendFunnelEvent(event, extra, metrics) {
761
752
  signal: AbortSignal.timeout(3000),
762
753
  }).catch(() => { });
763
754
  }
755
+ // Mirrors one ambient net-token ledger entry to the API (Phase 4 v2: the
756
+ // org-wide dashboard aggregates these per team). Best-effort fire-and-forget —
757
+ // the local JSONL ledger stays authoritative; this never throws and never
758
+ // blocks a hook. Returns the fetch promise so short-lived CLI paths can flush
759
+ // (bounded) before process.exit kills the socket.
760
+ function reportAmbientLedgerEvent(instanceId, entry) {
761
+ if (!instanceId || !JWT || process.env.CACHLY_NO_TELEMETRY === '1')
762
+ return Promise.resolve();
763
+ return fetch(`${API_URL}/api/v1/instances/${instanceId}/ambient-events`, {
764
+ method: 'POST',
765
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
766
+ body: JSON.stringify({ events: [entry] }),
767
+ signal: AbortSignal.timeout(3000),
768
+ }).then(() => undefined, () => undefined);
769
+ }
764
770
  async function sendAnonymousTelemetry(toolName) {
765
771
  if (_telemetryPingSent)
766
772
  return;
@@ -882,7 +888,7 @@ async function handleTool(name, args) {
882
888
  '',
883
889
  'After sign-in: call **any tool again** — your Brain activates instantly.',
884
890
  '',
885
- '✨ Free forever · No credit card · 136 MCP tools · GDPR · EU servers',
891
+ '✨ Free forever · No credit card · 126 MCP tools · GDPR · EU servers',
886
892
  ].join('\n');
887
893
  }
888
894
  // Device flow unavailable (network issue) — fall back to manual setup
@@ -1071,10 +1077,6 @@ async function handleTool(name, args) {
1071
1077
  }
1072
1078
  return fedbrainResult;
1073
1079
  }
1074
- // Delegate Travel Chaos Organizer tools (requires TCO_API_URL env var)
1075
- const tcoResult = await handleTcoTool(name, args, JWT ?? '');
1076
- if (tcoResult !== null)
1077
- return tcoResult;
1078
1080
  switch (name) {
1079
1081
  // ── Instance management ──────────────────────────────────────────────
1080
1082
  case 'get_api_status': {
@@ -1195,7 +1197,19 @@ async function handleTool(name, args) {
1195
1197
  return `⚠️ Unknown tool: ${name}`;
1196
1198
  }
1197
1199
  }
1198
- const server = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} } });
1200
+ // Pushed to every MCP host at connect time (initialize response). This is the
1201
+ // "SessionStart-equivalent" for harnesses without a per-prompt hook — Cursor,
1202
+ // Windsurf, Cline, Copilot agent-mode all surface `instructions`. It's a one-time
1203
+ // nudge (not per-prompt recall), so it stays short and behavior-focused.
1204
+ const CACHLY_MCP_INSTRUCTIONS = `This project has a Cachly AI Brain — persistent, cross-session memory of past fixes, decisions, and gotchas for THIS codebase. Use it proactively; it makes you faster and stops you repeating known mistakes.
1205
+
1206
+ - BEFORE a non-trivial task: call smart_recall with a short description of what you're about to do. If it returns lessons, apply them before researching from scratch.
1207
+ - WHEN debugging an error: call causal_trace with the exact error first — a known fix may already exist.
1208
+ - AFTER a fix, deploy, or discovery: call learn_from_attempts so the knowledge persists.
1209
+ - BEFORE a risky op (deploy, migration, dependency bump): call brain_predict.
1210
+
1211
+ The Brain instance id comes from the CACHLY_BRAIN_INSTANCE_ID environment variable.`;
1212
+ const server = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} }, instructions: CACHLY_MCP_INSTRUCTIONS });
1199
1213
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
1200
1214
  // ── Auto-session management ───────────────────────────────────────────────────
1201
1215
  // Transparently starts a Brain session on the first tool call of a process
@@ -1484,31 +1498,49 @@ Report predicted failure risks to the user before proceeding.
1484
1498
  Never commit code that does not compile. Run \`tsc --noEmit\` / \`go build ./...\` before every commit.
1485
1499
  ${CLAUDE_MD_MARKER_END}`;
1486
1500
  }
1501
+ // Cursor loads rules from .cursor/rules/*.mdc with YAML frontmatter. alwaysApply
1502
+ // makes the block active in every request (the closest Cursor gets to a
1503
+ // SessionStart briefing). The frontmatter sits OUTSIDE the brain markers so
1504
+ // idempotent marker-replacement leaves it intact.
1505
+ const CURSOR_MDC_FRONTMATTER = '---\n' +
1506
+ 'description: Cachly AI Brain — persistent memory protocol (recall before tasks, learn after fixes)\n' +
1507
+ 'alwaysApply: true\n' +
1508
+ '---\n\n';
1487
1509
  /**
1488
- * Schreibt das Brain-Protokoll in alle relevanten Instruction-Dateien:
1489
- * - CLAUDE.md
1490
- * - AGENTS.md
1491
- * - .github/copilot-instructions.md
1492
- * Idempotent, Marker-basiert.
1510
+ * Schreibt das Brain-Protokoll in alle relevanten Instruction-/Rules-Dateien:
1511
+ * - CLAUDE.md (Claude Code)
1512
+ * - AGENTS.md (Codex / generic agents)
1513
+ * - .github/copilot-instructions.md (GitHub Copilot)
1514
+ * - .cursor/rules/cachly.mdc (Cursor — frontmatter + alwaysApply)
1515
+ * - .windsurfrules (Windsurf / Cascade)
1516
+ * - .clinerules (Cline)
1517
+ * Idempotent, Marker-basiert. Cross-Harness Tier A (siehe make_cachly_great_again.md §6.7).
1493
1518
  */
1494
1519
  export async function writeInstructions(projectDir, instanceId) {
1495
1520
  const { writeFile, appendFile, readFile, mkdir } = await import('node:fs/promises');
1496
1521
  const { existsSync } = await import('node:fs');
1497
1522
  const { resolve, dirname } = await import('node:path');
1523
+ // `prefix` is written only when the file is created fresh (Cursor .mdc needs it);
1524
+ // on marker-update/append the prefix is left untouched.
1498
1525
  const files = [
1499
- resolve(projectDir, 'CLAUDE.md'),
1500
- resolve(projectDir, 'AGENTS.md'),
1501
- resolve(projectDir, '.github', 'copilot-instructions.md'),
1526
+ { path: resolve(projectDir, 'CLAUDE.md') },
1527
+ { path: resolve(projectDir, 'AGENTS.md') },
1528
+ { path: resolve(projectDir, '.github', 'copilot-instructions.md') },
1529
+ { path: resolve(projectDir, '.cursor', 'rules', 'cachly.mdc'), prefix: CURSOR_MDC_FRONTMATTER },
1530
+ { path: resolve(projectDir, '.windsurfrules') },
1531
+ { path: resolve(projectDir, '.clinerules') },
1502
1532
  ];
1503
1533
  const block = '\n' + buildClaudeMdBlock(instanceId) + '\n';
1504
1534
  const results = {};
1505
- for (const file of files) {
1535
+ for (const { path: file, prefix } of files) {
1506
1536
  await mkdir(dirname(file), { recursive: true });
1507
1537
  if (existsSync(file)) {
1508
1538
  const existing = await readFile(file, 'utf-8');
1509
1539
  if (existing.includes(CLAUDE_MD_MARKER_START)) {
1510
- // Idempotent update: replace existing block
1511
- const updated = existing.replace(new RegExp(`${CLAUDE_MD_MARKER_START}[\s\S]*?${CLAUDE_MD_MARKER_END}`), buildClaudeMdBlock(instanceId));
1540
+ // Idempotent update: replace existing block ([\s\S] must keep its
1541
+ // backslashes so it matches any char — a bare [\s\S] in the template
1542
+ // literal collapses to [sS] and the replace silently no-ops).
1543
+ const updated = existing.replace(new RegExp(`${CLAUDE_MD_MARKER_START}[\\s\\S]*?${CLAUDE_MD_MARKER_END}`), buildClaudeMdBlock(instanceId));
1512
1544
  await writeFile(file, updated, 'utf-8');
1513
1545
  results[file] = 'updated';
1514
1546
  continue;
@@ -1517,7 +1549,7 @@ export async function writeInstructions(projectDir, instanceId) {
1517
1549
  results[file] = 'appended';
1518
1550
  continue;
1519
1551
  }
1520
- await writeFile(file, block.trimStart(), 'utf-8');
1552
+ await writeFile(file, (prefix ?? '') + block.trimStart(), 'utf-8');
1521
1553
  results[file] = 'written';
1522
1554
  }
1523
1555
  return results;
@@ -2341,7 +2373,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
2341
2373
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
2342
2374
  console.log('');
2343
2375
  console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
2344
- console.log(' \x1b[90mFree forever · GDPR · German servers · 121 MCP tools\x1b[0m');
2376
+ console.log(' \x1b[90mFree forever · GDPR · German servers · 126 MCP tools\x1b[0m');
2345
2377
  console.log('');
2346
2378
  process.exit(0);
2347
2379
  }
@@ -2425,6 +2457,22 @@ if (process.argv[2] === 'init') {
2425
2457
  console.log(`✓ CLS hook already current in .git/hooks/post-commit`);
2426
2458
  }
2427
2459
  catch { /* non-critical — git hook is a best-effort feature */ }
2460
+ // ── Ambient Recall Phase 4: auto-install SessionStart + UserPromptSubmit hooks ─
2461
+ // Only for Claude Code (the hooks config lives in .claude/settings.json).
2462
+ if (editor === 'claude') {
2463
+ try {
2464
+ const a = await installAmbientHooks(projectDir, instanceId, JWT || undefined);
2465
+ const scriptVerb = a.scripts === 'written' ? '✅ Written' : a.scripts === 'upgraded' ? `✅ Upgraded (→ ${AMBIENT_HOOK_VERSION})` : '✓ Unchanged';
2466
+ console.log(`${scriptVerb}: .claude/hooks/ (Ambient Recall — session briefing, per-prompt recall, file briefing, auto-learn)`);
2467
+ if (a.settings === 'written')
2468
+ console.log(`✅ Written: .claude/settings.json (Ambient Recall hooks wired)`);
2469
+ else if (a.settings === 'merged')
2470
+ console.log(`✅ Merged: .claude/settings.json (Ambient Recall hooks wired)`);
2471
+ else
2472
+ console.log(`✓ Ambient Recall hooks already wired in .claude/settings.json`);
2473
+ }
2474
+ catch { /* non-critical — ambient hooks are a best-effort feature */ }
2475
+ }
2428
2476
  const initSecs = ((Date.now() - initStart) / 1000).toFixed(1);
2429
2477
  console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}! (${initSecs}s)`);
2430
2478
  console.log(` Restart your editor — the \`cachly\` MCP tools will appear.`);
@@ -3412,6 +3460,138 @@ if (process.argv[2] === 'learn-git') {
3412
3460
  }
3413
3461
  process.exit(0);
3414
3462
  }
3463
+ // ── ambient-recall: CLI entrypoint for the Claude Code hooks ──────────────────
3464
+ // Invoked as: <hook payload JSON on stdin> | cachly ambient-recall
3465
+ // SessionStart/UserPromptSubmit/PreToolUse payloads recall through the §6.3
3466
+ // relevance gate (ambient-cli.ts) and print the `hookSpecificOutput` JSON that
3467
+ // Claude Code injects as additionalContext — or nothing. Stop payloads instead
3468
+ // feed a fix-signal observation to auto_learn_session (the automatic
3469
+ // `learn_from_attempts`). Injections are booked into the net-token ledger
3470
+ // (§6.2) and auto-backoff kicks in when the recent window is net-negative.
3471
+ // Best-effort: no JWT, no stdin, a slow brain or any error → prints nothing and
3472
+ // exits 0 so a hook can NEVER block or corrupt the agent's turn.
3473
+ if (process.argv[2] === 'ambient-recall') {
3474
+ try {
3475
+ if (process.stdin.isTTY)
3476
+ process.exit(0); // no piped payload → nothing to do
3477
+ const chunks = [];
3478
+ for await (const chunk of process.stdin)
3479
+ chunks.push(chunk);
3480
+ const raw = Buffer.concat(chunks).toString('utf-8');
3481
+ if (!JWT)
3482
+ process.exit(0); // not authenticated → silent no-op
3483
+ const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? _defaultInstanceId;
3484
+ // Stop event → auto-learn, never inject (roadmap §6.1 PostToolUse/Stop row).
3485
+ const stopPayload = parseHookPayload(raw);
3486
+ if (stopPayload?.hook_event_name === 'Stop') {
3487
+ const obs = stopObservation(stopPayload);
3488
+ if (obs && instanceId) {
3489
+ await handleTool('auto_learn_session', { instance_id: instanceId, observations: [obs] });
3490
+ }
3491
+ process.exit(0);
3492
+ }
3493
+ let reported;
3494
+ const out = await runAmbient(raw, {
3495
+ timeoutMs: 3000, // §6.3 guardrail 4: never stall the prompt on a slow brain
3496
+ // Ambient injection budget: one relevance-ranked briefing, hard-capped.
3497
+ // smart_recall already returns its hits best-first, so the capped head is
3498
+ // the high-signal part. Per-lesson structured candidates are a future slice.
3499
+ gate: { topK: 1, maxTokens: 500 },
3500
+ backoff: async () => shouldBackoff(await readLedger()),
3501
+ onInject: (tokens, event) => {
3502
+ const entry = { ts: new Date().toISOString(), event, injected: tokens, prevented: 0 };
3503
+ void appendLedgerEntry(entry);
3504
+ reported = reportAmbientLedgerEvent(instanceId, entry); // org dashboard mirror
3505
+ },
3506
+ recall: async (query) => {
3507
+ if (!instanceId)
3508
+ return [];
3509
+ const text = String((await handleTool('smart_recall', { instance_id: instanceId, query })) ?? '');
3510
+ const miss = text.length < 80 || /No lessons found|no lessons|No matches found/i.test(text);
3511
+ if (miss)
3512
+ return [];
3513
+ return [{ id: 'ambient', summary: truncateToTokens(text, 500), confidence: 0.9, score: 0.9 }];
3514
+ },
3515
+ });
3516
+ if (out)
3517
+ process.stdout.write(out);
3518
+ // Bounded flush so the dashboard mirror survives process.exit — never more
3519
+ // than 500ms on top of a turn that already injected.
3520
+ if (reported)
3521
+ await Promise.race([reported, new Promise((r) => setTimeout(r, 500))]);
3522
+ }
3523
+ catch {
3524
+ // Swallow everything — an ambient hook must never break the agent.
3525
+ }
3526
+ process.exit(0);
3527
+ }
3528
+ // ── ambient-credit: agent-reported prevented-token credit (§6.2) ──────────────
3529
+ // Invoked as: cachly ambient-credit <tokens> [note…] — the injected context's
3530
+ // footer invites the agent to call this when a recalled lesson changed its path.
3531
+ // This is the client-side signal that makes the net ledger (and auto-backoff)
3532
+ // meaningful before the server-side dashboard exists. Silent + exit 0 always.
3533
+ if (process.argv[2] === 'ambient-credit') {
3534
+ try {
3535
+ const tokens = Math.round(Number(process.argv[3]));
3536
+ if (Number.isFinite(tokens) && tokens > 0) {
3537
+ // Cap a single credit: self-reported savings should never let one
3538
+ // enthusiastic claim mask weeks of negative balance.
3539
+ const capped = Math.min(tokens, 20_000);
3540
+ const note = process.argv.slice(4).join(' ').slice(0, 200) || undefined;
3541
+ const entry = { ts: new Date().toISOString(), event: 'credit', injected: 0, prevented: capped, note };
3542
+ await appendLedgerEntry(entry);
3543
+ await reportAmbientLedgerEvent(process.env.CACHLY_BRAIN_INSTANCE_ID ?? _defaultInstanceId, entry); // org dashboard mirror
3544
+ console.log(`✅ ambient credit recorded: ${capped} tokens${note ? ` (${note})` : ''}`);
3545
+ }
3546
+ else {
3547
+ console.error('Usage: npx @cachly-dev/mcp-server@latest ambient-credit <tokens-saved> [note]');
3548
+ }
3549
+ }
3550
+ catch { /* telemetry — never fail loudly */ }
3551
+ process.exit(0);
3552
+ }
3553
+ // ── ambient-stats: the honest net-token readout (§6.2) ────────────────────────
3554
+ // Shows injected vs prevented and the NET — even when it is negative — plus
3555
+ // whether auto-backoff is currently pausing injection.
3556
+ if (process.argv[2] === 'ambient-stats') {
3557
+ const entries = await readLedger();
3558
+ const bal = netBalance(entries);
3559
+ const backing = shouldBackoff(entries);
3560
+ const recent = entries.slice(-5);
3561
+ console.log('\n🧠 Ambient Recall — net-token ledger\n');
3562
+ console.log(` Turns recorded: ${entries.length}`);
3563
+ console.log(` Injected tokens: ${bal.injected}`);
3564
+ console.log(` Prevented tokens: ${bal.prevented} (agent-reported via ambient-credit)`);
3565
+ console.log(` NET: ${bal.net >= 0 ? '+' : ''}${bal.net} tokens`);
3566
+ console.log(` Auto-backoff: ${backing ? '🔴 ACTIVE — recent window is net-negative, injection paused' : '🟢 inactive'}`);
3567
+ if (recent.length > 0) {
3568
+ console.log('\n Last entries:');
3569
+ for (const e of recent) {
3570
+ const what = e.prevented > 0 ? `+${e.prevented} prevented` : `-${e.injected} injected`;
3571
+ console.log(` • ${e.ts.slice(0, 19)} ${e.event}: ${what}${e.note ? ` (${e.note})` : ''}`);
3572
+ }
3573
+ }
3574
+ console.log('');
3575
+ process.exit(0);
3576
+ }
3577
+ // ── doctor: one-command setup diagnosis ───────────────────────────────────────
3578
+ // Walks the whole activation chain — runtime → credential → API → auth →
3579
+ // instance → hooks → ledger — and prints one ✅/⚠️/❌ report with a concrete
3580
+ // next step per finding. Exit 1 only on hard failures (warnings stay 0).
3581
+ if (process.argv[2] === 'doctor') {
3582
+ const { checkNodeVersion, checkCredential, checkApiReachable, checkAuthAccepted, checkInstance, inspectAmbientHooks, checkHooks, checkLedger, renderDoctorReport, doctorExitCode, } = await import('./doctor.js');
3583
+ const checks = [
3584
+ checkNodeVersion(),
3585
+ checkCredential(JWT),
3586
+ await checkApiReachable(API_URL),
3587
+ await checkAuthAccepted(API_URL, JWT),
3588
+ checkInstance(process.env.CACHLY_BRAIN_INSTANCE_ID ?? _defaultInstanceId),
3589
+ checkHooks(inspectAmbientHooks(process.cwd())),
3590
+ checkLedger(await readLedger(), defaultLedgerPath()),
3591
+ ];
3592
+ console.log(renderDoctorReport(checks));
3593
+ process.exit(doctorExitCode(checks));
3594
+ }
3415
3595
  // ── cls-ingest: CLI entrypoint for the git post-commit hook ───────────────────
3416
3596
  // Invoked as: cachly cls-ingest '<json>' where json = { instance_id, source, payload }.
3417
3597
  // Auth comes from CACHLY_JWT (embedded in the generated hook). This is a silent,
@@ -3485,7 +3665,7 @@ if (!JWT) {
3485
3665
  // • Editor as an MCP stdio server (non-TTY) → ONE stderr hint, then KEEP RUNNING
3486
3666
  // so tools/list works and the first tool call starts the browser sign-in.
3487
3667
  // Skip entirely for CLI subcommands that intentionally run without credentials.
3488
- const _cliNoAuthCommands = ['demo', 'share', 'publish', 'health', 'autosetup', 'setup', 'autopilot', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade', 'bench', 'tool-specs', 'openapi'];
3668
+ const _cliNoAuthCommands = ['demo', 'share', 'publish', 'health', 'autosetup', 'setup', 'autopilot', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade', 'bench', 'tool-specs', 'openapi', 'ambient-credit', 'ambient-stats', 'doctor'];
3489
3669
  if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '')) {
3490
3670
  const runningInTerminal = !process.argv[2] && process.stdout.isTTY === true && _isMain;
3491
3671
  if (runningInTerminal) {