@cachly-dev/mcp-server 0.10.76 → 0.10.78

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.
@@ -0,0 +1,256 @@
1
+ import { safeJsonParse } from '../utils.js';
2
+ export const SHARE_TOOL_NAMES = new Set([
3
+ 'brain_share', 'brain_import',
4
+ ]);
5
+ async function scanAllLessons(redis, topicFilter) {
6
+ const keys = [];
7
+ const stream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
8
+ await new Promise((res, rej) => {
9
+ stream.on('data', (batch) => keys.push(...batch));
10
+ stream.on('end', res);
11
+ stream.on('error', rej);
12
+ });
13
+ const lessons = [];
14
+ for (const key of keys.slice(0, 500)) {
15
+ const raw = await redis.get(key);
16
+ if (!raw)
17
+ continue;
18
+ const lesson = safeJsonParse(raw, null);
19
+ if (!lesson || !lesson.topic)
20
+ continue;
21
+ if (topicFilter && topicFilter.length > 0) {
22
+ const matches = topicFilter.some(f => lesson.topic.includes(f));
23
+ if (!matches)
24
+ continue;
25
+ }
26
+ // Strip fields that should not be shared (internal audit trails, etc.)
27
+ lessons.push({
28
+ topic: lesson.topic,
29
+ outcome: lesson.outcome,
30
+ ...(lesson.what_worked ? { what_worked: lesson.what_worked } : {}),
31
+ ...(lesson.what_failed ? { what_failed: lesson.what_failed } : {}),
32
+ ...(lesson.ctx ? { ctx: lesson.ctx } : {}),
33
+ ...(lesson.tags?.length ? { tags: lesson.tags } : {}),
34
+ ...(lesson.author ? { author: lesson.author } : {}),
35
+ ...(lesson.service ? { service: lesson.service } : {}),
36
+ confidence: lesson.confidence ?? 1.0,
37
+ ts: lesson.ts,
38
+ });
39
+ }
40
+ return lessons;
41
+ }
42
+ export async function handleShareTool(name, args, getConnection, apiFetch) {
43
+ switch (name) {
44
+ // ── brain_share ────────────────────────────────────────────────────────────
45
+ case 'brain_share': {
46
+ const { instance_id, title = 'My Brain Snapshot', description = '', topic_filter, visibility = 'unlisted', max_lessons = 100, dry_run = false, } = args;
47
+ if (!instance_id)
48
+ return '⚠️ `brain_share` requires `instance_id`.';
49
+ const redis = await getConnection(instance_id);
50
+ const topicFilter = Array.isArray(topic_filter) && topic_filter.length > 0 ? topic_filter : undefined;
51
+ const allLessons = await scanAllLessons(redis, topicFilter);
52
+ const lessons = allLessons.slice(0, max_lessons);
53
+ if (lessons.length === 0) {
54
+ return [
55
+ `📤 **brain_share** — nothing to share yet`,
56
+ ``,
57
+ `No lessons found in Brain \`${instance_id}\`${topicFilter ? ` matching topics: ${topicFilter.join(', ')}` : ''}.`,
58
+ ``,
59
+ `Store lessons first with \`learn_from_attempts(topic="...", what_worked="...")\`.`,
60
+ ].join('\n');
61
+ }
62
+ if (dry_run) {
63
+ const sample = lessons.slice(0, 5).map(l => {
64
+ const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
65
+ return ` ${icon} \`${l.topic}\` — ${(l.what_worked ?? l.what_failed ?? '').slice(0, 80)}`;
66
+ });
67
+ return [
68
+ `📤 **brain_share (DRY RUN)** — would share ${lessons.length} lesson${lessons.length !== 1 ? 's' : ''}`,
69
+ ``,
70
+ `**Title:** ${title}`,
71
+ `**Visibility:** ${visibility}`,
72
+ topicFilter ? `**Topic filter:** ${topicFilter.join(', ')}` : '',
73
+ ``,
74
+ `**Sample lessons (first 5):**`,
75
+ ...sample,
76
+ lessons.length > 5 ? ` _...and ${lessons.length - 5} more_` : '',
77
+ ``,
78
+ `_Remove \`dry_run=true\` to create the public share._`,
79
+ ].filter(l => l !== '').join('\n');
80
+ }
81
+ // POST to API
82
+ let shareId = null;
83
+ let shareUrl = null;
84
+ try {
85
+ const result = await apiFetch('/api/v1/brains/share', {
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({ title, description, visibility, lessons }),
89
+ });
90
+ shareId = result.share_id ?? null;
91
+ if (shareId)
92
+ shareUrl = `https://cachly.dev/brain/share/${shareId}`;
93
+ }
94
+ catch (e) {
95
+ // API endpoint may not be live yet — store locally and return JSON
96
+ const err = e.message ?? '';
97
+ if (err.includes('404') || err.includes('not found') || err.includes('ECONNREFUSED')) {
98
+ const exportJson = JSON.stringify({ title, description, visibility, lessons, exported_at: new Date().toISOString() }, null, 2);
99
+ return [
100
+ `📤 **brain_share** — exported ${lessons.length} lesson${lessons.length !== 1 ? 's' : ''}`,
101
+ ``,
102
+ `⚠️ The public share API is not yet available. Here is your portable snapshot:`,
103
+ ``,
104
+ `\`\`\`json`,
105
+ exportJson.slice(0, 3000) + (exportJson.length > 3000 ? '\n... (truncated)' : ''),
106
+ `\`\`\``,
107
+ ``,
108
+ `_To import this on another Brain, use \`brain_import\` with the JSON payload._`,
109
+ ].join('\n');
110
+ }
111
+ return `❌ brain_share failed: ${e.message}`;
112
+ }
113
+ const sample = lessons.slice(0, 5).map(l => {
114
+ const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
115
+ return ` ${icon} \`${l.topic}\``;
116
+ });
117
+ // Record provenance
118
+ try {
119
+ const prov = JSON.stringify({ share_id: shareId, title, visibility, lesson_count: lessons.length, created_at: new Date().toISOString() });
120
+ await redis.rpush('cachly:brain:shares', prov);
121
+ await redis.ltrim('cachly:brain:shares', -20, -1);
122
+ }
123
+ catch { /* non-critical */ }
124
+ return [
125
+ `📤 **brain_share** — ${lessons.length} lesson${lessons.length !== 1 ? 's' : ''} shared`,
126
+ ``,
127
+ `**Share ID:** \`${shareId}\``,
128
+ `**URL:** ${shareUrl}`,
129
+ `**Title:** ${title}`,
130
+ `**Visible:** ${visibility}`,
131
+ ``,
132
+ `**Sample lessons:**`,
133
+ ...sample,
134
+ lessons.length > 5 ? ` _...and ${lessons.length - 5} more_` : '',
135
+ ``,
136
+ `**Anyone can import this Brain with:**`,
137
+ `\`brain_import(instance_id="<their-id>", share_id="${shareId}")\``,
138
+ ``,
139
+ `_To list all your shares, run \`brain_share_list\` (coming soon)._`,
140
+ ].filter(l => l !== '').join('\n');
141
+ }
142
+ // ── brain_import ───────────────────────────────────────────────────────────
143
+ case 'brain_import': {
144
+ const { instance_id, share_id, topic_prefix = '', min_confidence = 0.0, dry_run = false, overwrite = false, } = args;
145
+ if (!instance_id)
146
+ return '⚠️ `brain_import` requires `instance_id`.';
147
+ if (!share_id)
148
+ return '⚠️ `brain_import` requires `share_id`. Get one from `brain_share`.';
149
+ // Extract share ID from URL if passed as URL
150
+ const cleanShareId = share_id.replace(/^.*\/brain\/share\//, '').trim();
151
+ // Fetch the public share
152
+ let payload;
153
+ try {
154
+ payload = await apiFetch(`/api/v1/brains/share/${cleanShareId}`);
155
+ }
156
+ catch (e) {
157
+ const err = e.message ?? '';
158
+ if (err.includes('404') || err.includes('not found')) {
159
+ return [
160
+ `❌ **brain_import** — share not found`,
161
+ ``,
162
+ `Share ID \`${cleanShareId}\` does not exist or has been removed.`,
163
+ ``,
164
+ `Ask the Brain owner to re-run \`brain_share\` and share the new ID with you.`,
165
+ ].join('\n');
166
+ }
167
+ return `❌ brain_import failed: ${e.message}`;
168
+ }
169
+ const lessons = payload.lessons ?? [];
170
+ if (lessons.length === 0) {
171
+ return `⚠️ Share \`${cleanShareId}\` exists but contains no lessons.`;
172
+ }
173
+ const redis = await getConnection(instance_id);
174
+ let imported = 0;
175
+ let skipped = 0;
176
+ let conflicts = 0;
177
+ const importLog = [];
178
+ for (const lesson of lessons) {
179
+ if ((lesson.confidence ?? 1.0) < min_confidence) {
180
+ skipped++;
181
+ continue;
182
+ }
183
+ const topic = topic_prefix ? `${topic_prefix}:${lesson.topic}` : lesson.topic;
184
+ const bestKey = `cachly:lesson:best:${topic}`;
185
+ // Check for existing lesson
186
+ if (!overwrite) {
187
+ const existing = await redis.get(bestKey);
188
+ if (existing) {
189
+ conflicts++;
190
+ continue;
191
+ }
192
+ }
193
+ const lessonToStore = JSON.stringify({
194
+ ...lesson,
195
+ topic,
196
+ imported_from: cleanShareId,
197
+ imported_at: new Date().toISOString(),
198
+ });
199
+ if (!dry_run) {
200
+ await redis.set(bestKey, lessonToStore);
201
+ const listKey = `cachly:lessons:${topic}`;
202
+ await redis.rpush(listKey, lessonToStore);
203
+ await redis.ltrim(listKey, -100, -1);
204
+ await redis.expire(listKey, 90 * 86400);
205
+ }
206
+ imported++;
207
+ if (importLog.length < 6) {
208
+ const icon = lesson.outcome === 'success' ? '✅' : lesson.outcome === 'failure' ? '❌' : '⚠️';
209
+ importLog.push(` ${icon} \`${topic}\` — ${(lesson.what_worked ?? lesson.what_failed ?? '').slice(0, 80)}`);
210
+ }
211
+ }
212
+ // Record import provenance
213
+ if (!dry_run && imported > 0) {
214
+ try {
215
+ const prov = JSON.stringify({
216
+ share_id: cleanShareId, title: payload.title,
217
+ imported: imported, skipped, conflicts,
218
+ topic_prefix, imported_at: new Date().toISOString(),
219
+ });
220
+ await redis.rpush('cachly:brain:imports', prov);
221
+ await redis.ltrim('cachly:brain:imports', -20, -1);
222
+ }
223
+ catch { /* non-critical */ }
224
+ }
225
+ const dryTag = dry_run ? ' (DRY RUN — no writes)' : '';
226
+ const shareTitle = payload.title ?? cleanShareId;
227
+ const lines = [
228
+ `📥 **brain_import${dryTag}** — from "${shareTitle}"`,
229
+ ``,
230
+ `**Share ID:** \`${cleanShareId}\``,
231
+ `**Lessons imported:** ${imported}`,
232
+ ...(skipped > 0 ? [`**Skipped (low confidence):** ${skipped}`] : []),
233
+ ...(conflicts > 0 ? [`**Skipped (already exist):** ${conflicts} _(pass \`overwrite=true\` to replace)_`] : []),
234
+ ``,
235
+ ];
236
+ if (importLog.length > 0) {
237
+ lines.push(`**Sample lessons imported:**`, ...importLog);
238
+ if (imported > 6)
239
+ lines.push(` _...and ${imported - 6} more_`);
240
+ }
241
+ if (imported > 0 && !dry_run) {
242
+ lines.push(``, `✅ Brain updated — use \`smart_recall(query="...")\` to query the new knowledge.`);
243
+ }
244
+ else if (dry_run) {
245
+ lines.push(``, `_Remove \`dry_run=true\` to actually import these lessons._`);
246
+ }
247
+ else if (imported === 0 && conflicts > 0) {
248
+ lines.push(``, `ℹ️ All lessons already exist in your Brain. Pass \`overwrite=true\` to update them.`);
249
+ }
250
+ return lines.join('\n');
251
+ }
252
+ default:
253
+ return null;
254
+ }
255
+ }
256
+ //# sourceMappingURL=share.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share.js","sourceRoot":"","sources":["../../../src/handlers/share.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAK5C,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IACtC,aAAa,EAAE,cAAc;CAC9B,CAAC,CAAC;AA0BH,KAAK,UAAU,cAAc,CAAC,KAAY,EAAE,WAAsB;IAChE,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,sBAAsB,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAC/E,MAAM,IAAI,OAAO,CAAO,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACnC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAe,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;QAC5D,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACtB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAmB,EAAE,CAAC;IACnC,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,MAAM,MAAM,GAAG,aAAa,CAAsB,GAAG,EAAE,IAAI,CAAC,CAAC;QAC7D,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK;YAAE,SAAS;QACvC,IAAI,WAAW,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1C,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;YAChE,IAAI,CAAC,OAAO;gBAAE,SAAS;QACzB,CAAC;QACD,uEAAuE;QACvE,OAAO,CAAC,IAAI,CAAC;YACX,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1C,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnD,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACtD,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,GAAG;YACpC,EAAE,EAAE,MAAM,CAAC,EAAE;SACd,CAAC,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAY,EACZ,IAA6B,EAC7B,aAA4B,EAC5B,QAAkB;IAElB,QAAQ,IAAI,EAAE,CAAC;QAEb,8EAA8E;QAC9E,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,MAAM,EACJ,WAAW,EACX,KAAK,GAAG,mBAAmB,EAC3B,WAAW,GAAG,EAAE,EAChB,YAAY,EACZ,UAAU,GAAG,UAAU,EACvB,WAAW,GAAG,GAAG,EACjB,OAAO,GAAG,KAAK,GAChB,GAAG,IAQH,CAAC;YAEF,IAAI,CAAC,WAAW;gBAAE,OAAO,0CAA0C,CAAC;YAEpE,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,CAAC;YAC/C,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;YACtG,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;YAC5D,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,WAAqB,CAAC,CAAC;YAE3D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACzB,OAAO;oBACL,2CAA2C;oBAC3C,EAAE;oBACF,+BAA+B,WAAW,KAAK,WAAW,CAAC,CAAC,CAAC,qBAAqB,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG;oBAClH,EAAE;oBACF,mFAAmF;iBACpF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACf,CAAC;YAED,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;oBACzC,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;oBAClF,OAAO,KAAK,IAAI,MAAM,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;gBAC7F,CAAC,CAAC,CAAC;gBACH,OAAO;oBACL,8CAA8C,OAAO,CAAC,MAAM,UAAU,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;oBACvG,EAAE;oBACF,cAAc,KAAK,EAAE;oBACrB,mBAAmB,UAAU,EAAE;oBAC/B,WAAW,CAAC,CAAC,CAAC,qBAAqB,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;oBAChE,EAAE;oBACF,+BAA+B;oBAC/B,GAAG,MAAM;oBACT,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,OAAO,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;oBACjE,EAAE;oBACF,uDAAuD;iBACxD,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrC,CAAC;YAED,cAAc;YACd,IAAI,OAAO,GAAkB,IAAI,CAAC;YAClC,IAAI,QAAQ,GAAkB,IAAI,CAAC;YACnC,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAe,sBAAsB,EAAE;oBAClE,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;oBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;iBAClE,CAAC,CAAC;gBACH,OAAO,GAAG,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC;gBAClC,IAAI,OAAO;oBAAE,QAAQ,GAAG,kCAAkC,OAAO,EAAE,CAAC;YACtE,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,mEAAmE;gBACnE,MAAM,GAAG,GAAI,CAAW,CAAC,OAAO,IAAI,EAAE,CAAC;gBACvC,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;oBACrF,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;oBAC/H,OAAO;wBACL,iCAAiC,OAAO,CAAC,MAAM,UAAU,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;wBAC1F,EAAE;wBACF,+EAA+E;wBAC/E,EAAE;wBACF,YAAY;wBACZ,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC;wBACjF,QAAQ;wBACR,EAAE;wBACF,gFAAgF;qBACjF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACf,CAAC;gBACD,OAAO,yBAA0B,CAAW,CAAC,OAAO,EAAE,CAAC;YACzD,CAAC;YAED,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;gBACzC,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;gBAClF,OAAO,KAAK,IAAI,MAAM,CAAC,CAAC,KAAK,IAAI,CAAC;YACpC,CAAC,CAAC,CAAC;YAEH,oBAAoB;YACpB,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;gBAC1I,MAAM,KAAK,CAAC,KAAK,CAAC,qBAAqB,EAAE,IAAI,CAAC,CAAC;gBAC/C,MAAM,KAAK,CAAC,KAAK,CAAC,qBAAqB,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;YACpD,CAAC;YAAC,MAAM,CAAC,CAAC,kBAAkB,CAAC,CAAC;YAE9B,OAAO;gBACL,wBAAwB,OAAO,CAAC,MAAM,UAAU,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,SAAS;gBACxF,EAAE;gBACF,mBAAmB,OAAO,IAAI;gBAC9B,iBAAiB,QAAQ,EAAE;gBAC3B,iBAAiB,KAAK,EAAE;gBACxB,iBAAiB,UAAU,EAAE;gBAC7B,EAAE;gBACF,qBAAqB;gBACrB,GAAG,MAAM;gBACT,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,OAAO,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;gBACjE,EAAE;gBACF,wCAAwC;gBACxC,sDAAsD,OAAO,MAAM;gBACnE,EAAE;gBACF,oEAAoE;aACrE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrC,CAAC;QAED,8EAA8E;QAC9E,KAAK,cAAc,CAAC,CAAC,CAAC;YACpB,MAAM,EACJ,WAAW,EACX,QAAQ,EACR,YAAY,GAAG,EAAE,EACjB,cAAc,GAAG,GAAG,EACpB,OAAO,GAAG,KAAK,EACf,SAAS,GAAG,KAAK,GAClB,GAAG,IAOH,CAAC;YAEF,IAAI,CAAC,WAAW;gBAAE,OAAO,2CAA2C,CAAC;YACrE,IAAI,CAAC,QAAQ;gBAAE,OAAO,oEAAoE,CAAC;YAE3F,6CAA6C;YAC7C,MAAM,YAAY,GAAG,QAAQ,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAExE,yBAAyB;YACzB,IAAI,OAAqB,CAAC;YAC1B,IAAI,CAAC;gBACH,OAAO,GAAG,MAAM,QAAQ,CAAe,wBAAwB,YAAY,EAAE,CAAC,CAAC;YACjF,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,GAAG,GAAI,CAAW,CAAC,OAAO,IAAI,EAAE,CAAC;gBACvC,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;oBACrD,OAAO;wBACL,sCAAsC;wBACtC,EAAE;wBACF,cAAc,YAAY,wCAAwC;wBAClE,EAAE;wBACF,8EAA8E;qBAC/E,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACf,CAAC;gBACD,OAAO,0BAA2B,CAAW,CAAC,OAAO,EAAE,CAAC;YAC1D,CAAC;YAED,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;YACtC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACzB,OAAO,cAAc,YAAY,oCAAoC,CAAC;YACxE,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,CAAC;YAC/C,IAAI,QAAQ,GAAG,CAAC,CAAC;YACjB,IAAI,OAAO,GAAG,CAAC,CAAC;YAChB,IAAI,SAAS,GAAG,CAAC,CAAC;YAClB,MAAM,SAAS,GAAa,EAAE,CAAC;YAE/B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,GAAG,CAAC,GAAI,cAAyB,EAAE,CAAC;oBAAC,OAAO,EAAE,CAAC;oBAAC,SAAS;gBAAC,CAAC;gBAErF,MAAM,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,YAAY,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;gBAC9E,MAAM,OAAO,GAAG,sBAAsB,KAAK,EAAE,CAAC;gBAE9C,4BAA4B;gBAC5B,IAAI,CAAC,SAAS,EAAE,CAAC;oBACf,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;oBAC1C,IAAI,QAAQ,EAAE,CAAC;wBAAC,SAAS,EAAE,CAAC;wBAAC,SAAS;oBAAC,CAAC;gBAC1C,CAAC;gBAED,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC;oBACnC,GAAG,MAAM;oBACT,KAAK;oBACL,aAAa,EAAE,YAAY;oBAC3B,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;iBACtC,CAAC,CAAC;gBAEH,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;oBACxC,MAAM,OAAO,GAAG,kBAAkB,KAAK,EAAE,CAAC;oBAC1C,MAAM,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;oBAC1C,MAAM,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;oBACrC,MAAM,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,GAAG,KAAK,CAAC,CAAC;gBAC1C,CAAC;gBAED,QAAQ,EAAE,CAAC;gBACX,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACzB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;oBAC5F,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,MAAM,KAAK,QAAQ,CAAC,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;gBAC9G,CAAC;YACH,CAAC;YAED,2BAA2B;YAC3B,IAAI,CAAC,OAAO,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;gBAC7B,IAAI,CAAC;oBACH,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;wBAC1B,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK;wBAC5C,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS;wBACtC,YAAY,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;qBACpD,CAAC,CAAC;oBACH,MAAM,KAAK,CAAC,KAAK,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAC;oBAChD,MAAM,KAAK,CAAC,KAAK,CAAC,sBAAsB,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;gBACrD,CAAC;gBAAC,MAAM,CAAC,CAAC,kBAAkB,CAAC,CAAC;YAChC,CAAC;YAED,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,EAAE,CAAC;YACvD,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,IAAI,YAAY,CAAC;YACjD,MAAM,KAAK,GAAG;gBACZ,oBAAoB,MAAM,cAAc,UAAU,GAAG;gBACrD,EAAE;gBACF,mBAAmB,YAAY,IAAI;gBACnC,yBAAyB,QAAQ,EAAE;gBACnC,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,iCAAiC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACpE,GAAG,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,gCAAgC,SAAS,0CAA0C,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/G,EAAE;aACH,CAAC;YAEF,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzB,KAAK,CAAC,IAAI,CAAC,8BAA8B,EAAE,GAAG,SAAS,CAAC,CAAC;gBACzD,IAAI,QAAQ,GAAG,CAAC;oBAAE,KAAK,CAAC,IAAI,CAAC,aAAa,QAAQ,GAAG,CAAC,QAAQ,CAAC,CAAC;YAClE,CAAC;YAED,IAAI,QAAQ,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC7B,KAAK,CAAC,IAAI,CACR,EAAE,EACF,iFAAiF,CAClF,CAAC;YACJ,CAAC;iBAAM,IAAI,OAAO,EAAE,CAAC;gBACnB,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,6DAA6D,CAAC,CAAC;YAChF,CAAC;iBAAM,IAAI,QAAQ,KAAK,CAAC,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;gBAC3C,KAAK,CAAC,IAAI,CACR,EAAE,EACF,qFAAqF,CACtF,CAAC;YACJ,CAAC;YAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QAED;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC"}
package/dist/src/index.js CHANGED
@@ -60,10 +60,12 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
60
60
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
61
61
  import { Redis } from 'ioredis';
62
62
  // ── Config ────────────────────────────────────────────────────────────────────
63
- const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
63
+ // Mutable so the `setup`/`init` CLI can honor a `--api-url` flag for self-hosting.
64
+ // Reassigned (if at all) before any network call is made.
65
+ let API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
64
66
  let JWT = process.env.CACHLY_JWT ?? '';
65
67
  const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
66
- const CURRENT_VERSION = '0.10.76';
68
+ const CURRENT_VERSION = '0.10.78';
67
69
  // Max time to wait for a freshly-provisioned instance to become "running" before
68
70
  // giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
69
71
  // old hard-coded 25s caused spurious "instance_not_reachable" on first run.
@@ -607,6 +609,7 @@ import { handleRoadmapTool } from './handlers/roadmap.js';
607
609
  import { handleAdvancedTool } from './handlers/advanced.js';
608
610
  import { handleSyndicateTool } from './handlers/syndicate.js';
609
611
  import { handleFedbrainTool, _lastBrainFromGitCounts } from './handlers/fedbrain.js';
612
+ import { handleShareTool } from './handlers/share.js';
610
613
  // ── Tools (imported from tools.ts) ─────────────────────────────────────────
611
614
  import { TOOLS } from './tools.js';
612
615
  // ── Handlers ──────────────────────────────────────────────────────────────────
@@ -837,6 +840,9 @@ async function handleTool(name, args) {
837
840
  }
838
841
  return syndicateResult;
839
842
  }
843
+ const shareResult = await handleShareTool(name, args, getConnection, apiFetch);
844
+ if (shareResult !== null)
845
+ return shareResult;
840
846
  const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
841
847
  if (fedbrainResult !== null) {
842
848
  // brain_from_git and brain_predict_failures are in fedbrain handler.
@@ -1110,13 +1116,26 @@ const EDITOR_FILES = {
1110
1116
  };
1111
1117
  const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
1112
1118
  const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
1119
+ const DEFAULT_API_URL = 'https://api.cachly.dev';
1120
+ // Build the env block written into an editor's MCP config. Self-hosting is
1121
+ // first-class: if the operator runs setup/init against their own backend
1122
+ // (CACHLY_API_URL set to a non-default URL), that URL is baked into the config so
1123
+ // the editor-launched server talks to the self-hosted instance — not api.cachly.dev.
1124
+ // For the default cloud backend we OMIT CACHLY_API_URL to keep configs clean (the
1125
+ // binary already defaults to it).
1126
+ function buildServerEnv(apiKey, instanceId) {
1127
+ const env = { CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId };
1128
+ if (API_URL && API_URL !== DEFAULT_API_URL)
1129
+ env.CACHLY_API_URL = API_URL;
1130
+ return env;
1131
+ }
1113
1132
  function buildMcpConfig(apiKey, instanceId, editor) {
1114
1133
  if (editor === 'continue') {
1115
1134
  return JSON.stringify({
1116
1135
  experimental: {
1117
1136
  modelContextProtocolServers: [{
1118
1137
  transport: { type: 'stdio', command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'] },
1119
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
1138
+ env: buildServerEnv(apiKey, instanceId),
1120
1139
  }],
1121
1140
  },
1122
1141
  }, null, 2);
@@ -1128,7 +1147,7 @@ function buildMcpConfig(apiKey, instanceId, editor) {
1128
1147
  command: {
1129
1148
  path: 'npx',
1130
1149
  args: ['-y', '@cachly-dev/mcp-server@latest'],
1131
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
1150
+ env: buildServerEnv(apiKey, instanceId),
1132
1151
  },
1133
1152
  settings: {},
1134
1153
  },
@@ -1140,7 +1159,7 @@ function buildMcpConfig(apiKey, instanceId, editor) {
1140
1159
  cachly: {
1141
1160
  command: 'npx',
1142
1161
  args: ['-y', '@cachly-dev/mcp-server@latest'],
1143
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
1162
+ env: buildServerEnv(apiKey, instanceId),
1144
1163
  },
1145
1164
  },
1146
1165
  }, null, 2);
@@ -1151,7 +1170,7 @@ async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsOps) {
1151
1170
  const cachlyEntry = {
1152
1171
  command: 'npx',
1153
1172
  args: ['-y', '@cachly-dev/mcp-server@latest'],
1154
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
1173
+ env: buildServerEnv(apiKey, instanceId),
1155
1174
  };
1156
1175
  if (!fsOps.existsSync(configPath))
1157
1176
  return buildMcpConfig(apiKey, instanceId, editor);
@@ -1558,7 +1577,9 @@ if (process.argv[2] === 'join') {
1558
1577
  env['CACHLY_BRAIN_INSTANCE_ID'] = instance_id;
1559
1578
  if (joinJwt && !env['CACHLY_JWT'])
1560
1579
  env['CACHLY_JWT'] = joinJwt;
1561
- env['CACHLY_API_URL'] ??= 'https://api.cachly.dev';
1580
+ // Only persist a self-hosted backend URL; leave default-cloud configs clean.
1581
+ if (API_URL !== DEFAULT_API_URL)
1582
+ env['CACHLY_API_URL'] = API_URL;
1562
1583
  servers['cachly'] = { ...existing, command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'], env };
1563
1584
  cfg['mcpServers'] = servers;
1564
1585
  await wfJ(ePath, JSON.stringify(cfg, null, 2), 'utf-8');
@@ -1923,6 +1944,11 @@ if (process.argv[2] === 'init') {
1923
1944
  let apiKey = flag('api-key') ?? process.env.CACHLY_JWT;
1924
1945
  const editor = (flag('editor') ?? 'claude').toLowerCase();
1925
1946
  const projectDir = resolve(flag('project-dir') ?? '.');
1947
+ // Self-hosting: --api-url overrides the backend baked into the written config.
1948
+ const apiUrlFlag = flag('api-url');
1949
+ if (apiUrlFlag) {
1950
+ API_URL = apiUrlFlag.replace(/\/+$/, '');
1951
+ }
1926
1952
  // Zero-arg path: fall back to credentials saved by a previous `setup`
1927
1953
  // (~/.claude/mcp.json). Makes `init` a fast, idempotent re-configuration command
1928
1954
  // you can run in any project without copy-pasting tokens.
@@ -1944,7 +1970,7 @@ if (process.argv[2] === 'init') {
1944
1970
  catch { /* fall through to the usage error below */ }
1945
1971
  }
1946
1972
  if (!instanceId || !apiKey) {
1947
- 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]\n');
1973
+ 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');
1948
1974
  console.error('First time? Run the zero-config wizard (signs you in, auto-provisions): npx @cachly-dev/mcp-server@latest setup\n');
1949
1975
  console.error('Get your credentials from: https://cachly.dev/setup-ai\n');
1950
1976
  process.exit(1);
@@ -2108,6 +2134,11 @@ if (process.argv[2] === 'health') {
2108
2134
  if (!jwt) {
2109
2135
  fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
2110
2136
  }
2137
+ else if (jwt.startsWith('cky_')) {
2138
+ // Long-lived API key (the credential `setup` provisions). It is not a JWT and
2139
+ // never expires client-side — treat a well-formed key as healthy.
2140
+ ok('Long-lived API key set (cky_…) — no client-side expiry');
2141
+ }
2111
2142
  else {
2112
2143
  try {
2113
2144
  const parts = jwt.split('.');
@@ -2127,7 +2158,7 @@ if (process.argv[2] === 'health') {
2127
2158
  }
2128
2159
  }
2129
2160
  catch {
2130
- fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
2161
+ fail('CACHLY_JWT format invalid (expected a cky_ API key or a 3-part JWT)');
2131
2162
  }
2132
2163
  }
2133
2164
  // ── 2. Brain API reachability ───────────────────────────────────────────────
@@ -2275,6 +2306,30 @@ if (process.argv[2] === 'health') {
2275
2306
  else {
2276
2307
  warn('.git/hooks/post-commit not found — run: npx @cachly-dev/mcp-server@latest setup');
2277
2308
  }
2309
+ // ── 6. Embedding provider (BYOK) ─────────────────────────────────────────────
2310
+ console.log('\n🧬 Embedding provider (semantic search)');
2311
+ {
2312
+ const providerKeys = [
2313
+ ['OPENAI_API_KEY', 'openai'], ['GEMINI_API_KEY', 'gemini'],
2314
+ ['MISTRAL_API_KEY', 'mistral'], ['COHERE_API_KEY', 'cohere'],
2315
+ ['OLLAMA_BASE_URL', 'ollama'],
2316
+ ];
2317
+ const explicit = (process.env.CACHLY_EMBED_PROVIDER ?? '').toLowerCase();
2318
+ const byok = providerKeys.find(([envVar]) => process.env[envVar]);
2319
+ if (explicit && explicit !== 'cachly') {
2320
+ const envVar = providerKeys.find(([, p]) => p === explicit)?.[0];
2321
+ if (envVar && process.env[envVar])
2322
+ ok(`BYOK: ${explicit} (key present in ${envVar})`);
2323
+ else
2324
+ warn(`CACHLY_EMBED_PROVIDER=${explicit} but its key env var is not set — semantic search will fail`);
2325
+ }
2326
+ else if (byok) {
2327
+ ok(`BYOK: ${byok[1]} auto-detected (${byok[0]} present)`);
2328
+ }
2329
+ else {
2330
+ ok('Server-side embeddings (cachly) — no key needed, uses your JWT');
2331
+ }
2332
+ }
2278
2333
  // ── Summary ─────────────────────────────────────────────────────────────────
2279
2334
  console.log(`\n${'─'.repeat(40)}`);
2280
2335
  if (failed === 0) {
@@ -2295,6 +2350,13 @@ if (process.argv[2] === 'setup') {
2295
2350
  const { resolve, dirname } = await import('node:path');
2296
2351
  const { createInterface } = await import('node:readline');
2297
2352
  const setupStartMs = Date.now();
2353
+ // Self-hosting: `setup --api-url https://cachly.mycorp.internal` points the whole
2354
+ // wizard (auth, provisioning, config writes) at a private backend.
2355
+ const _apiUrlIdx = process.argv.indexOf('--api-url');
2356
+ if (_apiUrlIdx !== -1 && process.argv[_apiUrlIdx + 1]) {
2357
+ API_URL = process.argv[_apiUrlIdx + 1].replace(/\/+$/, '');
2358
+ console.log(`ℹ️ Using self-hosted backend: ${API_URL}\n`);
2359
+ }
2298
2360
  sendFunnelEvent('setup_started');
2299
2361
  // --yes / -y → non-interactive mode (skips all prompts, picks defaults).
2300
2362
  // Also auto-detect a non-interactive stdin (VSCode tasks, CI, piped input):
@@ -2656,7 +2718,7 @@ if (process.argv[2] === 'setup') {
2656
2718
  globalConfig.mcpServers['cachly'] = {
2657
2719
  command: 'npx',
2658
2720
  args: ['-y', '@cachly-dev/mcp-server@latest'],
2659
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: token, CACHLY_BRAIN_INSTANCE_ID: instance.id },
2721
+ env: buildServerEnv(token, instance.id),
2660
2722
  };
2661
2723
  await writeFile(globalClaudePath, JSON.stringify(globalConfig, null, 2), 'utf-8');
2662
2724
  console.log(`✅ Written: ~/.claude/mcp.json (global — works in every project)`);
@@ -2932,37 +2994,45 @@ if (process.argv[2] === 'learn-git') {
2932
2994
  process.exit(0);
2933
2995
  }
2934
2996
  // ── Start ─────────────────────────────────────────────────────────────────────
2935
- // Warn on stderr when credentials are missing so the user sees a clear
2936
- // actionable message in their editor's MCP log instead of silent failures.
2937
- // Skip for CLI commands that intentionally run without credentials.
2997
+ // Credential-missing handling at startup. The critical distinction is HOW we were
2998
+ // launched, because writing anything to stdout in stdio-MCP mode corrupts the
2999
+ // JSON-RPC stream and exiting kills the zero-credential device-flow onboarding.
3000
+ // • Human in a real terminal (TTY), no args → show the setup banner, exit 0.
3001
+ // • Editor as an MCP stdio server (non-TTY) → ONE stderr hint, then KEEP RUNNING
3002
+ // so tools/list works and the first tool call starts the browser sign-in.
3003
+ // Skip entirely for CLI subcommands that intentionally run without credentials.
2938
3004
  const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
2939
- if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
2940
- // No args + no JWT in a non-TTY context = running as MCP server without credentials.
2941
- // Print actionable setup banner to stdout so it's captured by callers/tests.
2942
- const banner = '\n' +
2943
- '╔══════════════════════════════════════════════════════════════════╗\n' +
2944
- '║ 🧠 cachly AI Brain — Setup required ║\n' +
2945
- '╠══════════════════════════════════════════════════════════════════╣\n' +
2946
- '║ ║\n' +
2947
- '║ CACHLY_JWT is not set. Get your free credentials at: ║\n' +
2948
- '║ ║\n' +
2949
- '║ 👉 https://cachly.dev/setup-ai ║\n' +
2950
- '║ ║\n' +
2951
- '║ Then run the interactive setup wizard: ║\n' +
2952
- '║ ║\n' +
2953
- '║ npx @cachly-dev/mcp-server@latest setup ║\n' +
2954
- '║ ║\n' +
2955
- '║ Free tier — no credit card required. ║\n' +
2956
- '╚══════════════════════════════════════════════════════════════════╝\n' +
2957
- '\n';
2958
- // When invoked with no args as the main entry point, write to stdout + exit cleanly.
2959
- // When used as MCP server (args present), write to stderr so editors see it.
2960
- if (!process.argv[2] && _isMain) {
3005
+ if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '')) {
3006
+ const runningInTerminal = !process.argv[2] && process.stdout.isTTY === true && _isMain;
3007
+ if (runningInTerminal) {
3008
+ // A human ran `npx @cachly-dev/mcp-server` directly with no credentials.
3009
+ // Show them how to set up, then exit cleanly.
3010
+ const banner = '\n' +
3011
+ '╔══════════════════════════════════════════════════════════════════╗\n' +
3012
+ '║ 🧠 cachly AI Brain — Setup required ║\n' +
3013
+ '╠══════════════════════════════════════════════════════════════════╣\n' +
3014
+ '║ ║\n' +
3015
+ '║ CACHLY_JWT is not set. Get your free credentials at: ║\n' +
3016
+ '║ ║\n' +
3017
+ '║ 👉 https://cachly.dev/setup-ai ║\n' +
3018
+ '║ ║\n' +
3019
+ '║ Then run the interactive setup wizard: ║\n' +
3020
+ '║ ║\n' +
3021
+ '║ npx @cachly-dev/mcp-server@latest setup ║\n' +
3022
+ '║ ║\n' +
3023
+ '║ Free tier — no credit card required. ║\n' +
3024
+ '╚══════════════════════════════════════════════════════════════════╝\n' +
3025
+ '\n';
2961
3026
  process.stdout.write(banner);
2962
3027
  process.exit(0);
2963
3028
  }
2964
3029
  else {
2965
- process.stderr.write(banner);
3030
+ // Launched as an MCP stdio server (or HTTP) without credentials. NEVER touch
3031
+ // stdout here — it carries the JSON-RPC protocol. Emit a single actionable
3032
+ // hint to stderr (shown in the editor's MCP log) and fall through so the
3033
+ // server starts and the zero-credential device flow can run on first tool call.
3034
+ process.stderr.write('\n🧠 cachly: no CACHLY_JWT set yet — call any cachly tool and a 10-second browser sign-in starts automatically.\n' +
3035
+ ' (Or run once: npx @cachly-dev/mcp-server@latest setup)\n\n');
2966
3036
  }
2967
3037
  }
2968
3038
  else {