@cachly-dev/mcp-server 0.10.85 → 0.10.87
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 +19 -1
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/index.js +187 -122
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
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.87';
|
|
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.
|
|
@@ -77,35 +77,100 @@ const PROVISION_TIMEOUT_MS = Number(process.env.CACHLY_PROVISION_TIMEOUT_MS ?? 9
|
|
|
77
77
|
let _defaultInstanceId = process.env.CACHLY_BRAIN_INSTANCE_ID ?? '';
|
|
78
78
|
// Timestamp of last failed fetch — retries after 30 s (not permanently blocked).
|
|
79
79
|
let _defaultInstanceLastAttempt = 0;
|
|
80
|
+
// In-flight resolution guard: a burst of parallel tool calls on startup must not
|
|
81
|
+
// each list-and-provision (which would race and create duplicate instances).
|
|
82
|
+
let _resolveInFlight = null;
|
|
83
|
+
/**
|
|
84
|
+
* Auto-provision a Brain instance via the idempotent find-or-create endpoint.
|
|
85
|
+
* Returns the instance id, or '' on failure. Fires auto_provision_failed telemetry
|
|
86
|
+
* on a non-2xx response so the activation funnel can spot free-tier breakage.
|
|
87
|
+
*/
|
|
88
|
+
async function autoProvisionInstance() {
|
|
89
|
+
if (!JWT)
|
|
90
|
+
return '';
|
|
91
|
+
try {
|
|
92
|
+
const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { Authorization: `Bearer ${JWT}`, 'Content-Type': 'application/json' },
|
|
95
|
+
signal: AbortSignal.timeout(12000),
|
|
96
|
+
});
|
|
97
|
+
if (autoRes.ok) {
|
|
98
|
+
const body = await autoRes.json();
|
|
99
|
+
const id = body.instance?.id ?? body.instance_id;
|
|
100
|
+
if (id)
|
|
101
|
+
return id;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
|
|
107
|
+
body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
|
|
108
|
+
signal: AbortSignal.timeout(3000),
|
|
109
|
+
}).catch(() => { });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
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
|
+
}
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
80
123
|
async function resolveDefaultInstanceId() {
|
|
81
124
|
if (_defaultInstanceId)
|
|
82
125
|
return _defaultInstanceId;
|
|
83
126
|
if (!JWT)
|
|
84
127
|
return '';
|
|
128
|
+
// Coalesce concurrent resolutions so parallel tool calls share one round-trip.
|
|
129
|
+
if (_resolveInFlight)
|
|
130
|
+
return _resolveInFlight;
|
|
85
131
|
// Cooldown: don't hammer the API on every tool call after a transient failure.
|
|
86
132
|
const now = Date.now();
|
|
87
133
|
if (_defaultInstanceLastAttempt > 0 && now - _defaultInstanceLastAttempt < 30_000)
|
|
88
134
|
return '';
|
|
89
135
|
_defaultInstanceLastAttempt = now;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
136
|
+
_resolveInFlight = (async () => {
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(`${API_URL}/api/v1/instances`, {
|
|
139
|
+
headers: { Authorization: `Bearer ${JWT}`, Accept: 'application/json' },
|
|
140
|
+
signal: AbortSignal.timeout(5000),
|
|
141
|
+
});
|
|
142
|
+
if (res.ok) {
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
const instances = (data?.data ?? []);
|
|
145
|
+
// Prefer running, fall back to provisioning so the ID resolves even during startup.
|
|
146
|
+
const best = instances.find(i => i.status === 'running') ?? instances.find(i => i.status === 'provisioning');
|
|
147
|
+
if (best) {
|
|
148
|
+
_defaultInstanceId = best.id;
|
|
149
|
+
_defaultInstanceLastAttempt = 0;
|
|
150
|
+
return _defaultInstanceId;
|
|
151
|
+
}
|
|
152
|
+
// Authenticated but zero instances → auto-provision so the user is never
|
|
153
|
+
// stuck. This self-heals the silent failure mode where device-flow
|
|
154
|
+
// provisioning failed once and every later tool call then ran with an
|
|
155
|
+
// empty instance_id (no recalls, never counted as an active instance).
|
|
156
|
+
const provisioned = await autoProvisionInstance();
|
|
157
|
+
if (provisioned) {
|
|
158
|
+
_defaultInstanceId = provisioned;
|
|
159
|
+
_defaultInstanceLastAttempt = 0;
|
|
160
|
+
void persistInstanceIdToConfig(provisioned);
|
|
161
|
+
return _defaultInstanceId;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
105
164
|
}
|
|
165
|
+
catch { /* transient error — will retry after cooldown */ }
|
|
166
|
+
return '';
|
|
167
|
+
})();
|
|
168
|
+
try {
|
|
169
|
+
return await _resolveInFlight;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
_resolveInFlight = null;
|
|
106
173
|
}
|
|
107
|
-
catch { /* transient error — will retry after cooldown */ }
|
|
108
|
-
return '';
|
|
109
174
|
}
|
|
110
175
|
// ── Self-Healing Auth ─────────────────────────────────────────────────────────
|
|
111
176
|
// Prevents the silent "0 recalls because the token quietly died" failure mode.
|
|
@@ -117,20 +182,24 @@ let _authHealAttemptedAt = 0;
|
|
|
117
182
|
const AUTH_HEAL_COOLDOWN_MS = 60_000;
|
|
118
183
|
// Surfaced in session_start / get_api_status so the user is never left guessing.
|
|
119
184
|
let _authDegradedNotice = '';
|
|
120
|
-
/** 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
|
+
*/
|
|
121
189
|
async function persistApiKeyToConfig(apiKey) {
|
|
122
190
|
try {
|
|
123
191
|
const { writeFile, mkdir, readFile } = await import('node:fs/promises');
|
|
124
192
|
const { existsSync } = await import('node:fs');
|
|
125
193
|
const { resolve, dirname } = await import('node:path');
|
|
126
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
194
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? process.env.APPDATA ?? '';
|
|
127
195
|
if (!home)
|
|
128
196
|
return;
|
|
129
|
-
|
|
197
|
+
// Primary config: create or update ~/.claude/mcp.json
|
|
198
|
+
const claudePath = resolve(home, '.claude', 'mcp.json');
|
|
130
199
|
let cfg = {};
|
|
131
|
-
if (existsSync(
|
|
200
|
+
if (existsSync(claudePath)) {
|
|
132
201
|
try {
|
|
133
|
-
cfg = JSON.parse(await readFile(
|
|
202
|
+
cfg = JSON.parse(await readFile(claudePath, 'utf-8'));
|
|
134
203
|
}
|
|
135
204
|
catch { /* corrupt — start fresh */ }
|
|
136
205
|
}
|
|
@@ -146,11 +215,67 @@ async function persistApiKeyToConfig(apiKey) {
|
|
|
146
215
|
env: { CACHLY_JWT: apiKey },
|
|
147
216
|
};
|
|
148
217
|
}
|
|
149
|
-
await mkdir(dirname(
|
|
150
|
-
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
|
+
}));
|
|
151
240
|
}
|
|
152
241
|
catch { /* non-critical — never break a tool call on a filesystem error */ }
|
|
153
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Persist the resolved instance id to ~/.claude/mcp.json so restarts reuse it
|
|
245
|
+
* (avoids a list/provision round-trip on every startup). Only updates an
|
|
246
|
+
* existing cachly entry — does not create config from scratch.
|
|
247
|
+
*/
|
|
248
|
+
async function persistInstanceIdToConfig(instanceId) {
|
|
249
|
+
if (!instanceId)
|
|
250
|
+
return;
|
|
251
|
+
try {
|
|
252
|
+
const { writeFile, readFile } = await import('node:fs/promises');
|
|
253
|
+
const { existsSync } = await import('node:fs');
|
|
254
|
+
const { resolve } = await import('node:path');
|
|
255
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? process.env.APPDATA ?? '';
|
|
256
|
+
if (!home)
|
|
257
|
+
return;
|
|
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
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
catch { /* non-critical */ }
|
|
278
|
+
}
|
|
154
279
|
/**
|
|
155
280
|
* Exchange the current (still-valid) token for a fresh long-lived API key.
|
|
156
281
|
* Returns true on success. Used by self-healing when a token is near expiry.
|
|
@@ -312,67 +437,13 @@ async function pollDeviceFlow(flow) {
|
|
|
312
437
|
// Persist the API key to ~/.claude/mcp.json so subsequent MCP server restarts
|
|
313
438
|
// keep the token. Without this, JWT is lost on restart and brain_recall_count
|
|
314
439
|
// is never incremented (telemetry has no api_key → no tenant resolution).
|
|
315
|
-
void (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
321
|
-
if (!home)
|
|
322
|
-
return;
|
|
323
|
-
const configPath = resolve(home, '.claude', 'mcp.json');
|
|
324
|
-
let cfg = {};
|
|
325
|
-
if (existsSync(configPath)) {
|
|
326
|
-
try {
|
|
327
|
-
cfg = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
328
|
-
}
|
|
329
|
-
catch { /* corrupt — start fresh */ }
|
|
330
|
-
}
|
|
331
|
-
cfg.mcpServers ??= {};
|
|
332
|
-
const existing = cfg.mcpServers['cachly'];
|
|
333
|
-
if (existing) {
|
|
334
|
-
existing.env ??= {};
|
|
335
|
-
existing.env['CACHLY_JWT'] = apiKey;
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
cfg.mcpServers['cachly'] = {
|
|
339
|
-
command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
340
|
-
env: { CACHLY_JWT: apiKey },
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
344
|
-
await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
345
|
-
}
|
|
346
|
-
catch { /* non-critical — don't break the flow on filesystem errors */ }
|
|
347
|
-
})();
|
|
348
|
-
// Auto-provision: find or create the user's brain instance.
|
|
440
|
+
void persistApiKeyToConfig(apiKey);
|
|
441
|
+
// Auto-provision: find or create the user's brain instance. resolveDefaultInstanceId
|
|
442
|
+
// now lists existing instances and auto-provisions when the account has none,
|
|
443
|
+
// so a single call covers both paths (and self-heals on every later tool call
|
|
444
|
+
// if provisioning is briefly unavailable here).
|
|
349
445
|
_defaultInstanceLastAttempt = 0;
|
|
350
446
|
await resolveDefaultInstanceId();
|
|
351
|
-
if (!_defaultInstanceId) {
|
|
352
|
-
try {
|
|
353
|
-
const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
|
|
354
|
-
method: 'POST',
|
|
355
|
-
headers: { Authorization: `Bearer ${JWT}`, 'Content-Type': 'application/json' },
|
|
356
|
-
signal: AbortSignal.timeout(12000),
|
|
357
|
-
});
|
|
358
|
-
if (autoRes.ok) {
|
|
359
|
-
const body = await autoRes.json();
|
|
360
|
-
const id = body.instance?.id ?? body.instance_id;
|
|
361
|
-
if (id)
|
|
362
|
-
_defaultInstanceId = id;
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
// Log so telemetry can detect free-tier auto-provision failures
|
|
366
|
-
void fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
367
|
-
method: 'POST',
|
|
368
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${JWT}` },
|
|
369
|
-
body: JSON.stringify({ event: 'auto_provision_failed', status: autoRes.status, version: CURRENT_VERSION }),
|
|
370
|
-
signal: AbortSignal.timeout(3000),
|
|
371
|
-
}).catch(() => { });
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
catch { /* non-fatal — server unreachable, getConnection will surface the error */ }
|
|
375
|
-
}
|
|
376
447
|
// Give a provisioning instance a head-start so getConnection's wait is shorter.
|
|
377
448
|
// The instance is almost always ready by the time the user's tool re-enters.
|
|
378
449
|
if (_defaultInstanceId) {
|
|
@@ -405,37 +476,10 @@ async function pollDeviceFlow(flow) {
|
|
|
405
476
|
}
|
|
406
477
|
catch { /* non-fatal — getConnection will handle the wait if still provisioning */ }
|
|
407
478
|
}
|
|
408
|
-
// Persist instance_id to ~/.claude/mcp.json after it's known
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
void (async () => {
|
|
413
|
-
try {
|
|
414
|
-
const { writeFile, readFile } = await import('node:fs/promises');
|
|
415
|
-
const { existsSync } = await import('node:fs');
|
|
416
|
-
const { resolve } = await import('node:path');
|
|
417
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
418
|
-
if (!home)
|
|
419
|
-
return;
|
|
420
|
-
const configPath = resolve(home, '.claude', 'mcp.json');
|
|
421
|
-
if (!existsSync(configPath))
|
|
422
|
-
return;
|
|
423
|
-
let cfg = {};
|
|
424
|
-
try {
|
|
425
|
-
cfg = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
const entry = cfg.mcpServers?.['cachly'];
|
|
431
|
-
if (entry?.env) {
|
|
432
|
-
entry.env['CACHLY_BRAIN_INSTANCE_ID'] = _defaultInstanceId;
|
|
433
|
-
await writeFile(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
catch { /* non-critical */ }
|
|
437
|
-
})();
|
|
438
|
-
}
|
|
479
|
+
// Persist instance_id to ~/.claude/mcp.json after it's known, so every
|
|
480
|
+
// restart reuses it instead of making an extra list call on startup.
|
|
481
|
+
if (_defaultInstanceId)
|
|
482
|
+
void persistInstanceIdToConfig(_defaultInstanceId);
|
|
439
483
|
return 'done';
|
|
440
484
|
}
|
|
441
485
|
if (data.error === 'slow_down')
|
|
@@ -457,10 +501,16 @@ export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp,
|
|
|
457
501
|
const pool = new Map();
|
|
458
502
|
async function getConnection(instance_id) {
|
|
459
503
|
if (!instance_id) {
|
|
460
|
-
|
|
504
|
+
// With auto-provisioning, this only happens when the API was briefly
|
|
505
|
+
// unreachable during resolution. The next tool call self-heals, so lead
|
|
506
|
+
// with "retry" rather than asking the user to configure anything.
|
|
507
|
+
throw new McpError(ErrorCode.InvalidRequest, '⏳ Your Brain instance is still being set up.\n\n' +
|
|
508
|
+
'This usually resolves in a few seconds — just call the tool again and it ' +
|
|
509
|
+
'will connect automatically.\n\n' +
|
|
510
|
+
'If it keeps happening:\n' +
|
|
511
|
+
'• Run `list_instances` to check your instances, or\n' +
|
|
461
512
|
'• Set CACHLY_BRAIN_INSTANCE_ID in your MCP config, or\n' +
|
|
462
|
-
'•
|
|
463
|
-
'• Run `list_instances` to see your available instances.');
|
|
513
|
+
'• Reach us at support@cachly.dev.');
|
|
464
514
|
}
|
|
465
515
|
if (pool.has(instance_id))
|
|
466
516
|
return pool.get(instance_id);
|
|
@@ -693,6 +743,7 @@ async function handleTool(name, args) {
|
|
|
693
743
|
return handleTool(name, args);
|
|
694
744
|
}
|
|
695
745
|
if (result === 'expired') {
|
|
746
|
+
sendFunnelEvent('device_flow_failed', { reason: 'timeout' });
|
|
696
747
|
_deviceFlow = null;
|
|
697
748
|
return '⌛ **Authentication timed out.** Please call any tool again to restart the sign-in flow.';
|
|
698
749
|
}
|
|
@@ -732,7 +783,7 @@ async function handleTool(name, args) {
|
|
|
732
783
|
'',
|
|
733
784
|
'After sign-in: call **any tool again** — your Brain activates instantly.',
|
|
734
785
|
'',
|
|
735
|
-
'✨ Free forever · No credit card ·
|
|
786
|
+
'✨ Free forever · No credit card · 114 MCP tools · GDPR · EU servers',
|
|
736
787
|
].join('\n');
|
|
737
788
|
}
|
|
738
789
|
// Device flow unavailable (network issue) — fall back to manual setup
|
|
@@ -813,12 +864,26 @@ async function handleTool(name, args) {
|
|
|
813
864
|
// Append bootstrap summary to the session_start briefing.
|
|
814
865
|
// brainResult is always a string here (handleBrainTool returns string for session_start).
|
|
815
866
|
const bootstrapText = String(gitBootstrap);
|
|
816
|
-
|
|
817
|
-
|
|
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;
|
|
818
874
|
}
|
|
819
875
|
}
|
|
820
876
|
}
|
|
821
|
-
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
|
+
}
|
|
822
887
|
}
|
|
823
888
|
}
|
|
824
889
|
else if (name === 'session_end') {
|