@cachly-dev/mcp-server 0.10.121 → 0.10.123

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 (58) hide show
  1. package/README.md +31 -15
  2. package/dist/src/ambient-deps.d.ts +26 -0
  3. package/dist/src/ambient-deps.js +55 -0
  4. package/dist/src/ambient-deps.js.map +1 -0
  5. package/dist/src/ambient-hooks.d.ts +11 -2
  6. package/dist/src/ambient-hooks.js +11 -2
  7. package/dist/src/ambient-hooks.js.map +1 -1
  8. package/dist/src/ambient-ledger.js +2 -2
  9. package/dist/src/ambient-ledger.js.map +1 -1
  10. package/dist/src/ambient-memory.js +2 -2
  11. package/dist/src/ambient-memory.js.map +1 -1
  12. package/dist/src/auth.js +2 -1
  13. package/dist/src/auth.js.map +1 -1
  14. package/dist/src/brain-export.d.ts +40 -0
  15. package/dist/src/brain-export.js +89 -0
  16. package/dist/src/brain-export.js.map +1 -0
  17. package/dist/src/cachly-url.d.ts +68 -0
  18. package/dist/src/cachly-url.js +21 -0
  19. package/dist/src/cachly-url.js.map +1 -0
  20. package/dist/src/credentials.d.ts +37 -0
  21. package/dist/src/credentials.js +85 -0
  22. package/dist/src/credentials.js.map +1 -0
  23. package/dist/src/doctor.js +2 -1
  24. package/dist/src/doctor.js.map +1 -1
  25. package/dist/src/first-recall-proof.d.ts +19 -0
  26. package/dist/src/first-recall-proof.js +41 -0
  27. package/dist/src/first-recall-proof.js.map +1 -0
  28. package/dist/src/handlers/advanced.js +13 -29
  29. package/dist/src/handlers/advanced.js.map +1 -1
  30. package/dist/src/handlers/brain.js +132 -41
  31. package/dist/src/handlers/brain.js.map +1 -1
  32. package/dist/src/handlers/cache.js +3 -2
  33. package/dist/src/handlers/cache.js.map +1 -1
  34. package/dist/src/handlers/fedbrain.js +15 -4
  35. package/dist/src/handlers/fedbrain.js.map +1 -1
  36. package/dist/src/handlers/share.js +4 -3
  37. package/dist/src/handlers/share.js.map +1 -1
  38. package/dist/src/handlers/team.js +2 -1
  39. package/dist/src/handlers/team.js.map +1 -1
  40. package/dist/src/index.d.ts +32 -2
  41. package/dist/src/index.js +308 -94
  42. package/dist/src/index.js.map +1 -1
  43. package/dist/src/kostprobe.d.ts +25 -0
  44. package/dist/src/kostprobe.js +125 -0
  45. package/dist/src/kostprobe.js.map +1 -0
  46. package/dist/src/telemetry-types.d.ts +1 -1
  47. package/dist/src/tools.d.ts +0 -99
  48. package/dist/src/tools.js +0 -110
  49. package/dist/src/tools.js.map +1 -1
  50. package/dist/src/toolspecs.d.ts +1 -1
  51. package/dist/src/toolspecs.js +1 -1
  52. package/dist/src/upgrade-nudge.d.ts +15 -0
  53. package/dist/src/upgrade-nudge.js +29 -0
  54. package/dist/src/upgrade-nudge.js.map +1 -0
  55. package/glama.json +1 -1
  56. package/package.json +2 -2
  57. package/scripts/postinstall.js +1 -1
  58. package/server.json +3 -3
package/dist/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { jwtExpiryMs, checkJwt, handleApiError, diagnoseAuth, planAuthHeal, readClientCredentialsFromEnv, buildClientCredentialsBody, clientCredentialsTokenUrl } from './auth.js';
3
+ import { cachlyUrl } from './cachly-url.js';
3
4
  import { notify } from './notifier.js';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { createRequire } from 'node:module';
@@ -52,7 +53,7 @@ import { Redis } from 'ioredis';
52
53
  // Mutable so the `setup`/`init` CLI can honor a `--api-url` flag for self-hosting.
53
54
  // Reassigned (if at all) before any network call is made.
54
55
  let API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
55
- let JWT = process.env.CACHLY_JWT ?? '';
56
+ let JWT = resolveApiKey() ?? '';
56
57
  const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
57
58
  // Resolve the package version at runtime from package.json so the telemetry
58
59
  // `version` field always matches the published npm version. A hardcoded constant
@@ -218,9 +219,11 @@ async function persistApiKeyToConfig(apiKey) {
218
219
  existing.env['CACHLY_JWT'] = apiKey;
219
220
  }
220
221
  else {
222
+ const freshEnv = {};
223
+ freshEnv['CACHLY_JWT'] = apiKey;
221
224
  cfg.mcpServers['cachly'] = {
222
225
  command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'],
223
- env: { CACHLY_JWT: apiKey },
226
+ env: freshEnv,
224
227
  };
225
228
  }
226
229
  await mkdir(dirname(claudePath), { recursive: true });
@@ -378,7 +381,7 @@ async function startDeviceFlow() {
378
381
  if (res.ok) {
379
382
  const data = await res.json();
380
383
  // Append the user_code so the web page (cachly.dev/device?code=…) auto-submits.
381
- const base = data.verification_uri || 'https://cachly.dev/device';
384
+ const base = data.verification_uri || cachlyUrl('/device', 'ambient-signin');
382
385
  const verifyUrl = `${base}${base.includes('?') ? '&' : '?'}code=${encodeURIComponent(data.user_code)}`;
383
386
  return {
384
387
  deviceCode: data.device_code,
@@ -472,6 +475,9 @@ async function pollDeviceFlow(flow) {
472
475
  // keep the token. Without this, JWT is lost on restart and brain_recall_count
473
476
  // is never incremented (telemetry has no api_key → no tenant resolution).
474
477
  void persistApiKeyToConfig(apiKey);
478
+ // GROW-015: also persist to ~/.cachly/credentials.json — the source the
479
+ // ambient hooks (bare OS processes, no MCP-config env) resolve at run time.
480
+ saveApiKey(apiKey);
475
481
  // Auto-provision: find or create the user's brain instance. resolveDefaultInstanceId
476
482
  // now lists existing instances and auto-provisions when the account has none,
477
483
  // so a single call covers both paths (and self-heals on every later tool call
@@ -627,20 +633,20 @@ async function getConnection(instance_id) {
627
633
  }
628
634
  else if (inst.status === 'failed') {
629
635
  hint = `❌ Brain instance "${inst.name}" failed to start.\n\n` +
630
- `Our system will retry automatically. Check status at: https://cachly.dev/instances\n` +
636
+ `Our system will retry automatically. Check status at: ${cachlyUrl('/instances', 'instance-error')}\n` +
631
637
  `If this persists, contact support@cachly.dev.`;
632
638
  }
633
639
  else if (inst.status === 'suspended') {
634
640
  hint = `⏸ Brain instance "${inst.name}" is suspended (billing issue).\n\n` +
635
- `Update your payment method at: https://cachly.dev/billing`;
641
+ `Update your payment method at: ${cachlyUrl('/billing', 'upgrade')}`;
636
642
  }
637
643
  else if (inst.status === 'pending_payment') {
638
644
  hint = `💳 Brain instance "${inst.name}" is waiting for payment.\n\n` +
639
- `Complete your checkout at: https://cachly.dev/instances`;
645
+ `Complete your checkout at: ${cachlyUrl('/instances', 'upgrade')}`;
640
646
  }
641
647
  else {
642
648
  hint = `Brain instance "${inst.name}" is not reachable (status: ${inst.status}).\n\n` +
643
- `• View your instance at: https://cachly.dev/instances\n` +
649
+ `• View your instance at: ${cachlyUrl('/instances', 'instance-error')}\n` +
644
650
  `• Run \`get_api_status\` for a full diagnostic.`;
645
651
  }
646
652
  throw new McpError(ErrorCode.InvalidRequest, hint);
@@ -754,14 +760,19 @@ import { handleRoadmapTool } from './handlers/roadmap.js';
754
760
  import { handleAdvancedTool } from './handlers/advanced.js';
755
761
  import { handleSyndicateTool } from './handlers/syndicate.js';
756
762
  import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
763
+ import { extractFirstRecallProof, renderFirstRecallProof } from './first-recall-proof.js';
757
764
  import { installClsPostCommitHook, CLS_HOOK_VERSION } from './cls-hook.js';
758
765
  import { installAmbientHooks, AMBIENT_HOOK_VERSION } from './ambient-hooks.js';
759
- import { runAmbient, truncateToTokens, parseHookPayload, stopObservation } from './ambient-cli.js';
766
+ import { runAmbient, parseHookPayload, stopObservation } from './ambient-cli.js';
760
767
  import { appendLedgerEntry, readLedger, defaultLedgerPath } from './ambient-ledger.js';
761
768
  import { loadAmbientMemory, saveAmbientMemory } from './ambient-memory.js';
769
+ import { buildAmbientDeps } from './ambient-deps.js';
770
+ import { resolveApiKey, saveApiKey } from './credentials.js';
771
+ import { buildLessonsMarkdown, buildLessonsJsonl } from './brain-export.js';
762
772
  import { detectEditor as detectEditorImpl } from './editor.js';
763
773
  import { milestoneSent, markMilestoneSent } from './funnel-milestones.js';
764
- import { candidateIdFor, netBalance, shouldBackoff } from './ambient-recall.js';
774
+ import { kostprobeUebrig, kostprobeVerbrauchen, kostprobeHinweis, schrankeNachKostproben, } from './kostprobe.js';
775
+ import { netBalance, shouldBackoff } from './ambient-recall.js';
765
776
  import { handleShareTool } from './handlers/share.js';
766
777
  import { handleVizTool } from './handlers/viz.js';
767
778
  // ── Tools (imported from tools.ts) ─────────────────────────────────────────
@@ -776,6 +787,24 @@ let _firstCallSuccessSent = false;
776
787
  function detectEditor() {
777
788
  return detectEditorImpl();
778
789
  }
790
+ /**
791
+ * Pulls topic + preview snippet out of a `smart_recall` result's top hit
792
+ * (format: `**💡 <topic>** ... \n> <preview>`). Returns `null` on any
793
+ * mismatch — the seed-path proof is a bonus, never invented.
794
+ */
795
+ function extractTopHit(text) {
796
+ if (!text)
797
+ return null;
798
+ const topicMatch = text.match(/\*\*💡\s+([^*]+)\*\*/);
799
+ const whatMatch = text.match(/^>\s+(.+)$/m);
800
+ if (!topicMatch || !whatMatch)
801
+ return null;
802
+ const topic = topicMatch[1].trim();
803
+ const what = whatMatch[1].trim();
804
+ if (!topic || !what)
805
+ return null;
806
+ return { topic, what };
807
+ }
779
808
  /** Derive an anonymous, non-reversible fingerprint from the JWT sub claim. */
780
809
  function _jwtUserFingerprint(jwt) {
781
810
  try {
@@ -871,7 +900,7 @@ const PREMIUM_PITCH = {
871
900
  global_learn: "Team Brain — contribute to your organization's shared memory.",
872
901
  global_recall: 'Team Brain — recall across your entire organization.',
873
902
  };
874
- const UPGRADE_URL_FG = 'https://cachly.dev/billing';
903
+ const UPGRADE_URL_FG = cachlyUrl('/billing', 'upgrade');
875
904
  const _tierCache = new Map();
876
905
  async function getTierInfo(instanceId) {
877
906
  const cached = _tierCache.get(instanceId);
@@ -889,7 +918,21 @@ async function getTierInfo(instanceId) {
889
918
  return { tier: 'unknown', isFree: false };
890
919
  }
891
920
  }
892
- /** Returns an upgrade teaser if a free-tier caller reached for a premium tool, else null. */
921
+ /**
922
+ * Entscheidet, was mit einem gesperrten Werkzeug auf der Gratis-Stufe
923
+ * passiert. Drei Ausgänge:
924
+ *
925
+ * null → durchlassen (kein Premium-Werkzeug, oder bezahlte Stufe,
926
+ * oder es sind noch Kostproben übrig)
927
+ * {sperre} → Text zurückgeben, Werkzeug NICHT ausführen
928
+ * {kostprobe} → durchlassen UND nach dem Ergebnis einen Hinweis anhängen
929
+ *
930
+ * Warum überhaupt Kostproben: Gemessen am 11.08.2026 ist `premium_gate_hit`
931
+ * 36-mal gefeuert — von einer einzigen Person, dem Entwickler selbst, über
932
+ * fünf Wochen. Er hat nicht aufgerüstet. Die Schranke gab eine
933
+ * Verkaufsansage, nie eine Kostprobe; man vermisst nicht, was man nie hatte.
934
+ * Einzelheiten in kostprobe.ts.
935
+ */
893
936
  async function featureGate(name, instanceId) {
894
937
  if (!PREMIUM_TOOLS.has(name) || !instanceId)
895
938
  return null;
@@ -897,14 +940,14 @@ async function featureGate(name, instanceId) {
897
940
  if (!isFree)
898
941
  return null;
899
942
  const pitch = PREMIUM_PITCH[name] ?? 'A Premium intelligence feature.';
900
- return [
901
- `🔒 **\`${name}\` is a Premium feature**`,
902
- ``,
903
- `${pitch}`,
904
- ``,
905
- `Your Free Brain keeps full keyword + causal-graph recall, learning, and sessions — forever.`,
906
- `Unlock the deeper layer (causal tracing · predictive risk · Team Brain) → ${UPGRADE_URL_FG}`,
907
- ].join('\n');
943
+ if (kostprobeUebrig(name)) {
944
+ // Durchlassen und danach kurz sagen, was das war. Der Zähler wird erst
945
+ // HIER erhöht, also nur wenn wirklich ausgeführt wird — ein abgebrochener
946
+ // Aufruf darf keine Kostprobe kosten.
947
+ const verbraucht = kostprobeVerbrauchen(name);
948
+ return { kostprobe: kostprobeHinweis(name, verbraucht, UPGRADE_URL_FG) };
949
+ }
950
+ return { sperre: schrankeNachKostproben(name, pitch, UPGRADE_URL_FG) };
908
951
  }
909
952
  async function handleTool(name, args) {
910
953
  // Guard: if no JWT, return actionable onboarding message instead of HTTP 401
@@ -954,7 +997,7 @@ async function handleTool(name, args) {
954
997
  'That’s it — your Brain activates automatically the moment you finish',
955
998
  'signing in. Just keep working; your next request arrives brain-powered.',
956
999
  '',
957
- '✨ Free forever · No credit card · 126 MCP tools · GDPR · EU servers',
1000
+ '✨ Free forever · No credit card · 122 MCP tools · GDPR · EU servers',
958
1001
  ].join('\n');
959
1002
  }
960
1003
  // Device flow unavailable (network issue) — fall back to manual setup
@@ -966,7 +1009,7 @@ async function handleTool(name, args) {
966
1009
  ' npx @cachly-dev/mcp-server@latest autopilot',
967
1010
  ' ```',
968
1011
  '',
969
- 'Or get your API key at: https://cachly.dev/setup-ai',
1012
+ `Or get your API key at: ${cachlyUrl('/setup-ai', 'ambient-signin')}`,
970
1013
  '',
971
1014
  '✨ Free tier includes: 1 Brain instance, persistent memory, semantic search.',
972
1015
  ].join('\n');
@@ -977,12 +1020,19 @@ async function handleTool(name, args) {
977
1020
  if (defaultId)
978
1021
  args = { ...args, instance_id: defaultId };
979
1022
  }
980
- // Feature-Gate: free tier reaching for a Premium tool gets the upgrade pitch
981
- // at the moment of need (the magic moment of basic recall stays untouched).
982
- const gateMsg = await featureGate(name, args.instance_id);
983
- if (gateMsg !== null) {
984
- sendFunnelEvent('premium_gate_hit', { tool: name, instance_id: args.instance_id ?? '' });
985
- return gateMsg;
1023
+ // Feature-Gate mit Kostprobe: die ersten Läufe eines Premium-Werkzeugs
1024
+ // laufen auf der Gratis-Stufe echt durch, erst danach kommt die Schranke.
1025
+ // Der Hinweis wird an das ECHTE Ergebnis angehängt, nicht davorgesetzt —
1026
+ // wirken soll die Antwort, nicht der Text.
1027
+ const gate = await featureGate(name, args.instance_id);
1028
+ let kostprobeHinweisText = '';
1029
+ if (gate !== null) {
1030
+ if ('sperre' in gate) {
1031
+ sendFunnelEvent('premium_gate_hit', { tool: name, instance_id: args.instance_id ?? '' });
1032
+ return gate.sperre;
1033
+ }
1034
+ kostprobeHinweisText = gate.kostprobe;
1035
+ sendFunnelEvent('premium_taste_used', { tool: name, instance_id: args.instance_id ?? '' });
986
1036
  }
987
1037
  // Delegate brain tools (learn, recall, session, etc.)
988
1038
  const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
@@ -1113,16 +1163,23 @@ async function handleTool(name, args) {
1113
1163
  const cacheResult = await handleCacheTool(name, args, getConnection, apiFetch);
1114
1164
  if (cacheResult !== null)
1115
1165
  return cacheResult;
1166
+ // Der Kostproben-Hinweis wird NUR hier angehaengt — an den drei Handlern,
1167
+ // die die gesperrten Werkzeuge bedienen: team.ts (team_*, global_*),
1168
+ // advanced.ts (causal_trace) und syndicate.ts (brain_predict und die
1169
+ // uebrigen Vorhersagen). Er kommt ans ENDE, hinter das echte Ergebnis:
1170
+ // wirken soll die Antwort, nicht der Text. Bei allen anderen Werkzeugen ist
1171
+ // kostprobeHinweisText leer und die Zeile aendert nichts.
1172
+ const mitHinweis = (r) => (kostprobeHinweisText ? r + kostprobeHinweisText : r);
1116
1173
  // Delegate team/brain advanced tools
1117
1174
  const teamResult = await handleTeamTool(name, args, getConnection, apiFetch);
1118
1175
  if (teamResult !== null)
1119
- return teamResult;
1176
+ return mitHinweis(teamResult);
1120
1177
  const roadmapResult = await handleRoadmapTool(name, args, getConnection, apiFetch);
1121
1178
  if (roadmapResult !== null)
1122
1179
  return roadmapResult;
1123
1180
  const advancedResult = await handleAdvancedTool(name, args, getConnection, apiFetch);
1124
1181
  if (advancedResult !== null)
1125
- return advancedResult;
1182
+ return mitHinweis(advancedResult);
1126
1183
  const syndicateResult = await handleSyndicateTool(name, args, getConnection, apiFetch);
1127
1184
  if (syndicateResult !== null) {
1128
1185
  // brain_predict is in syndicate handler — track telemetry here.
@@ -1131,7 +1188,7 @@ async function handleTool(name, args) {
1131
1188
  const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
1132
1189
  sendFunnelEvent('brain_predict', telemetryExtra);
1133
1190
  }
1134
- return syndicateResult;
1191
+ return mitHinweis(syndicateResult);
1135
1192
  }
1136
1193
  const shareResult = await handleShareTool(name, args, getConnection, apiFetch);
1137
1194
  if (shareResult !== null)
@@ -1180,7 +1237,7 @@ async function handleTool(name, args) {
1180
1237
  ` 💓 Health: ${healthStatus}`,
1181
1238
  ` 🔑 Auth: ❌ CACHLY_JWT not set`,
1182
1239
  ``,
1183
- `💡 Get your API token at https://cachly.dev/instances → Settings → API Token`,
1240
+ `💡 Get your API token at ${cachlyUrl('/instances', 'api-status')} → Settings → API Token`,
1184
1241
  ].join('\n');
1185
1242
  }
1186
1243
  // Decode JWT claims (inspection only, no verification)
@@ -1212,7 +1269,7 @@ async function handleTool(name, args) {
1212
1269
  const listRes = await apiFetch('/api/v1/instances');
1213
1270
  const instances = listRes.data ?? [];
1214
1271
  if (instances.length === 0) {
1215
- instanceInfo = '\n\n🧠 **Brain Instances:** none — create one at https://cachly.dev/instances';
1272
+ instanceInfo = `\n\n🧠 **Brain Instances:** none — create one at ${cachlyUrl('/instances', 'api-status')}`;
1216
1273
  }
1217
1274
  else {
1218
1275
  const lines = ['\n\n🧠 **Brain Instances:**'];
@@ -1270,9 +1327,18 @@ async function handleTool(name, args) {
1270
1327
  ].join('\n');
1271
1328
  }
1272
1329
  default:
1273
- return `⚠️ Unknown tool: ${name}`;
1330
+ throw new UnknownToolError(`Unknown tool: ${name}`);
1274
1331
  }
1275
1332
  }
1333
+ /**
1334
+ * Thrown by handleTool for a tool name that matches none of the delegated
1335
+ * handlers and none of the explicit switch cases. Caught by the top-level
1336
+ * request handler and turned into an `isError` result — never a silent
1337
+ * success — so a client can no longer mistake a missing handler for a
1338
+ * completed call.
1339
+ */
1340
+ class UnknownToolError extends Error {
1341
+ }
1276
1342
  // Pushed to every MCP host at connect time (initialize response). This is the
1277
1343
  // "SessionStart-equivalent" for harnesses without a per-prompt hook — Cursor,
1278
1344
  // Windsurf, Cline, Copilot agent-mode all surface `instructions`. It's a one-time
@@ -1381,6 +1447,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1381
1447
  return { content: [{ type: 'text', text }] };
1382
1448
  }
1383
1449
  catch (err) {
1450
+ if (err instanceof UnknownToolError) {
1451
+ return { content: [{ type: 'text', text: err.message }], isError: true };
1452
+ }
1384
1453
  if (err instanceof McpError)
1385
1454
  throw err;
1386
1455
  const msg = err.message ?? String(err);
@@ -1435,14 +1504,25 @@ export const EDITOR_FILES = {
1435
1504
  export const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
1436
1505
  export const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
1437
1506
  const DEFAULT_API_URL = 'https://api.cachly.dev';
1438
- // Build the env block written into an editor's MCP config. Self-hosting is
1439
- // first-class: if the operator runs setup/init against their own backend
1440
- // (CACHLY_API_URL set to a non-default URL), that URL is baked into the config so
1441
- // the editor-launched server talks to the self-hosted instance — not api.cachly.dev.
1442
- // For the default cloud backend we OMIT CACHLY_API_URL to keep configs clean (the
1443
- // binary already defaults to it).
1444
- export function buildServerEnv(apiKey, instanceId) {
1445
- const env = { CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId };
1507
+ /**
1508
+ * Builds the `env` block written into an editor's MCP config. Self-hosting is
1509
+ * first-class: if the operator runs setup/init against their own backend
1510
+ * (CACHLY_API_URL set to a non-default URL), that URL is baked into the config
1511
+ * so the editor-launched server talks to the self-hosted instance — not
1512
+ * api.cachly.dev. For the default cloud backend we OMIT CACHLY_API_URL to keep
1513
+ * configs clean (the binary already defaults to it).
1514
+ *
1515
+ * GROW-033: the API key itself is OMITTED by default, so every PROJECT config
1516
+ * this feeds (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, …) is safe
1517
+ * to commit. The running server resolves its key at start time via
1518
+ * `resolveApiKey` instead (env, then the home credentials file). Pass
1519
+ * `{ includeApiKey: true }` only for the one config that never leaves the
1520
+ * user's machine: the global `~/.claude/mcp.json`.
1521
+ */
1522
+ export function buildServerEnv(apiKey, instanceId, opts = {}) {
1523
+ const env = { CACHLY_BRAIN_INSTANCE_ID: instanceId };
1524
+ if (opts.includeApiKey)
1525
+ env.CACHLY_JWT = apiKey;
1446
1526
  if (API_URL && API_URL !== DEFAULT_API_URL)
1447
1527
  env.CACHLY_API_URL = API_URL;
1448
1528
  return env;
@@ -1482,9 +1562,19 @@ export function buildMcpConfig(apiKey, instanceId, editor) {
1482
1562
  },
1483
1563
  }, null, 2);
1484
1564
  }
1485
- // mergeMcpConfig reads an existing config file (if any), merges the cachly entry,
1486
- // and returns the updated JSON string — preserving all other MCP servers and settings.
1487
- export async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsOps) {
1565
+ /**
1566
+ * Reads an existing editor config file (if any), merges in the cachly entry,
1567
+ * and returns the updated JSON string — preserving all other MCP servers and
1568
+ * settings.
1569
+ *
1570
+ * BESTANDSSCHUTZ (GROW-033): before GROW-033 this file could carry the API
1571
+ * key in `env.CACHLY_JWT` as plaintext. If a legacy install still has that
1572
+ * Altbestand value in the file being merged, it is uebernommen into the home
1573
+ * credentials store via `saveApiKey` (the same store `resolveApiKey` already
1574
+ * reads) before the merged config below is built — so the project file stops
1575
+ * carrying the secret without anyone losing access.
1576
+ */
1577
+ export async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsOps, credOpts = {}) {
1488
1578
  const cachlyEntry = {
1489
1579
  command: 'npx',
1490
1580
  args: ['-y', '@cachly-dev/mcp-server@latest'],
@@ -1509,6 +1599,16 @@ export async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsO
1509
1599
  // Merge into experimental.modelContextProtocolServers array — replace cachly entry if present
1510
1600
  const exp = (existing['experimental'] ?? {});
1511
1601
  const servers = (exp['modelContextProtocolServers'] ?? []);
1602
+ let legacyKey;
1603
+ for (const s of servers) {
1604
+ const env = (s['env'] ?? {});
1605
+ if (env['CACHLY_JWT']) {
1606
+ legacyKey = env['CACHLY_JWT'];
1607
+ break;
1608
+ }
1609
+ }
1610
+ if (legacyKey)
1611
+ saveApiKey(legacyKey, credOpts);
1512
1612
  const filtered = servers.filter((s) => {
1513
1613
  const env = (s['env'] ?? {});
1514
1614
  return !env['CACHLY_JWT'] && !env['CACHLY_BRAIN_INSTANCE_ID'];
@@ -1520,12 +1620,20 @@ export async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsO
1520
1620
  if (editor === 'zed') {
1521
1621
  // Merge only context_servers.cachly — preserve all other Zed settings
1522
1622
  const cs = (existing['context_servers'] ?? {});
1623
+ const previousCachly = cs['cachly'];
1624
+ const legacyKey = previousCachly?.command?.env?.['CACHLY_JWT'];
1625
+ if (legacyKey)
1626
+ saveApiKey(legacyKey, credOpts);
1523
1627
  cs['cachly'] = { command: { path: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'], env: cachlyEntry.env }, settings: {} };
1524
1628
  existing['context_servers'] = cs;
1525
1629
  return JSON.stringify(existing, null, 2);
1526
1630
  }
1527
1631
  // Standard mcpServers format (Claude Code, Cursor, Windsurf, Copilot, Cline)
1528
1632
  const servers = (existing['mcpServers'] ?? {});
1633
+ const previousCachly = servers['cachly'];
1634
+ const legacyKey = previousCachly?.env?.['CACHLY_JWT'];
1635
+ if (legacyKey)
1636
+ saveApiKey(legacyKey, credOpts);
1529
1637
  servers['cachly'] = cachlyEntry;
1530
1638
  existing['mcpServers'] = servers;
1531
1639
  return JSON.stringify(existing, null, 2);
@@ -1658,7 +1766,7 @@ if (process.argv[2] === 'digest') {
1658
1766
  process.stdout.write('\n🧠 Fetching your weekly Brain digest...\n\n');
1659
1767
  try {
1660
1768
  // Fetch current stats
1661
- const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
1769
+ const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain-stats`, {
1662
1770
  headers: { Authorization: `Bearer ${apiKey}` },
1663
1771
  signal: AbortSignal.timeout(8000),
1664
1772
  });
@@ -1733,6 +1841,74 @@ if (process.argv[2] === 'digest') {
1733
1841
  }
1734
1842
  process.exit(0);
1735
1843
  }
1844
+ // ── CLI: cachly export ────────────────────────────────────────────────────────
1845
+ // Usage: npx @cachly-dev/mcp-server@latest export [--out DIR] [--format md|jsonl|both]
1846
+ // Writes your own Brain's lessons to disk: lessons.md to read, lessons.jsonl to
1847
+ // reuse. "No model lock-in — leave anytime and take your data," made real.
1848
+ if (process.argv[2] === 'export') {
1849
+ const { writeFile, mkdir } = await import('node:fs/promises');
1850
+ const { resolve } = await import('node:path');
1851
+ const argv = process.argv.slice(3);
1852
+ const flag = (name) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
1853
+ const format = flag('format') ?? 'both';
1854
+ if (format !== 'md' && format !== 'jsonl' && format !== 'both') {
1855
+ console.log(`\n❌ Unknown --format "${format}" — use md, jsonl, or both.\n`);
1856
+ process.exit(1);
1857
+ }
1858
+ const outDir = resolve(flag('out') ?? '.');
1859
+ const apiKey = JWT || resolveApiKey() || '';
1860
+ const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
1861
+ if (!apiKey || !instanceId) {
1862
+ console.log('\n⚠️ CACHLY_JWT and CACHLY_BRAIN_INSTANCE_ID must be set.');
1863
+ console.log(' Run: npx @cachly-dev/mcp-server@latest autopilot\n');
1864
+ process.exit(1);
1865
+ }
1866
+ process.stdout.write('\n📤 Exporting your Brain...\n\n');
1867
+ try {
1868
+ const memRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/memory`, {
1869
+ headers: { Authorization: `Bearer ${apiKey}` },
1870
+ signal: AbortSignal.timeout(8000),
1871
+ });
1872
+ if (!memRes.ok)
1873
+ throw new Error(`memory HTTP ${memRes.status}`);
1874
+ const mem = await memRes.json();
1875
+ const lessons = (mem.top_lessons ?? []).map((l) => ({
1876
+ topic: l.topic,
1877
+ outcome: l.outcome,
1878
+ severity: l.severity,
1879
+ tags: l.tags,
1880
+ whatWorked: l.what_worked,
1881
+ ts: l.ts,
1882
+ author: l.author,
1883
+ recallCount: l.recall_count,
1884
+ }));
1885
+ if (lessons.length === 0) {
1886
+ console.log(' No lessons in this Brain yet — nothing to export.\n');
1887
+ process.exit(0);
1888
+ }
1889
+ await mkdir(outDir, { recursive: true });
1890
+ const written = [];
1891
+ if (format === 'md' || format === 'both') {
1892
+ const mdPath = resolve(outDir, 'lessons.md');
1893
+ await writeFile(mdPath, buildLessonsMarkdown(lessons), 'utf-8');
1894
+ written.push(`${lessons.length} lesson${lessons.length === 1 ? '' : 's'} → ${mdPath}`);
1895
+ }
1896
+ if (format === 'jsonl' || format === 'both') {
1897
+ const jsonlPath = resolve(outDir, 'lessons.jsonl');
1898
+ await writeFile(jsonlPath, buildLessonsJsonl(lessons), 'utf-8');
1899
+ written.push(`${lessons.length} lesson${lessons.length === 1 ? '' : 's'} → ${jsonlPath}`);
1900
+ }
1901
+ console.log('✅ Export complete:');
1902
+ for (const line of written)
1903
+ console.log(` ${line}`);
1904
+ console.log('');
1905
+ }
1906
+ catch (e) {
1907
+ console.log(`\n❌ Could not export your Brain: ${e.message}\n`);
1908
+ process.exit(1);
1909
+ }
1910
+ process.exit(0);
1911
+ }
1736
1912
  // ── CLI: cachly invite ────────────────────────────────────────────────────────
1737
1913
  // Usage: npx @cachly-dev/mcp-server@latest invite [email]
1738
1914
  // Invites a teammate to share your Brain. Fastest referral loop.
@@ -1752,7 +1928,7 @@ if (process.argv[2] === 'invite') {
1752
1928
  if (!res.ok)
1753
1929
  throw new Error(`HTTP ${res.status}`);
1754
1930
  const data = await res.json();
1755
- const referralUrl = data.url ?? `https://cachly.dev/r/${data.code}`;
1931
+ const referralUrl = data.url ?? cachlyUrl(`/r/${data.code}`, 'invite');
1756
1932
  const count = data.referral_count ?? 0;
1757
1933
  const slackMsg = `Hey, I've been using cachly to give my AI persistent memory across sessions — no more re-explaining my stack every morning.\n\nYou get a free Brain: ${referralUrl}`;
1758
1934
  const tweetText = encodeURIComponent(`I gave my AI persistent memory. It remembers fixes, patterns, and context across every session — no more re-explaining.\n\nTry it free: ${referralUrl}\n\n@cachlydev`);
@@ -1890,7 +2066,7 @@ if (process.argv[2] === 'join') {
1890
2066
  }
1891
2067
  catch (e) {
1892
2068
  console.log(`\n ⚠️ Could not start sign-in: ${e.message}`);
1893
- console.log(` Set CACHLY_JWT manually from https://cachly.dev/setup-ai\n`);
2069
+ console.log(` Set CACHLY_JWT manually from ${cachlyUrl('/setup-ai', 'join')}\n`);
1894
2070
  process.exit(1);
1895
2071
  }
1896
2072
  }
@@ -2074,7 +2250,7 @@ if (process.argv[2] === 'bench') {
2074
2250
  console.log(' \x1b[1m→ Give your AI this recall quality on your own work (free, 1–5 min):\x1b[0m');
2075
2251
  console.log(' \x1b[32m$ npx @cachly-dev/mcp-server@latest autopilot\x1b[0m');
2076
2252
  console.log(' \x1b[90m Signs in, configures every editor, bootstraps from your git history.\x1b[0m');
2077
- console.log(' \x1b[90m Or create your Brain at: \x1b[36mhttps://cachly.dev/setup-ai\x1b[0m');
2253
+ console.log(` \x1b[90m Or create your Brain at: \x1b[36m${cachlyUrl('/setup-ai', 'bench')}\x1b[0m`);
2078
2254
  console.log('');
2079
2255
  }
2080
2256
  catch (e) {
@@ -2232,7 +2408,7 @@ if (process.argv[2] === 'demo') {
2232
2408
  authors: String(authors.size),
2233
2409
  hours: String(hoursWasted),
2234
2410
  });
2235
- const previewURL = `https://cachly.dev/preview?${previewParams.toString()}`;
2411
+ const previewURL = cachlyUrl(`/preview?${previewParams.toString()}`, 'demo');
2236
2412
  console.log(' \x1b[1mMake this permanent (free, 1–5 minutes):\x1b[0m');
2237
2413
  console.log(' \x1b[32m$ npx @cachly-dev/mcp-server@latest autopilot\x1b[0m');
2238
2414
  console.log('');
@@ -2256,7 +2432,7 @@ if (process.argv[2] === 'share') {
2256
2432
  }
2257
2433
  process.stdout.write('\n🧠 Fetching your Brain stats...\n');
2258
2434
  try {
2259
- const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
2435
+ const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain-stats`, {
2260
2436
  headers: { Authorization: `Bearer ${apiKey}` },
2261
2437
  signal: AbortSignal.timeout(8000),
2262
2438
  });
@@ -2287,7 +2463,7 @@ if (process.argv[2] === 'share') {
2287
2463
  console.log('└─────────────────────────────────────────────────────────────┘');
2288
2464
  console.log('');
2289
2465
  // Tweet text
2290
- const shareUrl = 'https://cachly.dev?ref=share&utm_source=x&utm_medium=social&utm_campaign=cli-share';
2466
+ const shareUrl = cachlyUrl('/', 'share');
2291
2467
  const tweet = `🧠 My AI coding assistant now has persistent memory.\n\n${lessons} lessons stored · ${recalls} recalls · ${level}\n\nFix a bug once → AI remembers forever. Across Claude Code, Cursor, Windsurf, Copilot.\n\n${shareUrl}\n\n#AIMemory #ClaudeCode #Cursor #DeveloperTools`;
2292
2468
  console.log(' \x1b[1m📋 Share on Twitter/X (copy this):\x1b[0m');
2293
2469
  console.log(' ─────────────────────────────────────');
@@ -2322,7 +2498,7 @@ if (process.argv[2] === 'publish') {
2322
2498
  process.stdout.write('\n🧠 Publishing Brain snapshot...\n');
2323
2499
  try {
2324
2500
  // Fetch stats for the card
2325
- const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
2501
+ const statsRes = await fetch(`${API_URL}/api/v1/instances/${instanceId}/brain-stats`, {
2326
2502
  headers: { Authorization: `Bearer ${apiKey}` },
2327
2503
  signal: AbortSignal.timeout(8000),
2328
2504
  });
@@ -2343,7 +2519,7 @@ if (process.argv[2] === 'publish') {
2343
2519
  throw new Error(`HTTP ${shareRes.status}`);
2344
2520
  const share = await shareRes.json();
2345
2521
  const shareId = share.share_id ?? 'unavailable';
2346
- const shareUrl = `https://cachly.dev/brain/share/${shareId}`;
2522
+ const shareUrl = cachlyUrl(`/brain/share/${shareId}`, 'publish');
2347
2523
  console.log('');
2348
2524
  console.log('┌─────────────────────────────────────────────────────────────┐');
2349
2525
  console.log('│ 📤 Brain Published · powered by \x1b[35mcachly.dev\x1b[0m │');
@@ -2372,7 +2548,7 @@ if (process.argv[2] === 'publish') {
2372
2548
  process.exit(0);
2373
2549
  }
2374
2550
  // ── CLI: cachly tool-specs / openapi ───────────────────────────────────────────
2375
- // Emit the 120-tool surface in any agent framework's dialect, derived from the
2551
+ // Emit the 122-tool surface in any agent framework's dialect, derived from the
2376
2552
  // single TOOLS source of truth. Lets OpenAI Assistants, the Anthropic Messages
2377
2553
  // API, LangChain, CrewAI and AutoGen wrap cachly without hand-written glue.
2378
2554
  // npx @cachly-dev/mcp-server tool-specs --format=openai > cachly.openai.json
@@ -2412,7 +2588,7 @@ if (process.argv[2] === 'badge') {
2412
2588
  process.exit(1);
2413
2589
  }
2414
2590
  const badgeUrl = `${API_URL}/api/v1/badge/${instanceId}`;
2415
- const targetUrl = 'https://cachly.dev';
2591
+ const targetUrl = cachlyUrl('/', 'badge');
2416
2592
  const markdown = `[![cachly Brain](${badgeUrl})](${targetUrl})`;
2417
2593
  const htmlBadge = `<a href="${targetUrl}"><img src="${badgeUrl}" alt="cachly Brain" /></a>`;
2418
2594
  console.log('');
@@ -2454,6 +2630,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
2454
2630
  console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest demo\x1b[0m ← Try it first (no account)');
2455
2631
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest setup\x1b[0m ← Interactive setup (pick editors)');
2456
2632
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest health\x1b[0m ← Check everything works');
2633
+ console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest doctor\x1b[0m ← Diagnose a broken or missing install');
2457
2634
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest digest\x1b[0m ← Weekly Brain summary');
2458
2635
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest share\x1b[0m ← Share your Brain stats');
2459
2636
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest badge\x1b[0m ← README badge for your Brain');
@@ -2463,7 +2640,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
2463
2640
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
2464
2641
  console.log('');
2465
2642
  console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
2466
- console.log(' \x1b[90mFree forever · GDPR · German servers · 126 MCP tools\x1b[0m');
2643
+ console.log(' \x1b[90mFree forever · GDPR · German servers · 122 MCP tools\x1b[0m');
2467
2644
  console.log('');
2468
2645
  process.exit(0);
2469
2646
  }
@@ -2507,7 +2684,7 @@ if (process.argv[2] === 'init') {
2507
2684
  if (!instanceId || !apiKey) {
2508
2685
  console.error('\nUsage: npx @cachly-dev/mcp-server@latest init --instance-id <uuid> --api-key <cky_live_...> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path] [--api-url https://your-self-hosted-backend]\n');
2509
2686
  console.error('First time? Run the zero-config wizard (signs you in, auto-provisions): npx @cachly-dev/mcp-server@latest autopilot\n');
2510
- console.error('Get your credentials from: https://cachly.dev/setup-ai\n');
2687
+ console.error(`Get your credentials from: ${cachlyUrl('/setup-ai', 'init')}\n`);
2511
2688
  process.exit(1);
2512
2689
  }
2513
2690
  const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
@@ -2516,6 +2693,7 @@ if (process.argv[2] === 'init') {
2516
2693
  const { existsSync: exInit } = await import('node:fs');
2517
2694
  const configExisted = exInit(configPath);
2518
2695
  const prevConfig = configExisted ? await readFile(configPath, 'utf-8').catch(() => '') : '';
2696
+ saveApiKey(apiKey);
2519
2697
  const merged = await mergeMcpConfig(configPath, apiKey, instanceId, editor, { readFile, existsSync: exInit });
2520
2698
  // Idempotent: only write when the content actually changes.
2521
2699
  if (merged !== prevConfig) {
@@ -2551,7 +2729,7 @@ if (process.argv[2] === 'init') {
2551
2729
  // Only for Claude Code (the hooks config lives in .claude/settings.json).
2552
2730
  if (editor === 'claude') {
2553
2731
  try {
2554
- const a = await installAmbientHooks(projectDir, instanceId, JWT || undefined);
2732
+ const a = await installAmbientHooks(projectDir, instanceId);
2555
2733
  const scriptVerb = a.scripts === 'written' ? '✅ Written' : a.scripts === 'upgraded' ? `✅ Upgraded (→ ${AMBIENT_HOOK_VERSION})` : '✓ Unchanged';
2556
2734
  console.log(`${scriptVerb}: .claude/hooks/ (Ambient Recall — session briefing, per-prompt recall, file briefing, auto-learn)`);
2557
2735
  if (a.settings === 'written')
@@ -2587,7 +2765,7 @@ if (process.argv[2] === 'status') {
2587
2765
  }
2588
2766
  try {
2589
2767
  const [statsRes, referralRes] = await Promise.all([
2590
- fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
2768
+ fetch(`${API_URL}/api/v1/instances/${instanceId}/brain-stats`, {
2591
2769
  headers: { Authorization: `Bearer ${apiKey}` },
2592
2770
  signal: AbortSignal.timeout(8000),
2593
2771
  }),
@@ -2676,10 +2854,10 @@ if (process.argv[2] === 'health') {
2676
2854
  const expMs = payload.exp ? payload.exp * 1000 : null;
2677
2855
  const minsLeft = expMs ? Math.floor((expMs - Date.now()) / 60_000) : null;
2678
2856
  if (expMs && expMs < Date.now()) {
2679
- fail(`JWT expired ${Math.abs(minsLeft)} minute(s) ago — get a new one: https://cachly.dev/setup-ai`);
2857
+ fail(`JWT expired ${Math.abs(minsLeft)} minute(s) ago — get a new one: ${cachlyUrl('/setup-ai', 'health')}`);
2680
2858
  }
2681
2859
  else if (minsLeft !== null && minsLeft < 30) {
2682
- warn(`JWT expires in ${minsLeft} minute(s) — refresh soon at https://cachly.dev/setup-ai`);
2860
+ warn(`JWT expires in ${minsLeft} minute(s) — refresh soon at ${cachlyUrl('/setup-ai', 'health')}`);
2683
2861
  }
2684
2862
  else {
2685
2863
  ok(`JWT valid${minsLeft !== null ? ` (expires in ${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m)` : ''}`);
@@ -2883,6 +3061,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
2883
3061
  const { resolve, dirname } = await import('node:path');
2884
3062
  const { createInterface } = await import('node:readline');
2885
3063
  const setupStartMs = Date.now();
3064
+ const cliSource = _isAutopilotCli ? 'autopilot' : 'setup';
2886
3065
  // Self-hosting: `setup --api-url https://cachly.mycorp.internal` points the whole
2887
3066
  // wizard (auth, provisioning, config writes) at a private backend.
2888
3067
  const _apiUrlIdx = process.argv.indexOf('--api-url');
@@ -3031,7 +3210,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3031
3210
  else {
3032
3211
  // Device flow unavailable — web fallback with automatic browser open
3033
3212
  sendFunnelEvent('device_flow_failed', { reason: 'device_flow_unavailable' });
3034
- const signupUrl = 'https://cachly.dev/setup-ai';
3213
+ const signupUrl = cachlyUrl('/setup-ai', cliSource);
3035
3214
  console.log(' ⚠️ Could not start automatic sign-in. Opening browser for web sign-in...\n');
3036
3215
  console.log(` URL: \x1b[36m${signupUrl}\x1b[0m`);
3037
3216
  console.log(' 1. Sign up / log in at the URL above');
@@ -3102,8 +3281,8 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3102
3281
  const msg = e.message;
3103
3282
  console.error(`\n\nFailed to fetch instances: ${msg}`);
3104
3283
  if (msg.includes('401')) {
3105
- console.error('Token rejected. Get a valid token at https://cachly.dev/setup-ai\n');
3106
- openInBrowser('https://cachly.dev/setup-ai');
3284
+ console.error(`Token rejected. Get a valid token at ${cachlyUrl('/setup-ai', cliSource)}\n`);
3285
+ openInBrowser(cachlyUrl('/setup-ai', cliSource));
3107
3286
  }
3108
3287
  rl.close();
3109
3288
  process.exit(1);
@@ -3152,8 +3331,8 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3152
3331
  console.log(` failed: ${e.message}\n`);
3153
3332
  }
3154
3333
  if (instances.length === 0) {
3155
- console.error('\nCould not create an instance automatically. Opening https://cachly.dev/instances …\n');
3156
- openInBrowser('https://cachly.dev/instances');
3334
+ console.error(`\nCould not create an instance automatically. Opening ${cachlyUrl('/instances', cliSource)} …\n`);
3335
+ openInBrowser(cachlyUrl('/instances', cliSource));
3157
3336
  rl.close();
3158
3337
  process.exit(1);
3159
3338
  }
@@ -3207,6 +3386,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3207
3386
  // always claiming "Brain is ready" even when a config never got written.
3208
3387
  const configWriteFailures = [];
3209
3388
  let configWriteSuccesses = 0;
3389
+ saveApiKey(token);
3210
3390
  for (const editor of editorsToSetup) {
3211
3391
  const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
3212
3392
  const configPath = resolve(cwd, configFile);
@@ -3242,7 +3422,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3242
3422
  globalConfig.mcpServers['cachly'] = {
3243
3423
  command: 'npx',
3244
3424
  args: ['-y', '@cachly-dev/mcp-server@latest'],
3245
- env: buildServerEnv(token, instance.id),
3425
+ env: buildServerEnv(token, instance.id, { includeApiKey: true }),
3246
3426
  };
3247
3427
  await writeFile(globalClaudePath, JSON.stringify(globalConfig, null, 2), 'utf-8');
3248
3428
  console.log(`✅ Written: ~/.claude/mcp.json (global — works in every project)`);
@@ -3272,9 +3452,14 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3272
3452
  repo_path: cwd,
3273
3453
  limit: 100,
3274
3454
  });
3275
- const match = gitResult.match(/(\d+) lesson/);
3276
- gitLessonCount = match ? parseInt(match[1], 10) : 0;
3277
- console.log(` ✓ ${match ? match[1] + ' lessons' : 'done'} extracted from git history`);
3455
+ const ingestedMatch = gitResult.match(/Ingested: \*\*(\d+)\*\*/);
3456
+ gitLessonCount = ingestedMatch ? parseInt(ingestedMatch[1], 10) : 0;
3457
+ console.log(` ✓ ${gitLessonCount} lesson${gitLessonCount === 1 ? '' : 's'} extracted from git history`);
3458
+ const gitProofBlock = extractFirstRecallProof(gitResult);
3459
+ if (gitProofBlock) {
3460
+ console.log('');
3461
+ console.log(gitProofBlock);
3462
+ }
3278
3463
  }
3279
3464
  catch (e) {
3280
3465
  console.log(` (skipped: ${e.message.slice(0, 80)})`);
@@ -3291,6 +3476,21 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3291
3476
  try {
3292
3477
  await handleTool('brain_seed_starter', { instance_id: instance.id });
3293
3478
  console.log(' ✓');
3479
+ // Bonus, never a blocker: run one real recall against the corpus we just
3480
+ // seeded and show the top hit, so the seed path gets a proof too — not
3481
+ // just the weaker '✓' it used to leave the user with.
3482
+ try {
3483
+ const seedRecallText = await handleTool('smart_recall', {
3484
+ instance_id: instance.id,
3485
+ query: 'docker build slow',
3486
+ });
3487
+ const seedHit = extractTopHit(seedRecallText);
3488
+ if (seedHit) {
3489
+ console.log('');
3490
+ console.log(renderFirstRecallProof(seedHit.topic, seedHit.what));
3491
+ }
3492
+ }
3493
+ catch { /* proof is a bonus, never blocks setup */ }
3294
3494
  }
3295
3495
  catch (e) {
3296
3496
  console.log(` (skipped: ${e.message.slice(0, 60)})`);
@@ -3300,7 +3500,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3300
3500
  // Fetch brain health from the API to show the user what their agent will see.
3301
3501
  process.stdout.write('\n⏳ Fetching your Brain health preview...');
3302
3502
  try {
3303
- const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain/stats`, {
3503
+ const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain-stats`, {
3304
3504
  headers: { Authorization: `Bearer ${token}` },
3305
3505
  signal: AbortSignal.timeout(6000),
3306
3506
  });
@@ -3370,7 +3570,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3370
3570
  console.log(`\n🎉 Your AI just got a permanent brain.`);
3371
3571
  console.log(` Restart your editor — from this session on, it arrives pre-briefed.\n`);
3372
3572
  console.log(` No more re-explaining your stack. No repeated mistakes.\n`);
3373
- console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
3573
+ console.log(` Dashboard: ${cachlyUrl(`/instances/${instance.id}`, cliSource)}`);
3374
3574
  console.log(`\n 📛 Add a live badge to your README:`);
3375
3575
  console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
3376
3576
  // ── Step 6c: Governance bootstrap — assign the first admin role ─────────────
@@ -3425,6 +3625,24 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3425
3625
  console.log(`\n✅ CLS git hook appended to existing post-commit.`);
3426
3626
  }
3427
3627
  catch { /* non-critical */ }
3628
+ // ── Ambient Recall Phase 4: auto-install SessionStart + UserPromptSubmit hooks ─
3629
+ // Only for Claude Code (the hooks config lives in .claude/settings.json). Without
3630
+ // this call, setup/autosetup/autopilot never install the hooks that make the
3631
+ // Brain speak up on its own — only `init` did (the bug POL-034 fixes).
3632
+ if (editorsToSetup.includes('claude')) {
3633
+ try {
3634
+ const a = await installAmbientHooks(process.cwd(), instance.id);
3635
+ const scriptVerb = a.scripts === 'written' ? '✅ Written' : a.scripts === 'upgraded' ? `✅ Upgraded (→ ${AMBIENT_HOOK_VERSION})` : '✓ Unchanged';
3636
+ console.log(`${scriptVerb}: .claude/hooks/ (Ambient Recall — session briefing, per-prompt recall, file briefing, auto-learn)`);
3637
+ if (a.settings === 'written')
3638
+ console.log(`✅ Written: .claude/settings.json (Ambient Recall hooks wired)`);
3639
+ else if (a.settings === 'merged')
3640
+ console.log(`✅ Merged: .claude/settings.json (Ambient Recall hooks wired)`);
3641
+ else
3642
+ console.log(`✓ Ambient Recall hooks already wired in .claude/settings.json`);
3643
+ }
3644
+ catch { /* non-critical — ambient hooks are a best-effort feature */ }
3645
+ }
3428
3646
  // Setup reached the end — close the funnel. Report config-write outcome so the
3429
3647
  // weekly funnel can spot permission/path problems blocking activation.
3430
3648
  JWT = token;
@@ -3452,7 +3670,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3452
3670
  console.log(` • ${f.file} — ${f.reason}`);
3453
3671
  console.log('\n Fix the permissions above and re-run, or configure manually:');
3454
3672
  console.log(` CACHLY_JWT=${token.slice(0, 12)}… CACHLY_BRAIN_INSTANCE_ID=${instance.id}`);
3455
- console.log(` Docs: https://cachly.dev/docs/mcp\n`);
3673
+ console.log(` Docs: ${cachlyUrl('/docs/mcp', cliSource)}\n`);
3456
3674
  rl.close();
3457
3675
  await new Promise(r => setTimeout(r, 300));
3458
3676
  process.exit(1);
@@ -3568,8 +3786,15 @@ if (process.argv[2] === 'ambient-recall') {
3568
3786
  for await (const chunk of process.stdin)
3569
3787
  chunks.push(chunk);
3570
3788
  const raw = Buffer.concat(chunks).toString('utf-8');
3789
+ // GROW-015: hooks run as bare OS processes and never see the MCP config's
3790
+ // env, so JWT is usually still empty here — resolve it the same way every
3791
+ // ambient/CLI caller does (env, then ~/.cachly, then a legacy .mcp.json).
3571
3792
  if (!JWT)
3572
- process.exit(0); // not authenticated → silent no-op
3793
+ JWT = resolveApiKey() ?? '';
3794
+ if (!JWT) {
3795
+ console.error('cachly ambient-recall: no API key found (CACHLY_JWT/CACHLY_API_KEY env, ~/.cachly/credentials.json, or .mcp.json) — recall skipped for this turn.');
3796
+ process.exit(0);
3797
+ }
3573
3798
  const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? _defaultInstanceId;
3574
3799
  // Stop event → auto-learn, never inject (roadmap §6.1 PostToolUse/Stop row).
3575
3800
  const stopPayload = parseHookPayload(raw);
@@ -3581,12 +3806,9 @@ if (process.argv[2] === 'ambient-recall') {
3581
3806
  process.exit(0);
3582
3807
  }
3583
3808
  let reported;
3584
- const out = await runAmbient(raw, {
3585
- timeoutMs: 3000, // §6.3 guardrail 4: never stall the prompt on a slow brain
3586
- // Ambient injection budget: one relevance-ranked briefing, hard-capped.
3587
- // smart_recall already returns its hits best-first, so the capped head is
3588
- // the high-signal part. Per-lesson structured candidates are a future slice.
3589
- gate: { topK: 1, maxTokens: 500 },
3809
+ const out = await runAmbient(raw, buildAmbientDeps({
3810
+ instanceId,
3811
+ smartRecall: async (query) => String((await handleTool('smart_recall', { instance_id: instanceId, query })) ?? ''),
3590
3812
  loadMemory: () => loadAmbientMemory(),
3591
3813
  saveMemory: (m) => saveAmbientMemory(m),
3592
3814
  backoff: async () => shouldBackoff(await readLedger()),
@@ -3595,17 +3817,7 @@ if (process.argv[2] === 'ambient-recall') {
3595
3817
  void appendLedgerEntry(entry);
3596
3818
  reported = reportAmbientLedgerEvent(instanceId, entry); // org dashboard mirror
3597
3819
  },
3598
- recall: async (query) => {
3599
- if (!instanceId)
3600
- return [];
3601
- const text = String((await handleTool('smart_recall', { instance_id: instanceId, query })) ?? '');
3602
- const miss = text.length < 80 || /No lessons found|no lessons|No matches found/i.test(text);
3603
- if (miss)
3604
- return [];
3605
- const summary = truncateToTokens(text, 500);
3606
- return [{ id: candidateIdFor(summary), summary, confidence: 0.9, score: 0.9 }];
3607
- },
3608
- });
3820
+ }));
3609
3821
  if (out)
3610
3822
  process.stdout.write(out);
3611
3823
  // Bounded flush so the dashboard mirror survives process.exit — never more
@@ -3764,6 +3976,8 @@ if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '')) {
3764
3976
  if (runningInTerminal) {
3765
3977
  // A human ran `npx @cachly-dev/mcp-server` directly with no credentials.
3766
3978
  // Show them how to set up, then exit cleanly.
3979
+ const bannerSetupUrl = cachlyUrl('/setup-ai', 'first-run');
3980
+ const bannerSetupLine = `║ 👉 ${bannerSetupUrl}`.padEnd(66) + '║\n';
3767
3981
  const banner = '\n' +
3768
3982
  '╔══════════════════════════════════════════════════════════════════╗\n' +
3769
3983
  '║ 🧠 cachly AI Brain — Setup required ║\n' +
@@ -3771,7 +3985,7 @@ if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '')) {
3771
3985
  '║ ║\n' +
3772
3986
  '║ CACHLY_JWT is not set. Get your free credentials at: ║\n' +
3773
3987
  '║ ║\n' +
3774
- '║ 👉 https://cachly.dev/setup-ai ║\n' +
3988
+ bannerSetupLine +
3775
3989
  '║ ║\n' +
3776
3990
  '║ Then run the one-command setup wizard: ║\n' +
3777
3991
  '║ ║\n' +
@@ -3799,11 +4013,11 @@ else {
3799
4013
  const minsLeft = Math.floor((expMs - Date.now()) / 60_000);
3800
4014
  if (minsLeft <= 0) {
3801
4015
  process.stderr.write(`\n⚠️ cachly: CACHLY_JWT expired ${Math.abs(minsLeft)} minute(s) ago.\n` +
3802
- ` All tool calls will fail. Get a fresh token at https://cachly.dev/setup-ai\n\n`);
4016
+ ` All tool calls will fail. Get a fresh token at ${cachlyUrl('/setup-ai', 'jwt-expiry')}\n\n`);
3803
4017
  }
3804
4018
  else if (minsLeft < 60) {
3805
4019
  process.stderr.write(`\n⚠️ cachly: CACHLY_JWT expires in ${minsLeft} minute(s).\n` +
3806
- ` Refresh it soon at https://cachly.dev/setup-ai to avoid interruptions.\n\n`);
4020
+ ` Refresh it soon at ${cachlyUrl('/setup-ai', 'jwt-expiry')} to avoid interruptions.\n\n`);
3807
4021
  }
3808
4022
  }
3809
4023
  }