@cachly-dev/mcp-server 0.10.117 → 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 (38) hide show
  1. package/README.md +2 -2
  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 +211 -14
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/tools.d.ts +5 -0
  34. package/dist/src/tools.js +12 -1
  35. package/dist/src/tools.js.map +1 -1
  36. package/glama.json +1 -1
  37. package/package.json +2 -2
  38. package/server.json +4 -4
package/dist/src/index.js CHANGED
@@ -694,6 +694,10 @@ import { handleAdvancedTool } from './handlers/advanced.js';
694
694
  import { handleSyndicateTool } from './handlers/syndicate.js';
695
695
  import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
696
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';
697
701
  import { handleShareTool } from './handlers/share.js';
698
702
  import { handleVizTool } from './handlers/viz.js';
699
703
  // ── Tools (imported from tools.ts) ─────────────────────────────────────────
@@ -748,6 +752,21 @@ function sendFunnelEvent(event, extra, metrics) {
748
752
  signal: AbortSignal.timeout(3000),
749
753
  }).catch(() => { });
750
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
+ }
751
770
  async function sendAnonymousTelemetry(toolName) {
752
771
  if (_telemetryPingSent)
753
772
  return;
@@ -1178,7 +1197,19 @@ async function handleTool(name, args) {
1178
1197
  return `⚠️ Unknown tool: ${name}`;
1179
1198
  }
1180
1199
  }
1181
- 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 });
1182
1213
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
1183
1214
  // ── Auto-session management ───────────────────────────────────────────────────
1184
1215
  // Transparently starts a Brain session on the first tool call of a process
@@ -1467,31 +1498,49 @@ Report predicted failure risks to the user before proceeding.
1467
1498
  Never commit code that does not compile. Run \`tsc --noEmit\` / \`go build ./...\` before every commit.
1468
1499
  ${CLAUDE_MD_MARKER_END}`;
1469
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';
1470
1509
  /**
1471
- * Schreibt das Brain-Protokoll in alle relevanten Instruction-Dateien:
1472
- * - CLAUDE.md
1473
- * - AGENTS.md
1474
- * - .github/copilot-instructions.md
1475
- * 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).
1476
1518
  */
1477
1519
  export async function writeInstructions(projectDir, instanceId) {
1478
1520
  const { writeFile, appendFile, readFile, mkdir } = await import('node:fs/promises');
1479
1521
  const { existsSync } = await import('node:fs');
1480
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.
1481
1525
  const files = [
1482
- resolve(projectDir, 'CLAUDE.md'),
1483
- resolve(projectDir, 'AGENTS.md'),
1484
- 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') },
1485
1532
  ];
1486
1533
  const block = '\n' + buildClaudeMdBlock(instanceId) + '\n';
1487
1534
  const results = {};
1488
- for (const file of files) {
1535
+ for (const { path: file, prefix } of files) {
1489
1536
  await mkdir(dirname(file), { recursive: true });
1490
1537
  if (existsSync(file)) {
1491
1538
  const existing = await readFile(file, 'utf-8');
1492
1539
  if (existing.includes(CLAUDE_MD_MARKER_START)) {
1493
- // Idempotent update: replace existing block
1494
- 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));
1495
1544
  await writeFile(file, updated, 'utf-8');
1496
1545
  results[file] = 'updated';
1497
1546
  continue;
@@ -1500,7 +1549,7 @@ export async function writeInstructions(projectDir, instanceId) {
1500
1549
  results[file] = 'appended';
1501
1550
  continue;
1502
1551
  }
1503
- await writeFile(file, block.trimStart(), 'utf-8');
1552
+ await writeFile(file, (prefix ?? '') + block.trimStart(), 'utf-8');
1504
1553
  results[file] = 'written';
1505
1554
  }
1506
1555
  return results;
@@ -2408,6 +2457,22 @@ if (process.argv[2] === 'init') {
2408
2457
  console.log(`✓ CLS hook already current in .git/hooks/post-commit`);
2409
2458
  }
2410
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
+ }
2411
2476
  const initSecs = ((Date.now() - initStart) / 1000).toFixed(1);
2412
2477
  console.log(`\n🧠 Cachly AI Brain configured for ${editor === 'claude' ? 'Claude Code' : editor}! (${initSecs}s)`);
2413
2478
  console.log(` Restart your editor — the \`cachly\` MCP tools will appear.`);
@@ -3395,6 +3460,138 @@ if (process.argv[2] === 'learn-git') {
3395
3460
  }
3396
3461
  process.exit(0);
3397
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
+ }
3398
3595
  // ── cls-ingest: CLI entrypoint for the git post-commit hook ───────────────────
3399
3596
  // Invoked as: cachly cls-ingest '<json>' where json = { instance_id, source, payload }.
3400
3597
  // Auth comes from CACHLY_JWT (embedded in the generated hook). This is a silent,
@@ -3468,7 +3665,7 @@ if (!JWT) {
3468
3665
  // • Editor as an MCP stdio server (non-TTY) → ONE stderr hint, then KEEP RUNNING
3469
3666
  // so tools/list works and the first tool call starts the browser sign-in.
3470
3667
  // Skip entirely for CLI subcommands that intentionally run without credentials.
3471
- 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'];
3472
3669
  if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '')) {
3473
3670
  const runningInTerminal = !process.argv[2] && process.stdout.isTTY === true && _isMain;
3474
3671
  if (runningInTerminal) {