@cachly-dev/mcp-server 0.10.122 → 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 (55) hide show
  1. package/README.md +31 -15
  2. package/dist/src/ambient-hooks.d.ts +11 -2
  3. package/dist/src/ambient-hooks.js +11 -2
  4. package/dist/src/ambient-hooks.js.map +1 -1
  5. package/dist/src/ambient-ledger.js +2 -2
  6. package/dist/src/ambient-ledger.js.map +1 -1
  7. package/dist/src/ambient-memory.js +2 -2
  8. package/dist/src/ambient-memory.js.map +1 -1
  9. package/dist/src/auth.js +2 -1
  10. package/dist/src/auth.js.map +1 -1
  11. package/dist/src/brain-export.d.ts +40 -0
  12. package/dist/src/brain-export.js +89 -0
  13. package/dist/src/brain-export.js.map +1 -0
  14. package/dist/src/cachly-url.d.ts +68 -0
  15. package/dist/src/cachly-url.js +21 -0
  16. package/dist/src/cachly-url.js.map +1 -0
  17. package/dist/src/credentials.d.ts +37 -0
  18. package/dist/src/credentials.js +85 -0
  19. package/dist/src/credentials.js.map +1 -0
  20. package/dist/src/doctor.js +2 -1
  21. package/dist/src/doctor.js.map +1 -1
  22. package/dist/src/first-recall-proof.d.ts +19 -0
  23. package/dist/src/first-recall-proof.js +41 -0
  24. package/dist/src/first-recall-proof.js.map +1 -0
  25. package/dist/src/handlers/advanced.js +13 -29
  26. package/dist/src/handlers/advanced.js.map +1 -1
  27. package/dist/src/handlers/brain.js +132 -41
  28. package/dist/src/handlers/brain.js.map +1 -1
  29. package/dist/src/handlers/cache.js +3 -2
  30. package/dist/src/handlers/cache.js.map +1 -1
  31. package/dist/src/handlers/fedbrain.js +15 -4
  32. package/dist/src/handlers/fedbrain.js.map +1 -1
  33. package/dist/src/handlers/share.js +4 -3
  34. package/dist/src/handlers/share.js.map +1 -1
  35. package/dist/src/handlers/team.js +2 -1
  36. package/dist/src/handlers/team.js.map +1 -1
  37. package/dist/src/index.d.ts +32 -2
  38. package/dist/src/index.js +301 -75
  39. package/dist/src/index.js.map +1 -1
  40. package/dist/src/kostprobe.d.ts +25 -0
  41. package/dist/src/kostprobe.js +125 -0
  42. package/dist/src/kostprobe.js.map +1 -0
  43. package/dist/src/telemetry-types.d.ts +1 -1
  44. package/dist/src/tools.d.ts +0 -99
  45. package/dist/src/tools.js +0 -110
  46. package/dist/src/tools.js.map +1 -1
  47. package/dist/src/toolspecs.d.ts +1 -1
  48. package/dist/src/toolspecs.js +1 -1
  49. package/dist/src/upgrade-nudge.d.ts +15 -0
  50. package/dist/src/upgrade-nudge.js +29 -0
  51. package/dist/src/upgrade-nudge.js.map +1 -0
  52. package/glama.json +1 -1
  53. package/package.json +2 -2
  54. package/scripts/postinstall.js +1 -1
  55. 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,18 @@ 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
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';
762
769
  import { buildAmbientDeps } from './ambient-deps.js';
770
+ import { resolveApiKey, saveApiKey } from './credentials.js';
771
+ import { buildLessonsMarkdown, buildLessonsJsonl } from './brain-export.js';
763
772
  import { detectEditor as detectEditorImpl } from './editor.js';
764
773
  import { milestoneSent, markMilestoneSent } from './funnel-milestones.js';
774
+ import { kostprobeUebrig, kostprobeVerbrauchen, kostprobeHinweis, schrankeNachKostproben, } from './kostprobe.js';
765
775
  import { netBalance, shouldBackoff } from './ambient-recall.js';
766
776
  import { handleShareTool } from './handlers/share.js';
767
777
  import { handleVizTool } from './handlers/viz.js';
@@ -777,6 +787,24 @@ let _firstCallSuccessSent = false;
777
787
  function detectEditor() {
778
788
  return detectEditorImpl();
779
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
+ }
780
808
  /** Derive an anonymous, non-reversible fingerprint from the JWT sub claim. */
781
809
  function _jwtUserFingerprint(jwt) {
782
810
  try {
@@ -872,7 +900,7 @@ const PREMIUM_PITCH = {
872
900
  global_learn: "Team Brain — contribute to your organization's shared memory.",
873
901
  global_recall: 'Team Brain — recall across your entire organization.',
874
902
  };
875
- const UPGRADE_URL_FG = 'https://cachly.dev/billing';
903
+ const UPGRADE_URL_FG = cachlyUrl('/billing', 'upgrade');
876
904
  const _tierCache = new Map();
877
905
  async function getTierInfo(instanceId) {
878
906
  const cached = _tierCache.get(instanceId);
@@ -890,7 +918,21 @@ async function getTierInfo(instanceId) {
890
918
  return { tier: 'unknown', isFree: false };
891
919
  }
892
920
  }
893
- /** 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
+ */
894
936
  async function featureGate(name, instanceId) {
895
937
  if (!PREMIUM_TOOLS.has(name) || !instanceId)
896
938
  return null;
@@ -898,14 +940,14 @@ async function featureGate(name, instanceId) {
898
940
  if (!isFree)
899
941
  return null;
900
942
  const pitch = PREMIUM_PITCH[name] ?? 'A Premium intelligence feature.';
901
- return [
902
- `🔒 **\`${name}\` is a Premium feature**`,
903
- ``,
904
- `${pitch}`,
905
- ``,
906
- `Your Free Brain keeps full keyword + causal-graph recall, learning, and sessions — forever.`,
907
- `Unlock the deeper layer (causal tracing · predictive risk · Team Brain) → ${UPGRADE_URL_FG}`,
908
- ].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) };
909
951
  }
910
952
  async function handleTool(name, args) {
911
953
  // Guard: if no JWT, return actionable onboarding message instead of HTTP 401
@@ -955,7 +997,7 @@ async function handleTool(name, args) {
955
997
  'That’s it — your Brain activates automatically the moment you finish',
956
998
  'signing in. Just keep working; your next request arrives brain-powered.',
957
999
  '',
958
- '✨ Free forever · No credit card · 126 MCP tools · GDPR · EU servers',
1000
+ '✨ Free forever · No credit card · 122 MCP tools · GDPR · EU servers',
959
1001
  ].join('\n');
960
1002
  }
961
1003
  // Device flow unavailable (network issue) — fall back to manual setup
@@ -967,7 +1009,7 @@ async function handleTool(name, args) {
967
1009
  ' npx @cachly-dev/mcp-server@latest autopilot',
968
1010
  ' ```',
969
1011
  '',
970
- 'Or get your API key at: https://cachly.dev/setup-ai',
1012
+ `Or get your API key at: ${cachlyUrl('/setup-ai', 'ambient-signin')}`,
971
1013
  '',
972
1014
  '✨ Free tier includes: 1 Brain instance, persistent memory, semantic search.',
973
1015
  ].join('\n');
@@ -978,12 +1020,19 @@ async function handleTool(name, args) {
978
1020
  if (defaultId)
979
1021
  args = { ...args, instance_id: defaultId };
980
1022
  }
981
- // Feature-Gate: free tier reaching for a Premium tool gets the upgrade pitch
982
- // at the moment of need (the magic moment of basic recall stays untouched).
983
- const gateMsg = await featureGate(name, args.instance_id);
984
- if (gateMsg !== null) {
985
- sendFunnelEvent('premium_gate_hit', { tool: name, instance_id: args.instance_id ?? '' });
986
- 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 ?? '' });
987
1036
  }
988
1037
  // Delegate brain tools (learn, recall, session, etc.)
989
1038
  const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
@@ -1114,16 +1163,23 @@ async function handleTool(name, args) {
1114
1163
  const cacheResult = await handleCacheTool(name, args, getConnection, apiFetch);
1115
1164
  if (cacheResult !== null)
1116
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);
1117
1173
  // Delegate team/brain advanced tools
1118
1174
  const teamResult = await handleTeamTool(name, args, getConnection, apiFetch);
1119
1175
  if (teamResult !== null)
1120
- return teamResult;
1176
+ return mitHinweis(teamResult);
1121
1177
  const roadmapResult = await handleRoadmapTool(name, args, getConnection, apiFetch);
1122
1178
  if (roadmapResult !== null)
1123
1179
  return roadmapResult;
1124
1180
  const advancedResult = await handleAdvancedTool(name, args, getConnection, apiFetch);
1125
1181
  if (advancedResult !== null)
1126
- return advancedResult;
1182
+ return mitHinweis(advancedResult);
1127
1183
  const syndicateResult = await handleSyndicateTool(name, args, getConnection, apiFetch);
1128
1184
  if (syndicateResult !== null) {
1129
1185
  // brain_predict is in syndicate handler — track telemetry here.
@@ -1132,7 +1188,7 @@ async function handleTool(name, args) {
1132
1188
  const telemetryExtra = JWT ? { api_key: JWT, instance_id: instanceId } : { instance_id: instanceId };
1133
1189
  sendFunnelEvent('brain_predict', telemetryExtra);
1134
1190
  }
1135
- return syndicateResult;
1191
+ return mitHinweis(syndicateResult);
1136
1192
  }
1137
1193
  const shareResult = await handleShareTool(name, args, getConnection, apiFetch);
1138
1194
  if (shareResult !== null)
@@ -1181,7 +1237,7 @@ async function handleTool(name, args) {
1181
1237
  ` 💓 Health: ${healthStatus}`,
1182
1238
  ` 🔑 Auth: ❌ CACHLY_JWT not set`,
1183
1239
  ``,
1184
- `💡 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`,
1185
1241
  ].join('\n');
1186
1242
  }
1187
1243
  // Decode JWT claims (inspection only, no verification)
@@ -1213,7 +1269,7 @@ async function handleTool(name, args) {
1213
1269
  const listRes = await apiFetch('/api/v1/instances');
1214
1270
  const instances = listRes.data ?? [];
1215
1271
  if (instances.length === 0) {
1216
- 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')}`;
1217
1273
  }
1218
1274
  else {
1219
1275
  const lines = ['\n\n🧠 **Brain Instances:**'];
@@ -1271,9 +1327,18 @@ async function handleTool(name, args) {
1271
1327
  ].join('\n');
1272
1328
  }
1273
1329
  default:
1274
- return `⚠️ Unknown tool: ${name}`;
1330
+ throw new UnknownToolError(`Unknown tool: ${name}`);
1275
1331
  }
1276
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
+ }
1277
1342
  // Pushed to every MCP host at connect time (initialize response). This is the
1278
1343
  // "SessionStart-equivalent" for harnesses without a per-prompt hook — Cursor,
1279
1344
  // Windsurf, Cline, Copilot agent-mode all surface `instructions`. It's a one-time
@@ -1382,6 +1447,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1382
1447
  return { content: [{ type: 'text', text }] };
1383
1448
  }
1384
1449
  catch (err) {
1450
+ if (err instanceof UnknownToolError) {
1451
+ return { content: [{ type: 'text', text: err.message }], isError: true };
1452
+ }
1385
1453
  if (err instanceof McpError)
1386
1454
  throw err;
1387
1455
  const msg = err.message ?? String(err);
@@ -1436,14 +1504,25 @@ export const EDITOR_FILES = {
1436
1504
  export const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
1437
1505
  export const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
1438
1506
  const DEFAULT_API_URL = 'https://api.cachly.dev';
1439
- // Build the env block written into an editor's MCP config. Self-hosting is
1440
- // first-class: if the operator runs setup/init against their own backend
1441
- // (CACHLY_API_URL set to a non-default URL), that URL is baked into the config so
1442
- // the editor-launched server talks to the self-hosted instance — not api.cachly.dev.
1443
- // For the default cloud backend we OMIT CACHLY_API_URL to keep configs clean (the
1444
- // binary already defaults to it).
1445
- export function buildServerEnv(apiKey, instanceId) {
1446
- 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;
1447
1526
  if (API_URL && API_URL !== DEFAULT_API_URL)
1448
1527
  env.CACHLY_API_URL = API_URL;
1449
1528
  return env;
@@ -1483,9 +1562,19 @@ export function buildMcpConfig(apiKey, instanceId, editor) {
1483
1562
  },
1484
1563
  }, null, 2);
1485
1564
  }
1486
- // mergeMcpConfig reads an existing config file (if any), merges the cachly entry,
1487
- // and returns the updated JSON string — preserving all other MCP servers and settings.
1488
- 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 = {}) {
1489
1578
  const cachlyEntry = {
1490
1579
  command: 'npx',
1491
1580
  args: ['-y', '@cachly-dev/mcp-server@latest'],
@@ -1510,6 +1599,16 @@ export async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsO
1510
1599
  // Merge into experimental.modelContextProtocolServers array — replace cachly entry if present
1511
1600
  const exp = (existing['experimental'] ?? {});
1512
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);
1513
1612
  const filtered = servers.filter((s) => {
1514
1613
  const env = (s['env'] ?? {});
1515
1614
  return !env['CACHLY_JWT'] && !env['CACHLY_BRAIN_INSTANCE_ID'];
@@ -1521,12 +1620,20 @@ export async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsO
1521
1620
  if (editor === 'zed') {
1522
1621
  // Merge only context_servers.cachly — preserve all other Zed settings
1523
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);
1524
1627
  cs['cachly'] = { command: { path: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'], env: cachlyEntry.env }, settings: {} };
1525
1628
  existing['context_servers'] = cs;
1526
1629
  return JSON.stringify(existing, null, 2);
1527
1630
  }
1528
1631
  // Standard mcpServers format (Claude Code, Cursor, Windsurf, Copilot, Cline)
1529
1632
  const servers = (existing['mcpServers'] ?? {});
1633
+ const previousCachly = servers['cachly'];
1634
+ const legacyKey = previousCachly?.env?.['CACHLY_JWT'];
1635
+ if (legacyKey)
1636
+ saveApiKey(legacyKey, credOpts);
1530
1637
  servers['cachly'] = cachlyEntry;
1531
1638
  existing['mcpServers'] = servers;
1532
1639
  return JSON.stringify(existing, null, 2);
@@ -1659,7 +1766,7 @@ if (process.argv[2] === 'digest') {
1659
1766
  process.stdout.write('\n🧠 Fetching your weekly Brain digest...\n\n');
1660
1767
  try {
1661
1768
  // Fetch current stats
1662
- 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`, {
1663
1770
  headers: { Authorization: `Bearer ${apiKey}` },
1664
1771
  signal: AbortSignal.timeout(8000),
1665
1772
  });
@@ -1734,6 +1841,74 @@ if (process.argv[2] === 'digest') {
1734
1841
  }
1735
1842
  process.exit(0);
1736
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
+ }
1737
1912
  // ── CLI: cachly invite ────────────────────────────────────────────────────────
1738
1913
  // Usage: npx @cachly-dev/mcp-server@latest invite [email]
1739
1914
  // Invites a teammate to share your Brain. Fastest referral loop.
@@ -1753,7 +1928,7 @@ if (process.argv[2] === 'invite') {
1753
1928
  if (!res.ok)
1754
1929
  throw new Error(`HTTP ${res.status}`);
1755
1930
  const data = await res.json();
1756
- const referralUrl = data.url ?? `https://cachly.dev/r/${data.code}`;
1931
+ const referralUrl = data.url ?? cachlyUrl(`/r/${data.code}`, 'invite');
1757
1932
  const count = data.referral_count ?? 0;
1758
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}`;
1759
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`);
@@ -1891,7 +2066,7 @@ if (process.argv[2] === 'join') {
1891
2066
  }
1892
2067
  catch (e) {
1893
2068
  console.log(`\n ⚠️ Could not start sign-in: ${e.message}`);
1894
- 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`);
1895
2070
  process.exit(1);
1896
2071
  }
1897
2072
  }
@@ -2075,7 +2250,7 @@ if (process.argv[2] === 'bench') {
2075
2250
  console.log(' \x1b[1m→ Give your AI this recall quality on your own work (free, 1–5 min):\x1b[0m');
2076
2251
  console.log(' \x1b[32m$ npx @cachly-dev/mcp-server@latest autopilot\x1b[0m');
2077
2252
  console.log(' \x1b[90m Signs in, configures every editor, bootstraps from your git history.\x1b[0m');
2078
- 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`);
2079
2254
  console.log('');
2080
2255
  }
2081
2256
  catch (e) {
@@ -2233,7 +2408,7 @@ if (process.argv[2] === 'demo') {
2233
2408
  authors: String(authors.size),
2234
2409
  hours: String(hoursWasted),
2235
2410
  });
2236
- const previewURL = `https://cachly.dev/preview?${previewParams.toString()}`;
2411
+ const previewURL = cachlyUrl(`/preview?${previewParams.toString()}`, 'demo');
2237
2412
  console.log(' \x1b[1mMake this permanent (free, 1–5 minutes):\x1b[0m');
2238
2413
  console.log(' \x1b[32m$ npx @cachly-dev/mcp-server@latest autopilot\x1b[0m');
2239
2414
  console.log('');
@@ -2257,7 +2432,7 @@ if (process.argv[2] === 'share') {
2257
2432
  }
2258
2433
  process.stdout.write('\n🧠 Fetching your Brain stats...\n');
2259
2434
  try {
2260
- 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`, {
2261
2436
  headers: { Authorization: `Bearer ${apiKey}` },
2262
2437
  signal: AbortSignal.timeout(8000),
2263
2438
  });
@@ -2288,7 +2463,7 @@ if (process.argv[2] === 'share') {
2288
2463
  console.log('└─────────────────────────────────────────────────────────────┘');
2289
2464
  console.log('');
2290
2465
  // Tweet text
2291
- const shareUrl = 'https://cachly.dev?ref=share&utm_source=x&utm_medium=social&utm_campaign=cli-share';
2466
+ const shareUrl = cachlyUrl('/', 'share');
2292
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`;
2293
2468
  console.log(' \x1b[1m📋 Share on Twitter/X (copy this):\x1b[0m');
2294
2469
  console.log(' ─────────────────────────────────────');
@@ -2323,7 +2498,7 @@ if (process.argv[2] === 'publish') {
2323
2498
  process.stdout.write('\n🧠 Publishing Brain snapshot...\n');
2324
2499
  try {
2325
2500
  // Fetch stats for the card
2326
- 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`, {
2327
2502
  headers: { Authorization: `Bearer ${apiKey}` },
2328
2503
  signal: AbortSignal.timeout(8000),
2329
2504
  });
@@ -2344,7 +2519,7 @@ if (process.argv[2] === 'publish') {
2344
2519
  throw new Error(`HTTP ${shareRes.status}`);
2345
2520
  const share = await shareRes.json();
2346
2521
  const shareId = share.share_id ?? 'unavailable';
2347
- const shareUrl = `https://cachly.dev/brain/share/${shareId}`;
2522
+ const shareUrl = cachlyUrl(`/brain/share/${shareId}`, 'publish');
2348
2523
  console.log('');
2349
2524
  console.log('┌─────────────────────────────────────────────────────────────┐');
2350
2525
  console.log('│ 📤 Brain Published · powered by \x1b[35mcachly.dev\x1b[0m │');
@@ -2373,7 +2548,7 @@ if (process.argv[2] === 'publish') {
2373
2548
  process.exit(0);
2374
2549
  }
2375
2550
  // ── CLI: cachly tool-specs / openapi ───────────────────────────────────────────
2376
- // 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
2377
2552
  // single TOOLS source of truth. Lets OpenAI Assistants, the Anthropic Messages
2378
2553
  // API, LangChain, CrewAI and AutoGen wrap cachly without hand-written glue.
2379
2554
  // npx @cachly-dev/mcp-server tool-specs --format=openai > cachly.openai.json
@@ -2413,7 +2588,7 @@ if (process.argv[2] === 'badge') {
2413
2588
  process.exit(1);
2414
2589
  }
2415
2590
  const badgeUrl = `${API_URL}/api/v1/badge/${instanceId}`;
2416
- const targetUrl = 'https://cachly.dev';
2591
+ const targetUrl = cachlyUrl('/', 'badge');
2417
2592
  const markdown = `[![cachly Brain](${badgeUrl})](${targetUrl})`;
2418
2593
  const htmlBadge = `<a href="${targetUrl}"><img src="${badgeUrl}" alt="cachly Brain" /></a>`;
2419
2594
  console.log('');
@@ -2455,6 +2630,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
2455
2630
  console.log(' \x1b[32m npx @cachly-dev/mcp-server@latest demo\x1b[0m ← Try it first (no account)');
2456
2631
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest setup\x1b[0m ← Interactive setup (pick editors)');
2457
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');
2458
2634
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest digest\x1b[0m ← Weekly Brain summary');
2459
2635
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest share\x1b[0m ← Share your Brain stats');
2460
2636
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest badge\x1b[0m ← README badge for your Brain');
@@ -2464,7 +2640,7 @@ if (!process.argv[2] && process.stdout.isTTY) {
2464
2640
  console.log(' \x1b[36m npx @cachly-dev/mcp-server@latest upgrade\x1b[0m ← Check for updates');
2465
2641
  console.log('');
2466
2642
  console.log(' \x1b[90mWorks with: Claude Code · Cursor · Windsurf · GitHub Copilot · Cline · Zed\x1b[0m');
2467
- 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');
2468
2644
  console.log('');
2469
2645
  process.exit(0);
2470
2646
  }
@@ -2508,7 +2684,7 @@ if (process.argv[2] === 'init') {
2508
2684
  if (!instanceId || !apiKey) {
2509
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');
2510
2686
  console.error('First time? Run the zero-config wizard (signs you in, auto-provisions): npx @cachly-dev/mcp-server@latest autopilot\n');
2511
- console.error('Get your credentials from: https://cachly.dev/setup-ai\n');
2687
+ console.error(`Get your credentials from: ${cachlyUrl('/setup-ai', 'init')}\n`);
2512
2688
  process.exit(1);
2513
2689
  }
2514
2690
  const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
@@ -2517,6 +2693,7 @@ if (process.argv[2] === 'init') {
2517
2693
  const { existsSync: exInit } = await import('node:fs');
2518
2694
  const configExisted = exInit(configPath);
2519
2695
  const prevConfig = configExisted ? await readFile(configPath, 'utf-8').catch(() => '') : '';
2696
+ saveApiKey(apiKey);
2520
2697
  const merged = await mergeMcpConfig(configPath, apiKey, instanceId, editor, { readFile, existsSync: exInit });
2521
2698
  // Idempotent: only write when the content actually changes.
2522
2699
  if (merged !== prevConfig) {
@@ -2552,7 +2729,7 @@ if (process.argv[2] === 'init') {
2552
2729
  // Only for Claude Code (the hooks config lives in .claude/settings.json).
2553
2730
  if (editor === 'claude') {
2554
2731
  try {
2555
- const a = await installAmbientHooks(projectDir, instanceId, JWT || undefined);
2732
+ const a = await installAmbientHooks(projectDir, instanceId);
2556
2733
  const scriptVerb = a.scripts === 'written' ? '✅ Written' : a.scripts === 'upgraded' ? `✅ Upgraded (→ ${AMBIENT_HOOK_VERSION})` : '✓ Unchanged';
2557
2734
  console.log(`${scriptVerb}: .claude/hooks/ (Ambient Recall — session briefing, per-prompt recall, file briefing, auto-learn)`);
2558
2735
  if (a.settings === 'written')
@@ -2588,7 +2765,7 @@ if (process.argv[2] === 'status') {
2588
2765
  }
2589
2766
  try {
2590
2767
  const [statsRes, referralRes] = await Promise.all([
2591
- fetch(`${API_URL}/api/v1/instances/${instanceId}/brain/stats`, {
2768
+ fetch(`${API_URL}/api/v1/instances/${instanceId}/brain-stats`, {
2592
2769
  headers: { Authorization: `Bearer ${apiKey}` },
2593
2770
  signal: AbortSignal.timeout(8000),
2594
2771
  }),
@@ -2677,10 +2854,10 @@ if (process.argv[2] === 'health') {
2677
2854
  const expMs = payload.exp ? payload.exp * 1000 : null;
2678
2855
  const minsLeft = expMs ? Math.floor((expMs - Date.now()) / 60_000) : null;
2679
2856
  if (expMs && expMs < Date.now()) {
2680
- 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')}`);
2681
2858
  }
2682
2859
  else if (minsLeft !== null && minsLeft < 30) {
2683
- 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')}`);
2684
2861
  }
2685
2862
  else {
2686
2863
  ok(`JWT valid${minsLeft !== null ? ` (expires in ${Math.floor(minsLeft / 60)}h ${minsLeft % 60}m)` : ''}`);
@@ -2884,6 +3061,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
2884
3061
  const { resolve, dirname } = await import('node:path');
2885
3062
  const { createInterface } = await import('node:readline');
2886
3063
  const setupStartMs = Date.now();
3064
+ const cliSource = _isAutopilotCli ? 'autopilot' : 'setup';
2887
3065
  // Self-hosting: `setup --api-url https://cachly.mycorp.internal` points the whole
2888
3066
  // wizard (auth, provisioning, config writes) at a private backend.
2889
3067
  const _apiUrlIdx = process.argv.indexOf('--api-url');
@@ -3032,7 +3210,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3032
3210
  else {
3033
3211
  // Device flow unavailable — web fallback with automatic browser open
3034
3212
  sendFunnelEvent('device_flow_failed', { reason: 'device_flow_unavailable' });
3035
- const signupUrl = 'https://cachly.dev/setup-ai';
3213
+ const signupUrl = cachlyUrl('/setup-ai', cliSource);
3036
3214
  console.log(' ⚠️ Could not start automatic sign-in. Opening browser for web sign-in...\n');
3037
3215
  console.log(` URL: \x1b[36m${signupUrl}\x1b[0m`);
3038
3216
  console.log(' 1. Sign up / log in at the URL above');
@@ -3103,8 +3281,8 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3103
3281
  const msg = e.message;
3104
3282
  console.error(`\n\nFailed to fetch instances: ${msg}`);
3105
3283
  if (msg.includes('401')) {
3106
- console.error('Token rejected. Get a valid token at https://cachly.dev/setup-ai\n');
3107
- 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));
3108
3286
  }
3109
3287
  rl.close();
3110
3288
  process.exit(1);
@@ -3153,8 +3331,8 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3153
3331
  console.log(` failed: ${e.message}\n`);
3154
3332
  }
3155
3333
  if (instances.length === 0) {
3156
- console.error('\nCould not create an instance automatically. Opening https://cachly.dev/instances …\n');
3157
- openInBrowser('https://cachly.dev/instances');
3334
+ console.error(`\nCould not create an instance automatically. Opening ${cachlyUrl('/instances', cliSource)} …\n`);
3335
+ openInBrowser(cachlyUrl('/instances', cliSource));
3158
3336
  rl.close();
3159
3337
  process.exit(1);
3160
3338
  }
@@ -3208,6 +3386,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3208
3386
  // always claiming "Brain is ready" even when a config never got written.
3209
3387
  const configWriteFailures = [];
3210
3388
  let configWriteSuccesses = 0;
3389
+ saveApiKey(token);
3211
3390
  for (const editor of editorsToSetup) {
3212
3391
  const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
3213
3392
  const configPath = resolve(cwd, configFile);
@@ -3243,7 +3422,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3243
3422
  globalConfig.mcpServers['cachly'] = {
3244
3423
  command: 'npx',
3245
3424
  args: ['-y', '@cachly-dev/mcp-server@latest'],
3246
- env: buildServerEnv(token, instance.id),
3425
+ env: buildServerEnv(token, instance.id, { includeApiKey: true }),
3247
3426
  };
3248
3427
  await writeFile(globalClaudePath, JSON.stringify(globalConfig, null, 2), 'utf-8');
3249
3428
  console.log(`✅ Written: ~/.claude/mcp.json (global — works in every project)`);
@@ -3273,9 +3452,14 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3273
3452
  repo_path: cwd,
3274
3453
  limit: 100,
3275
3454
  });
3276
- const match = gitResult.match(/(\d+) lesson/);
3277
- gitLessonCount = match ? parseInt(match[1], 10) : 0;
3278
- 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
+ }
3279
3463
  }
3280
3464
  catch (e) {
3281
3465
  console.log(` (skipped: ${e.message.slice(0, 80)})`);
@@ -3292,6 +3476,21 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3292
3476
  try {
3293
3477
  await handleTool('brain_seed_starter', { instance_id: instance.id });
3294
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 */ }
3295
3494
  }
3296
3495
  catch (e) {
3297
3496
  console.log(` (skipped: ${e.message.slice(0, 60)})`);
@@ -3301,7 +3500,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3301
3500
  // Fetch brain health from the API to show the user what their agent will see.
3302
3501
  process.stdout.write('\n⏳ Fetching your Brain health preview...');
3303
3502
  try {
3304
- 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`, {
3305
3504
  headers: { Authorization: `Bearer ${token}` },
3306
3505
  signal: AbortSignal.timeout(6000),
3307
3506
  });
@@ -3371,7 +3570,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3371
3570
  console.log(`\n🎉 Your AI just got a permanent brain.`);
3372
3571
  console.log(` Restart your editor — from this session on, it arrives pre-briefed.\n`);
3373
3572
  console.log(` No more re-explaining your stack. No repeated mistakes.\n`);
3374
- console.log(` Dashboard: https://cachly.dev/instances/${instance.id}`);
3573
+ console.log(` Dashboard: ${cachlyUrl(`/instances/${instance.id}`, cliSource)}`);
3375
3574
  console.log(`\n 📛 Add a live badge to your README:`);
3376
3575
  console.log(` npx @cachly-dev/mcp-server@latest badge\n`);
3377
3576
  // ── Step 6c: Governance bootstrap — assign the first admin role ─────────────
@@ -3426,6 +3625,24 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3426
3625
  console.log(`\n✅ CLS git hook appended to existing post-commit.`);
3427
3626
  }
3428
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
+ }
3429
3646
  // Setup reached the end — close the funnel. Report config-write outcome so the
3430
3647
  // weekly funnel can spot permission/path problems blocking activation.
3431
3648
  JWT = token;
@@ -3453,7 +3670,7 @@ if (process.argv[2] === 'autosetup' || process.argv[2] === 'setup' || _isAutopil
3453
3670
  console.log(` • ${f.file} — ${f.reason}`);
3454
3671
  console.log('\n Fix the permissions above and re-run, or configure manually:');
3455
3672
  console.log(` CACHLY_JWT=${token.slice(0, 12)}… CACHLY_BRAIN_INSTANCE_ID=${instance.id}`);
3456
- console.log(` Docs: https://cachly.dev/docs/mcp\n`);
3673
+ console.log(` Docs: ${cachlyUrl('/docs/mcp', cliSource)}\n`);
3457
3674
  rl.close();
3458
3675
  await new Promise(r => setTimeout(r, 300));
3459
3676
  process.exit(1);
@@ -3569,8 +3786,15 @@ if (process.argv[2] === 'ambient-recall') {
3569
3786
  for await (const chunk of process.stdin)
3570
3787
  chunks.push(chunk);
3571
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).
3572
3792
  if (!JWT)
3573
- 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
+ }
3574
3798
  const instanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? _defaultInstanceId;
3575
3799
  // Stop event → auto-learn, never inject (roadmap §6.1 PostToolUse/Stop row).
3576
3800
  const stopPayload = parseHookPayload(raw);
@@ -3752,6 +3976,8 @@ if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '')) {
3752
3976
  if (runningInTerminal) {
3753
3977
  // A human ran `npx @cachly-dev/mcp-server` directly with no credentials.
3754
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';
3755
3981
  const banner = '\n' +
3756
3982
  '╔══════════════════════════════════════════════════════════════════╗\n' +
3757
3983
  '║ 🧠 cachly AI Brain — Setup required ║\n' +
@@ -3759,7 +3985,7 @@ if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '')) {
3759
3985
  '║ ║\n' +
3760
3986
  '║ CACHLY_JWT is not set. Get your free credentials at: ║\n' +
3761
3987
  '║ ║\n' +
3762
- '║ 👉 https://cachly.dev/setup-ai ║\n' +
3988
+ bannerSetupLine +
3763
3989
  '║ ║\n' +
3764
3990
  '║ Then run the one-command setup wizard: ║\n' +
3765
3991
  '║ ║\n' +
@@ -3787,11 +4013,11 @@ else {
3787
4013
  const minsLeft = Math.floor((expMs - Date.now()) / 60_000);
3788
4014
  if (minsLeft <= 0) {
3789
4015
  process.stderr.write(`\n⚠️ cachly: CACHLY_JWT expired ${Math.abs(minsLeft)} minute(s) ago.\n` +
3790
- ` 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`);
3791
4017
  }
3792
4018
  else if (minsLeft < 60) {
3793
4019
  process.stderr.write(`\n⚠️ cachly: CACHLY_JWT expires in ${minsLeft} minute(s).\n` +
3794
- ` 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`);
3795
4021
  }
3796
4022
  }
3797
4023
  }