@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.
- package/README.md +31 -15
- package/dist/src/ambient-deps.d.ts +26 -0
- package/dist/src/ambient-deps.js +55 -0
- package/dist/src/ambient-deps.js.map +1 -0
- package/dist/src/ambient-hooks.d.ts +11 -2
- package/dist/src/ambient-hooks.js +11 -2
- package/dist/src/ambient-hooks.js.map +1 -1
- package/dist/src/ambient-ledger.js +2 -2
- package/dist/src/ambient-ledger.js.map +1 -1
- package/dist/src/ambient-memory.js +2 -2
- package/dist/src/ambient-memory.js.map +1 -1
- package/dist/src/auth.js +2 -1
- package/dist/src/auth.js.map +1 -1
- package/dist/src/brain-export.d.ts +40 -0
- package/dist/src/brain-export.js +89 -0
- package/dist/src/brain-export.js.map +1 -0
- package/dist/src/cachly-url.d.ts +68 -0
- package/dist/src/cachly-url.js +21 -0
- package/dist/src/cachly-url.js.map +1 -0
- package/dist/src/credentials.d.ts +37 -0
- package/dist/src/credentials.js +85 -0
- package/dist/src/credentials.js.map +1 -0
- package/dist/src/doctor.js +2 -1
- package/dist/src/doctor.js.map +1 -1
- package/dist/src/first-recall-proof.d.ts +19 -0
- package/dist/src/first-recall-proof.js +41 -0
- package/dist/src/first-recall-proof.js.map +1 -0
- package/dist/src/handlers/advanced.js +13 -29
- package/dist/src/handlers/advanced.js.map +1 -1
- package/dist/src/handlers/brain.js +132 -41
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/cache.js +3 -2
- package/dist/src/handlers/cache.js.map +1 -1
- package/dist/src/handlers/fedbrain.js +15 -4
- package/dist/src/handlers/fedbrain.js.map +1 -1
- package/dist/src/handlers/share.js +4 -3
- package/dist/src/handlers/share.js.map +1 -1
- package/dist/src/handlers/team.js +2 -1
- package/dist/src/handlers/team.js.map +1 -1
- package/dist/src/index.d.ts +32 -2
- package/dist/src/index.js +308 -94
- package/dist/src/index.js.map +1 -1
- package/dist/src/kostprobe.d.ts +25 -0
- package/dist/src/kostprobe.js +125 -0
- package/dist/src/kostprobe.js.map +1 -0
- package/dist/src/telemetry-types.d.ts +1 -1
- package/dist/src/tools.d.ts +0 -99
- package/dist/src/tools.js +0 -110
- package/dist/src/tools.js.map +1 -1
- package/dist/src/toolspecs.d.ts +1 -1
- package/dist/src/toolspecs.js +1 -1
- package/dist/src/upgrade-nudge.d.ts +15 -0
- package/dist/src/upgrade-nudge.js +29 -0
- package/dist/src/upgrade-nudge.js.map +1 -0
- package/glama.json +1 -1
- package/package.json +2 -2
- package/scripts/postinstall.js +1 -1
- 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 =
|
|
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:
|
|
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 || '
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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 {
|
|
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 = '
|
|
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
|
-
/**
|
|
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
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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 ·
|
|
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
|
-
|
|
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:
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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
|
|
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 ??
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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 = '
|
|
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
|
|
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 =
|
|
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
|
|
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 = '
|
|
2591
|
+
const targetUrl = cachlyUrl('/', 'badge');
|
|
2416
2592
|
const markdown = `[](${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 ·
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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 = '
|
|
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(
|
|
3106
|
-
openInBrowser('
|
|
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(
|
|
3156
|
-
openInBrowser('
|
|
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
|
|
3276
|
-
gitLessonCount =
|
|
3277
|
-
console.log(` ✓ ${
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
3586
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
4020
|
+
` Refresh it soon at ${cachlyUrl('/setup-ai', 'jwt-expiry')} to avoid interruptions.\n\n`);
|
|
3807
4021
|
}
|
|
3808
4022
|
}
|
|
3809
4023
|
}
|