@cachly-dev/mcp-server 0.10.86 → 0.10.88
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/dist/src/handlers/brain.js +18 -1
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +136 -40
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
package/dist/src/index.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP } from './search.js';
|
|
3
3
|
export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
|
|
4
|
+
export declare const EDITOR_FILES: Record<string, string>;
|
|
5
|
+
export declare const CLAUDE_MD_MARKER_START = "<!-- cachly-brain-start -->";
|
|
6
|
+
export declare const CLAUDE_MD_MARKER_END = "<!-- cachly-brain-end -->";
|
|
7
|
+
export declare function buildServerEnv(apiKey: string, instanceId: string): Record<string, string>;
|
|
8
|
+
export declare function buildMcpConfig(apiKey: string, instanceId: string, editor: string): string;
|
|
9
|
+
export declare function mergeMcpConfig(configPath: string, apiKey: string, instanceId: string, editor: string, fsOps: {
|
|
10
|
+
readFile: (p: string, enc: BufferEncoding) => Promise<string>;
|
|
11
|
+
existsSync: (p: string) => boolean;
|
|
12
|
+
}): Promise<string>;
|
|
13
|
+
export declare function buildClaudeMdBlock(instanceId: string): string;
|
package/dist/src/index.js
CHANGED
|
@@ -65,7 +65,7 @@ import { Redis } from 'ioredis';
|
|
|
65
65
|
let API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
66
66
|
let JWT = process.env.CACHLY_JWT ?? '';
|
|
67
67
|
const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
68
|
-
const CURRENT_VERSION = '0.10.
|
|
68
|
+
const CURRENT_VERSION = '0.10.88';
|
|
69
69
|
// Max time to wait for a freshly-provisioned instance to become "running" before
|
|
70
70
|
// giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
|
|
71
71
|
// old hard-coded 25s caused spurious "instance_not_reachable" on first run.
|
|
@@ -109,7 +109,15 @@ async function autoProvisionInstance() {
|
|
|
109
109
|
}).catch(() => { });
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
catch {
|
|
112
|
+
catch {
|
|
113
|
+
// Network unreachable — fire telemetry so silent provision failures are visible in the funnel.
|
|
114
|
+
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: { 'Content-Type': 'application/json' },
|
|
117
|
+
body: JSON.stringify({ event: 'auto_provision_failed', status: 0, reason: 'network_error', version: CURRENT_VERSION }),
|
|
118
|
+
signal: AbortSignal.timeout(3000),
|
|
119
|
+
}).catch(() => { });
|
|
120
|
+
}
|
|
113
121
|
return '';
|
|
114
122
|
}
|
|
115
123
|
async function resolveDefaultInstanceId() {
|
|
@@ -174,20 +182,24 @@ let _authHealAttemptedAt = 0;
|
|
|
174
182
|
const AUTH_HEAL_COOLDOWN_MS = 60_000;
|
|
175
183
|
// Surfaced in session_start / get_api_status so the user is never left guessing.
|
|
176
184
|
let _authDegradedNotice = '';
|
|
177
|
-
/** Persist a (possibly refreshed) API key to ~/.claude/mcp.json so restarts keep it.
|
|
185
|
+
/** Persist a (possibly refreshed) API key to ~/.claude/mcp.json so restarts keep it.
|
|
186
|
+
* Also updates CACHLY_JWT in Cursor and Windsurf configs when they already have a cachly entry,
|
|
187
|
+
* so users who auth via one editor don't lose their session in the others.
|
|
188
|
+
*/
|
|
178
189
|
async function persistApiKeyToConfig(apiKey) {
|
|
179
190
|
try {
|
|
180
191
|
const { writeFile, mkdir, readFile } = await import('node:fs/promises');
|
|
181
192
|
const { existsSync } = await import('node:fs');
|
|
182
193
|
const { resolve, dirname } = await import('node:path');
|
|
183
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
194
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? process.env.APPDATA ?? '';
|
|
184
195
|
if (!home)
|
|
185
196
|
return;
|
|
186
|
-
|
|
197
|
+
// Primary config: create or update ~/.claude/mcp.json
|
|
198
|
+
const claudePath = resolve(home, '.claude', 'mcp.json');
|
|
187
199
|
let cfg = {};
|
|
188
|
-
if (existsSync(
|
|
200
|
+
if (existsSync(claudePath)) {
|
|
189
201
|
try {
|
|
190
|
-
cfg = JSON.parse(await readFile(
|
|
202
|
+
cfg = JSON.parse(await readFile(claudePath, 'utf-8'));
|
|
191
203
|
}
|
|
192
204
|
catch { /* corrupt — start fresh */ }
|
|
193
205
|
}
|
|
@@ -203,8 +215,28 @@ async function persistApiKeyToConfig(apiKey) {
|
|
|
203
215
|
env: { CACHLY_JWT: apiKey },
|
|
204
216
|
};
|
|
205
217
|
}
|
|
206
|
-
await mkdir(dirname(
|
|
207
|
-
await writeFile(
|
|
218
|
+
await mkdir(dirname(claudePath), { recursive: true });
|
|
219
|
+
await writeFile(claudePath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
220
|
+
// Secondary configs: update CACHLY_JWT wherever cachly is already configured.
|
|
221
|
+
// Never create from scratch — only update existing entries (the user set these up).
|
|
222
|
+
const secondaryPaths = [
|
|
223
|
+
resolve(home, '.cursor', 'mcp.json'),
|
|
224
|
+
resolve(home, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
225
|
+
];
|
|
226
|
+
await Promise.allSettled(secondaryPaths.map(async (p) => {
|
|
227
|
+
if (!existsSync(p))
|
|
228
|
+
return;
|
|
229
|
+
try {
|
|
230
|
+
const raw = await readFile(p, 'utf-8');
|
|
231
|
+
const sc = JSON.parse(raw);
|
|
232
|
+
const entry = sc?.mcpServers?.['cachly'];
|
|
233
|
+
if (entry?.env) {
|
|
234
|
+
entry.env['CACHLY_JWT'] = apiKey;
|
|
235
|
+
await writeFile(p, JSON.stringify(sc, null, 2), 'utf-8');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch { /* corrupt or unwriteable — skip */ }
|
|
239
|
+
}));
|
|
208
240
|
}
|
|
209
241
|
catch { /* non-critical — never break a tool call on a filesystem error */ }
|
|
210
242
|
}
|
|
@@ -220,24 +252,27 @@ async function persistInstanceIdToConfig(instanceId) {
|
|
|
220
252
|
const { writeFile, readFile } = await import('node:fs/promises');
|
|
221
253
|
const { existsSync } = await import('node:fs');
|
|
222
254
|
const { resolve } = await import('node:path');
|
|
223
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
255
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? process.env.APPDATA ?? '';
|
|
224
256
|
if (!home)
|
|
225
257
|
return;
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
258
|
+
const configPaths = [
|
|
259
|
+
resolve(home, '.claude', 'mcp.json'),
|
|
260
|
+
resolve(home, '.cursor', 'mcp.json'),
|
|
261
|
+
resolve(home, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
262
|
+
];
|
|
263
|
+
await Promise.allSettled(configPaths.map(async (p) => {
|
|
264
|
+
if (!existsSync(p))
|
|
265
|
+
return;
|
|
266
|
+
try {
|
|
267
|
+
const cfg = JSON.parse(await readFile(p, 'utf-8'));
|
|
268
|
+
const entry = cfg?.mcpServers?.['cachly'];
|
|
269
|
+
if (entry?.env) {
|
|
270
|
+
entry.env['CACHLY_BRAIN_INSTANCE_ID'] = instanceId;
|
|
271
|
+
await writeFile(p, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch { /* corrupt or unwriteable — skip */ }
|
|
275
|
+
}));
|
|
241
276
|
}
|
|
242
277
|
catch { /* non-critical */ }
|
|
243
278
|
}
|
|
@@ -708,6 +743,7 @@ async function handleTool(name, args) {
|
|
|
708
743
|
return handleTool(name, args);
|
|
709
744
|
}
|
|
710
745
|
if (result === 'expired') {
|
|
746
|
+
sendFunnelEvent('device_flow_failed', { reason: 'timeout' });
|
|
711
747
|
_deviceFlow = null;
|
|
712
748
|
return '⌛ **Authentication timed out.** Please call any tool again to restart the sign-in flow.';
|
|
713
749
|
}
|
|
@@ -828,12 +864,26 @@ async function handleTool(name, args) {
|
|
|
828
864
|
// Append bootstrap summary to the session_start briefing.
|
|
829
865
|
// brainResult is always a string here (handleBrainTool returns string for session_start).
|
|
830
866
|
const bootstrapText = String(gitBootstrap);
|
|
831
|
-
|
|
832
|
-
|
|
867
|
+
// Show a concrete count so the user knows what was seeded (the WOW moment).
|
|
868
|
+
const totalSeeded = _lastBrainFromGitCounts?.total ?? 0;
|
|
869
|
+
const seedBanner = totalSeeded > 0
|
|
870
|
+
? `\n✅ **Brain bootstrapped from git — ${totalSeeded} lesson${totalSeeded === 1 ? '' : 's'} loaded.** Your AI already knows your history.`
|
|
871
|
+
: '';
|
|
872
|
+
if (bootstrapText || starterText || seedBanner) {
|
|
873
|
+
return String(brainResult) + seedBanner + '\n---\n' + bootstrapText + starterText;
|
|
833
874
|
}
|
|
834
875
|
}
|
|
835
876
|
}
|
|
836
|
-
catch {
|
|
877
|
+
catch (gitErr) {
|
|
878
|
+
// Bootstrap failed — fire telemetry so we can investigate and tell the user
|
|
879
|
+
// what happened (don't leave them wondering why the brain is empty).
|
|
880
|
+
sendFunnelEvent('brain_from_git_failed', { ...telemetryExtra,
|
|
881
|
+
reason: gitErr instanceof Error ? gitErr.message.slice(0, 120) : 'unknown' });
|
|
882
|
+
return String(brainResult) +
|
|
883
|
+
'\n\n> ⚠️ **Auto-bootstrap from git history failed** — your Brain is live but starts empty.\n' +
|
|
884
|
+
'> Run `brain_from_git(instance_id="' + instanceId + '", repo_path="' + String(args.workspace_path) + '")` manually to seed it.\n' +
|
|
885
|
+
'> Or try `brain_seed_starter` to load 16 universal engineering lessons instantly.';
|
|
886
|
+
}
|
|
837
887
|
}
|
|
838
888
|
}
|
|
839
889
|
else if (name === 'session_end') {
|
|
@@ -1163,7 +1213,7 @@ process.on('unhandledRejection', (reason) => {
|
|
|
1163
1213
|
process.stderr.write(`[cachly-mcp] unhandledRejection: ${msg}\n`);
|
|
1164
1214
|
});
|
|
1165
1215
|
// ── CLI helpers ───────────────────────────────────────────────────────────────
|
|
1166
|
-
const EDITOR_FILES = {
|
|
1216
|
+
export const EDITOR_FILES = {
|
|
1167
1217
|
claude: '.mcp.json',
|
|
1168
1218
|
cursor: '.cursor/mcp.json',
|
|
1169
1219
|
windsurf: '.windsurf/mcp.json',
|
|
@@ -1172,8 +1222,8 @@ const EDITOR_FILES = {
|
|
|
1172
1222
|
cline: '.vscode/mcp.json', // Cline respects VS Code project MCP config
|
|
1173
1223
|
zed: '.zed/settings.json', // Zed project-level context_servers
|
|
1174
1224
|
};
|
|
1175
|
-
const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
|
|
1176
|
-
const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
|
|
1225
|
+
export const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
|
|
1226
|
+
export const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
|
|
1177
1227
|
const DEFAULT_API_URL = 'https://api.cachly.dev';
|
|
1178
1228
|
// Build the env block written into an editor's MCP config. Self-hosting is
|
|
1179
1229
|
// first-class: if the operator runs setup/init against their own backend
|
|
@@ -1181,13 +1231,13 @@ const DEFAULT_API_URL = 'https://api.cachly.dev';
|
|
|
1181
1231
|
// the editor-launched server talks to the self-hosted instance — not api.cachly.dev.
|
|
1182
1232
|
// For the default cloud backend we OMIT CACHLY_API_URL to keep configs clean (the
|
|
1183
1233
|
// binary already defaults to it).
|
|
1184
|
-
function buildServerEnv(apiKey, instanceId) {
|
|
1234
|
+
export function buildServerEnv(apiKey, instanceId) {
|
|
1185
1235
|
const env = { CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId };
|
|
1186
1236
|
if (API_URL && API_URL !== DEFAULT_API_URL)
|
|
1187
1237
|
env.CACHLY_API_URL = API_URL;
|
|
1188
1238
|
return env;
|
|
1189
1239
|
}
|
|
1190
|
-
function buildMcpConfig(apiKey, instanceId, editor) {
|
|
1240
|
+
export function buildMcpConfig(apiKey, instanceId, editor) {
|
|
1191
1241
|
if (editor === 'continue') {
|
|
1192
1242
|
return JSON.stringify({
|
|
1193
1243
|
experimental: {
|
|
@@ -1224,7 +1274,7 @@ function buildMcpConfig(apiKey, instanceId, editor) {
|
|
|
1224
1274
|
}
|
|
1225
1275
|
// mergeMcpConfig reads an existing config file (if any), merges the cachly entry,
|
|
1226
1276
|
// and returns the updated JSON string — preserving all other MCP servers and settings.
|
|
1227
|
-
async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsOps) {
|
|
1277
|
+
export async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsOps) {
|
|
1228
1278
|
const cachlyEntry = {
|
|
1229
1279
|
command: 'npx',
|
|
1230
1280
|
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
@@ -1270,7 +1320,7 @@ async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsOps) {
|
|
|
1270
1320
|
existing['mcpServers'] = servers;
|
|
1271
1321
|
return JSON.stringify(existing, null, 2);
|
|
1272
1322
|
}
|
|
1273
|
-
function buildClaudeMdBlock(instanceId) {
|
|
1323
|
+
export function buildClaudeMdBlock(instanceId) {
|
|
1274
1324
|
return `${CLAUDE_MD_MARKER_START}
|
|
1275
1325
|
## Cachly AI Brain — Always Active
|
|
1276
1326
|
|
|
@@ -2849,6 +2899,10 @@ if (process.argv[2] === 'setup' || _isAutopilotCli) {
|
|
|
2849
2899
|
// Uses mergeMcpConfig to preserve all existing MCP servers — only adds/updates
|
|
2850
2900
|
// the cachly entry without touching filesystem, github, or other servers.
|
|
2851
2901
|
const { existsSync: exSetup } = await import('node:fs');
|
|
2902
|
+
// Track config-write failures so the final summary tells the truth instead of
|
|
2903
|
+
// always claiming "Brain is ready" even when a config never got written.
|
|
2904
|
+
const configWriteFailures = [];
|
|
2905
|
+
let configWriteSuccesses = 0;
|
|
2852
2906
|
for (const editor of editorsToSetup) {
|
|
2853
2907
|
const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
|
|
2854
2908
|
const configPath = resolve(cwd, configFile);
|
|
@@ -2858,10 +2912,13 @@ if (process.argv[2] === 'setup' || _isAutopilotCli) {
|
|
|
2858
2912
|
const merged = await mergeMcpConfig(configPath, token, instance.id, editor, { readFile, existsSync: exSetup });
|
|
2859
2913
|
await writeFile(configPath, merged, 'utf-8');
|
|
2860
2914
|
console.log(`✅ ${wasExisting ? 'Updated' : 'Written'}: ${configFile}`);
|
|
2915
|
+
configWriteSuccesses++;
|
|
2861
2916
|
}
|
|
2862
2917
|
catch (writeErr) {
|
|
2863
|
-
|
|
2918
|
+
const reason = writeErr.message;
|
|
2919
|
+
console.log(`⚠️ Could not write ${configFile}: ${reason}`);
|
|
2864
2920
|
console.log(` Fix permissions or run with sudo, then re-run setup.`);
|
|
2921
|
+
configWriteFailures.push({ file: configFile, reason });
|
|
2865
2922
|
}
|
|
2866
2923
|
}
|
|
2867
2924
|
// ── Step 4b: Write global Claude Code config (~/.claude/mcp.json) ─────────
|
|
@@ -2885,9 +2942,12 @@ if (process.argv[2] === 'setup' || _isAutopilotCli) {
|
|
|
2885
2942
|
};
|
|
2886
2943
|
await writeFile(globalClaudePath, JSON.stringify(globalConfig, null, 2), 'utf-8');
|
|
2887
2944
|
console.log(`✅ Written: ~/.claude/mcp.json (global — works in every project)`);
|
|
2945
|
+
configWriteSuccesses++;
|
|
2888
2946
|
}
|
|
2889
2947
|
catch (e) {
|
|
2890
|
-
|
|
2948
|
+
const reason = e.message;
|
|
2949
|
+
console.log(`⚠️ Could not write ~/.claude/mcp.json: ${reason}`);
|
|
2950
|
+
configWriteFailures.push({ file: '~/.claude/mcp.json', reason });
|
|
2891
2951
|
}
|
|
2892
2952
|
// ── Step 5: CLAUDE.md (always — idempotent) ───────────────────────────────
|
|
2893
2953
|
const mdResult = await writeClaudeMd(cwd, instance.id);
|
|
@@ -3085,15 +3145,51 @@ if (process.argv[2] === 'setup' || _isAutopilotCli) {
|
|
|
3085
3145
|
}
|
|
3086
3146
|
}
|
|
3087
3147
|
catch { /* non-critical */ }
|
|
3088
|
-
// Setup reached the end
|
|
3148
|
+
// Setup reached the end — close the funnel. Report config-write outcome so the
|
|
3149
|
+
// weekly funnel can spot permission/path problems blocking activation.
|
|
3089
3150
|
JWT = token;
|
|
3090
3151
|
const setupElapsedS = Math.round((Date.now() - setupStartMs) / 1000);
|
|
3091
|
-
|
|
3152
|
+
if (configWriteFailures.length > 0) {
|
|
3153
|
+
sendFunnelEvent('setup_config_write_failed', {
|
|
3154
|
+
instance_id: instance.id,
|
|
3155
|
+
failed: configWriteFailures.length,
|
|
3156
|
+
succeeded: configWriteSuccesses,
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
sendFunnelEvent('setup_completed', {
|
|
3160
|
+
instance_id: instance.id,
|
|
3161
|
+
elapsed_s: setupElapsedS,
|
|
3162
|
+
config_failures: configWriteFailures.length,
|
|
3163
|
+
});
|
|
3164
|
+
if (configWriteFailures.length > 0 && configWriteSuccesses === 0) {
|
|
3165
|
+
// Nothing got written — be honest, this is NOT "ready".
|
|
3166
|
+
console.log('\n╔══════════════════════════════════════════════════════╗');
|
|
3167
|
+
console.log('║ ⚠️ Setup incomplete — no editor config was written ║');
|
|
3168
|
+
console.log('╚══════════════════════════════════════════════════════╝');
|
|
3169
|
+
console.log('\n Your Brain instance exists, but no editor knows about it yet.');
|
|
3170
|
+
console.log(' Failed writes:');
|
|
3171
|
+
for (const f of configWriteFailures)
|
|
3172
|
+
console.log(` • ${f.file} — ${f.reason}`);
|
|
3173
|
+
console.log('\n Fix the permissions above and re-run, or configure manually:');
|
|
3174
|
+
console.log(` CACHLY_JWT=${token.slice(0, 12)}… CACHLY_BRAIN_INSTANCE_ID=${instance.id}`);
|
|
3175
|
+
console.log(` Docs: https://cachly.dev/docs/mcp\n`);
|
|
3176
|
+
rl.close();
|
|
3177
|
+
await new Promise(r => setTimeout(r, 300));
|
|
3178
|
+
process.exit(1);
|
|
3179
|
+
}
|
|
3092
3180
|
console.log('\n╔══════════════════════════════════════════════════════╗');
|
|
3093
3181
|
console.log('║ 🚀 Brain is ready. ║');
|
|
3094
3182
|
console.log(`║ ⏱️ Setup completed in ${String(setupElapsedS + 's').padEnd(5)} — restart your editor ║`);
|
|
3095
3183
|
console.log('║ and your AI arrives pre-briefed every session. ║');
|
|
3096
|
-
console.log('
|
|
3184
|
+
console.log('╚══════════════════════════════════════════════════════╝');
|
|
3185
|
+
if (configWriteFailures.length > 0) {
|
|
3186
|
+
// Partial success — some editors configured, some not. Tell the truth.
|
|
3187
|
+
console.log('\n ⚠️ Some editor configs could not be written (others succeeded):');
|
|
3188
|
+
for (const f of configWriteFailures)
|
|
3189
|
+
console.log(` • ${f.file} — ${f.reason}`);
|
|
3190
|
+
console.log(' Fix the permissions above and re-run setup to configure them too.');
|
|
3191
|
+
}
|
|
3192
|
+
console.log('');
|
|
3097
3193
|
rl.close();
|
|
3098
3194
|
// Give the fire-and-forget telemetry a moment to flush before exit.
|
|
3099
3195
|
await new Promise(r => setTimeout(r, 300));
|