@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.
- package/README.md +31 -15
- 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 +301 -75
- 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,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 = '
|
|
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
|
-
/**
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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 ·
|
|
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
|
-
|
|
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:
|
|
982
|
-
//
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
|
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 ??
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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 = '
|
|
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
|
|
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 =
|
|
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
|
|
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 = '
|
|
2591
|
+
const targetUrl = cachlyUrl('/', 'badge');
|
|
2417
2592
|
const markdown = `[](${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 ·
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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 = '
|
|
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(
|
|
3107
|
-
openInBrowser('
|
|
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(
|
|
3157
|
-
openInBrowser('
|
|
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
|
|
3277
|
-
gitLessonCount =
|
|
3278
|
-
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
|
+
}
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
4020
|
+
` Refresh it soon at ${cachlyUrl('/setup-ai', 'jwt-expiry')} to avoid interruptions.\n\n`);
|
|
3795
4021
|
}
|
|
3796
4022
|
}
|
|
3797
4023
|
}
|