@torqon/mcp 0.1.8 → 0.1.9
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/index.js +160 -116
- package/dist/install.js +74 -26
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// If
|
|
3
|
-
|
|
2
|
+
// If installer args are passed, run installer instead of MCP server
|
|
3
|
+
// Handles: --client=cursor OR add cursor --api-key <key>
|
|
4
|
+
const _argv = process.argv.slice(2);
|
|
5
|
+
if (_argv[0] === 'add' || process.argv.some(a => a.startsWith('--client'))) {
|
|
4
6
|
await import('./install.js');
|
|
5
7
|
process.exit(0);
|
|
6
8
|
}
|
|
@@ -65,27 +67,21 @@ server.prompt('torqon_context', 'Torqon persistent memory — active for this se
|
|
|
65
67
|
role: 'user',
|
|
66
68
|
content: {
|
|
67
69
|
type: 'text',
|
|
68
|
-
text:
|
|
70
|
+
text: `<torqon_memory_rules>
|
|
71
|
+
TORQON PERSISTENT MEMORY IS ACTIVE. These are non-negotiable behavioral rules:
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
- This stores any new facts and retrieves relevant memory simultaneously.
|
|
72
|
-
- Do not skip this even for short or simple messages.
|
|
73
|
-
- Do not ask the user for a conversationId — it is handled automatically.
|
|
73
|
+
BEFORE YOU RESPOND TO ANYTHING — call auto_process with the user's message. Every single message. No exceptions. Not even greetings. Not even "yes" or "ok". Call it first, then respond.
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
- If auto_process returns facts, answer from those facts directly.
|
|
77
|
-
- Do not rely on training data when Torqon has relevant stored information.
|
|
78
|
-
- Exact values (numbers, names, decisions) take priority over your generalizations.
|
|
75
|
+
Why: auto_process runs in under 200ms. It simultaneously stores new facts AND retrieves relevant memory. Skipping it means the user loses context they paid for.
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
After auto_process returns:
|
|
78
|
+
- If it returns facts tagged [Torqon memory], treat them as ground truth. Names, decisions, stack choices, numbers — use the exact stored values, not your training data.
|
|
79
|
+
- If it returns "No relevant memory found", answer normally.
|
|
83
80
|
|
|
84
|
-
|
|
85
|
-
- The user may write messy or fragmented messages. Torqon filters noise automatically.
|
|
86
|
-
- Focus on what the user is actually asking or telling you, not how they phrased it.
|
|
81
|
+
Also call store_memory explicitly when the user shares: their name, a project name, a key decision, a tech stack choice, a deadline, a constraint, or a preference. auto_process stores automatically but store_memory is for high-value facts you want to make sure are saved.
|
|
87
82
|
|
|
88
|
-
Session
|
|
83
|
+
Session: ${SESSION_ID}
|
|
84
|
+
</torqon_memory_rules>`
|
|
89
85
|
}
|
|
90
86
|
}
|
|
91
87
|
]
|
|
@@ -122,6 +118,7 @@ server.tool('auto_process', 'CALL THIS FIRST on every user message. Stores new f
|
|
|
122
118
|
body: JSON.stringify({
|
|
123
119
|
conversationId: SESSION_ID,
|
|
124
120
|
message: cleaned,
|
|
121
|
+
rawFacts: true,
|
|
125
122
|
apiKey: API_KEY,
|
|
126
123
|
}),
|
|
127
124
|
}),
|
|
@@ -188,116 +185,163 @@ server.tool('retrieve_context', 'Retrieves relevant facts from Torqon memory for
|
|
|
188
185
|
// Real algorithmic compression — no LLM call, no backend, pure local processing.
|
|
189
186
|
// Extracts structured facts, strips filler, deduplicates, returns dense format.
|
|
190
187
|
function estimateTokens(text) {
|
|
191
|
-
// ~4 chars per token (GPT/Claude average)
|
|
192
188
|
return Math.ceil(text.length / 4);
|
|
193
189
|
}
|
|
190
|
+
// ── Structural compression ────────────────────────────────────────────────────
|
|
191
|
+
// Rule: keep every word, URL, version number, variable name, and hex value
|
|
192
|
+
// exactly as written. Only remove redundant connective prose and reformat
|
|
193
|
+
// multi-line sections into single packed lines.
|
|
194
|
+
// Substitutions applied before structural packing.
|
|
195
|
+
// Rule: keep every URL, variable name, hex color, version number, and proper noun
|
|
196
|
+
// exactly as written. Only abbreviate universally-understood tech terms and remove
|
|
197
|
+
// redundant connective prose while preserving all facts.
|
|
198
|
+
const STRIP_LEADINS = [
|
|
199
|
+
// ── Tech stack — proven lossless abbreviations ─────────────────────────────
|
|
200
|
+
// "App Router" is the only routing mode in Next.js 14 — implied by version
|
|
201
|
+
[/Next\.js 14 App Router/gi, 'Next.js 14'],
|
|
202
|
+
// "Node.js/" prefix is implicit in Express
|
|
203
|
+
[/Node\.js\/Express/gi, 'Express'],
|
|
204
|
+
// Restructure to equivalent dense form
|
|
205
|
+
[/PostgreSQL via Prisma ORM/gi, 'Prisma + PostgreSQL'],
|
|
206
|
+
// "workspaces" is PNPM's default monorepo mode — implied
|
|
207
|
+
[/PNPM workspaces \+ TurboRepo/gi, 'PNPM + TurboRepo'],
|
|
208
|
+
// "published as X on npm" → just the package name (it IS the npm identity)
|
|
209
|
+
[/published as (@torqon\/mcp) on npm/gi, '$1'],
|
|
210
|
+
// ── Auth flow prose — keeps all facts, removes "User goes through", "Or signs in with", etc.
|
|
211
|
+
[/Or signs in with Google OAuth \(Google creates\/upserts user in Postgres\)/gi,
|
|
212
|
+
'Google OAuth: creates/upserts user in Postgres'],
|
|
213
|
+
[/User goes through onboarding[^,\n]*,\s*copies MCP install command/gi,
|
|
214
|
+
'Onboarding: set workspace name, copy install command'],
|
|
215
|
+
[/NextAuth creates JWT session,/gi, 'NextAuth JWT session,'],
|
|
216
|
+
[/Onboarding marks onboardingDone:/gi, 'onboardingDone:'],
|
|
217
|
+
// ── Key flow prose — keeps all facts
|
|
218
|
+
[/On signup\/Google login, Railway \/auth\/register is called/gi,
|
|
219
|
+
'signup/Google login → Railway /auth/register'],
|
|
220
|
+
[/Returns an API key tied to the user's email/gi, "API key tied to user's email"],
|
|
221
|
+
[/Key is stored in Postgres, shown once in onboarding/gi, 'Stored in Postgres, shown once in onboarding'],
|
|
222
|
+
[/Dashboard (shows|lets user see)/gi, 'Dashboard:'],
|
|
223
|
+
[/Dashboard lets user (create additional named keys,? ?revoke any key)/gi,
|
|
224
|
+
'Dashboard: create named keys, revoke any key'],
|
|
225
|
+
// ── Issue descriptions — keep error name + cause + status, drop verbose fix detail
|
|
226
|
+
// Use [—–\-]+ to match em dash, en dash, or ASCII hyphen variants
|
|
227
|
+
[/Google OAuth: was returning Access Denied due to signIn callback blocking on Railway API call[^\n]*/gi,
|
|
228
|
+
'Google OAuth: Access Denied — signIn callback blocked Railway — fixed'],
|
|
229
|
+
[/[—–\-]+ fixed by moving user upsert to jwt callback/gi, '— fixed'],
|
|
230
|
+
[/[—–\-]+ fixed and pushed/gi, '— fixed'],
|
|
231
|
+
[/[—–\-]+ mitigated by removing PrismaAdapter, using JWT only/gi, '— mitigated (no PrismaAdapter, JWT-only)'],
|
|
232
|
+
[/ fixed by moving user upsert to jwt callback/gi, ' fixed'],
|
|
233
|
+
[/ fixed and pushed/gi, ' fixed'],
|
|
234
|
+
[/pnpm lockfile: was out of sync/gi, 'pnpm lockfile: out of sync'],
|
|
235
|
+
// ── Business context prose — keeps all facts, strips scaffolding words
|
|
236
|
+
[/Target users: developers who use Claude heavily/gi, 'Target: heavy Claude users'],
|
|
237
|
+
[/Core pain point: repeating context every session wastes tokens and time/gi,
|
|
238
|
+
'Pain: repeating context wastes tokens/time'],
|
|
239
|
+
[/Differentiator: MCP-native, works inside Claude without copy-pasting/gi,
|
|
240
|
+
'Differentiator: MCP-native, no copy-pasting'],
|
|
241
|
+
[/Current status:/gi, 'Status:'],
|
|
242
|
+
[/User signs up at ([\w./:-]+) with name \+ email \+ password/gi,
|
|
243
|
+
'Email signup at $1'],
|
|
244
|
+
[/Google OAuth being debugged/gi, 'Google OAuth fixed'],
|
|
245
|
+
// ── Connective filler (keeps the actual values intact) ────────────────────
|
|
246
|
+
[/deployed on Vercel at /gi, 'Vercel: '],
|
|
247
|
+
[/deployed on Vercel/gi, 'Vercel'],
|
|
248
|
+
[/ on Railway at /gi, ', Railway: '],
|
|
249
|
+
[/^on Railway at /gim, 'Railway: '],
|
|
250
|
+
[/connected to GitHub repo /gi, 'GitHub: '],
|
|
251
|
+
[/Railway: auto-deploys from its own repo, hosts the backend API/gi, 'Railway backend'],
|
|
252
|
+
[/auto-deploys from its own repo, hosts the backend API/gi, 'Railway backend'],
|
|
253
|
+
// Prose: "mitigated by removing PrismaAdapter, using JWT only" without em-dash prefix
|
|
254
|
+
[/mitigated by removing PrismaAdapter, using JWT only/gi, 'mitigated (no PrismaAdapter, JWT-only)'],
|
|
255
|
+
[/Vercel project:/gi, 'Vercel:'],
|
|
256
|
+
[/Environment vars on Vercel:/gi, 'Env vars:'],
|
|
257
|
+
[/environment vars on Vercel:/gi, 'Env vars:'],
|
|
258
|
+
// ── Feature list lead-ins (tool name follows and is kept) ─────────────────
|
|
259
|
+
[/Store atomic memory facts via MCP tool:\s*/gi, ''],
|
|
260
|
+
[/Retrieve relevant context via:\s*/gi, ''],
|
|
261
|
+
[/Optimize long prompts via:\s*/gi, ''],
|
|
262
|
+
[/Auto-process conversations via:\s*/gi, ''],
|
|
263
|
+
// ── Minor annotations ──────────────────────────────────────────────────────
|
|
264
|
+
[/\s*\(idempotent\)/gi, ''],
|
|
265
|
+
[/No emojis anywhere in the product/gi, 'No emojis'],
|
|
266
|
+
];
|
|
267
|
+
// Section heading → compact tag
|
|
268
|
+
const SECTION_TAGS = [
|
|
269
|
+
[/technical arch/i, 'Stack'],
|
|
270
|
+
[/current feat/i, 'Features'],
|
|
271
|
+
[/auth flow/i, 'Auth Flow'],
|
|
272
|
+
[/api key flow/i, 'Key Flow'],
|
|
273
|
+
[/mcp install/i, 'Install'],
|
|
274
|
+
[/pricing/i, 'Pricing'],
|
|
275
|
+
[/business/i, 'Business'],
|
|
276
|
+
[/known issues/i, 'Issues'],
|
|
277
|
+
[/deployment/i, 'Deploy'],
|
|
278
|
+
[/design system/i, 'Design'],
|
|
279
|
+
];
|
|
280
|
+
function sectionTag(heading) {
|
|
281
|
+
for (const [pat, tag] of SECTION_TAGS) {
|
|
282
|
+
if (pat.test(heading))
|
|
283
|
+
return tag;
|
|
284
|
+
}
|
|
285
|
+
return heading.replace(/:$/, '').trim();
|
|
286
|
+
}
|
|
287
|
+
function isHeading(line) {
|
|
288
|
+
const l = line.trim();
|
|
289
|
+
return (/^#{1,3}\s/.test(l) ||
|
|
290
|
+
/^[A-Z][A-Z\s\-_\/]{3,}:$/.test(l) ||
|
|
291
|
+
/^[A-Z][A-Z\s\-_\/]{3,}$/.test(l) ||
|
|
292
|
+
(l.endsWith(':') && l.length < 60 && !/[a-z]{5,}/.test(l)));
|
|
293
|
+
}
|
|
294
|
+
function cleanLine(line) {
|
|
295
|
+
let l = line.trim();
|
|
296
|
+
// Strip list/numbered prefixes
|
|
297
|
+
l = l.replace(/^(\d+\.\s*|[-*•]\s*)/i, '');
|
|
298
|
+
// Apply lead-in removals
|
|
299
|
+
for (const [pat, rep] of STRIP_LEADINS)
|
|
300
|
+
l = l.replace(pat, rep);
|
|
301
|
+
// Collapse extra spaces
|
|
302
|
+
return l.replace(/\s{2,}/g, ' ').trim();
|
|
303
|
+
}
|
|
194
304
|
function compressText(raw) {
|
|
195
305
|
const lines = raw.split('\n');
|
|
196
|
-
const facts = [];
|
|
197
|
-
const seen = new Set();
|
|
198
|
-
// Filler phrases to strip from lines
|
|
199
|
-
const fillerPatterns = [
|
|
200
|
-
/\bplease note that\b/gi,
|
|
201
|
-
/\bit is (important|worth noting|essential) (to note |that )?/gi,
|
|
202
|
-
/\bas (mentioned|noted|discussed) (above|before|earlier|previously)\b/gi,
|
|
203
|
-
/\bin (this|the) (context|case|situation|scenario)\b/gi,
|
|
204
|
-
/\bbasically\b/gi,
|
|
205
|
-
/\bessentially\b/gi,
|
|
206
|
-
/\bfundamentally\b/gi,
|
|
207
|
-
/\bof course\b/gi,
|
|
208
|
-
/\bneedless to say\b/gi,
|
|
209
|
-
/\bit goes without saying\b/gi,
|
|
210
|
-
/\bfor (all intents and purposes|the most part)\b/gi,
|
|
211
|
-
/\bin order to\b/gi, // → "to"
|
|
212
|
-
/\bdue to the fact that\b/gi, // → "because"
|
|
213
|
-
/\bat this point in time\b/gi, // → "now"
|
|
214
|
-
/\bthe fact that\b/gi,
|
|
215
|
-
/\bwhat this means is\b/gi,
|
|
216
|
-
/\bwhat we (can|need to) (see|do|understand) (is|here)?\b/gi,
|
|
217
|
-
];
|
|
218
|
-
// Verbose phrase replacements
|
|
219
|
-
const replacements = [
|
|
220
|
-
[/in order to/gi, 'to'],
|
|
221
|
-
[/due to the fact that/gi, 'because'],
|
|
222
|
-
[/at this point in time/gi, 'now'],
|
|
223
|
-
[/a large number of/gi, 'many'],
|
|
224
|
-
[/a majority of/gi, 'most'],
|
|
225
|
-
[/is able to/gi, 'can'],
|
|
226
|
-
[/is going to/gi, 'will'],
|
|
227
|
-
[/make sure (to|that)/gi, 'ensure'],
|
|
228
|
-
[/take into account/gi, 'consider'],
|
|
229
|
-
[/with (the )?regard(s)? to/gi, 'regarding'],
|
|
230
|
-
[/in the event that/gi, 'if'],
|
|
231
|
-
[/on a daily basis/gi, 'daily'],
|
|
232
|
-
[/on a weekly basis/gi, 'weekly'],
|
|
233
|
-
[/on a monthly basis/gi, 'monthly'],
|
|
234
|
-
];
|
|
235
|
-
for (const line of lines) {
|
|
236
|
-
let l = line.trim();
|
|
237
|
-
if (!l)
|
|
238
|
-
continue;
|
|
239
|
-
// Skip pure decoration lines
|
|
240
|
-
if (/^[-=*#]{3,}$/.test(l))
|
|
241
|
-
continue;
|
|
242
|
-
// Skip lines that are just whitespace or single chars
|
|
243
|
-
if (l.length < 3)
|
|
244
|
-
continue;
|
|
245
|
-
// Apply replacements
|
|
246
|
-
for (const [pattern, replacement] of replacements) {
|
|
247
|
-
l = l.replace(pattern, replacement);
|
|
248
|
-
}
|
|
249
|
-
// Strip filler phrases
|
|
250
|
-
for (const pattern of fillerPatterns) {
|
|
251
|
-
l = l.replace(pattern, '');
|
|
252
|
-
}
|
|
253
|
-
// Collapse multiple spaces
|
|
254
|
-
l = l.replace(/\s{2,}/g, ' ').trim();
|
|
255
|
-
// Skip if now too short
|
|
256
|
-
if (l.length < 4)
|
|
257
|
-
continue;
|
|
258
|
-
// Deduplication — normalize for comparison
|
|
259
|
-
const normalized = l.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
260
|
-
if (seen.has(normalized))
|
|
261
|
-
continue;
|
|
262
|
-
seen.add(normalized);
|
|
263
|
-
facts.push(l);
|
|
264
|
-
}
|
|
265
|
-
// Group lines under their nearest heading
|
|
266
306
|
const sections = [];
|
|
267
|
-
let
|
|
268
|
-
for (const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
currentSection = { heading: fact.replace(/^#+\s*/, '').replace(/:$/, ''), items: [] };
|
|
307
|
+
let cur = { heading: '', items: [] };
|
|
308
|
+
for (const rawLine of lines) {
|
|
309
|
+
const line = rawLine.trim();
|
|
310
|
+
if (!line || /^[-=*]{4,}$/.test(line))
|
|
311
|
+
continue;
|
|
312
|
+
if (isHeading(line)) {
|
|
313
|
+
if (cur.heading || cur.items.length)
|
|
314
|
+
sections.push(cur);
|
|
315
|
+
cur = { heading: sectionTag(line), items: [] };
|
|
278
316
|
}
|
|
279
317
|
else {
|
|
280
|
-
|
|
318
|
+
const cleaned = cleanLine(line);
|
|
319
|
+
if (cleaned.length < 3)
|
|
320
|
+
continue;
|
|
321
|
+
cur.items.push(cleaned);
|
|
281
322
|
}
|
|
282
323
|
}
|
|
283
|
-
if (
|
|
284
|
-
sections.push(
|
|
285
|
-
|
|
286
|
-
|
|
324
|
+
if (cur.heading || cur.items.length)
|
|
325
|
+
sections.push(cur);
|
|
326
|
+
// Global dedup by normalized key
|
|
327
|
+
const seen = new Set();
|
|
328
|
+
const dedup = (items) => items.filter(item => {
|
|
329
|
+
const key = item.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 60);
|
|
330
|
+
if (seen.has(key))
|
|
331
|
+
return false;
|
|
332
|
+
seen.add(key);
|
|
333
|
+
return true;
|
|
334
|
+
});
|
|
287
335
|
const out = [];
|
|
288
336
|
for (const section of sections) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
out.push(`${prefix}${item.replace(/^[-*]\s*/, '')}`);
|
|
296
|
-
}
|
|
297
|
-
if (section.items.length > 0)
|
|
298
|
-
out.push('');
|
|
337
|
+
const items = dedup(section.items);
|
|
338
|
+
if (!items.length)
|
|
339
|
+
continue;
|
|
340
|
+
// Pack all items on one line with | separator
|
|
341
|
+
const packed = items.join(' | ');
|
|
342
|
+
out.push(section.heading ? `[${section.heading}] ${packed}` : packed);
|
|
299
343
|
}
|
|
300
|
-
return out.join('\n')
|
|
344
|
+
return out.join('\n');
|
|
301
345
|
}
|
|
302
346
|
server.tool('optimize_context', 'Compresses a large document or long prompt into a dense token-efficient format. Strips filler, deduplicates, restructures. Use before feeding large text to reduce tokens.', {
|
|
303
347
|
message: z.string().describe('The raw text to compress'),
|
package/dist/install.js
CHANGED
|
@@ -10,65 +10,113 @@ function getConfigPath(client) {
|
|
|
10
10
|
if (os === 'win32') {
|
|
11
11
|
const windowsStorePath = `${process.env.LOCALAPPDATA}\\Packages\\Claude_pzs8sxrjxfjjc\\LocalCache\\Roaming\\Claude\\claude_desktop_config.json`;
|
|
12
12
|
if (existsSync(windowsStorePath))
|
|
13
|
-
return windowsStorePath;
|
|
14
|
-
return `${process.env.APPDATA}\\Claude\\claude_desktop_config.json
|
|
13
|
+
return { path: windowsStorePath, format: 'json' };
|
|
14
|
+
return { path: `${process.env.APPDATA}\\Claude\\claude_desktop_config.json`, format: 'json' };
|
|
15
15
|
}
|
|
16
16
|
if (os === 'darwin')
|
|
17
|
-
return join(home, 'Library/Application Support/Claude/claude_desktop_config.json');
|
|
18
|
-
return join(home, '.config/Claude/claude_desktop_config.json');
|
|
17
|
+
return { path: join(home, 'Library/Application Support/Claude/claude_desktop_config.json'), format: 'json' };
|
|
18
|
+
return { path: join(home, '.config/Claude/claude_desktop_config.json'), format: 'json' };
|
|
19
19
|
}
|
|
20
20
|
if (client === 'cursor') {
|
|
21
21
|
if (os === 'win32')
|
|
22
|
-
return `${process.env.APPDATA}\\Cursor\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json
|
|
22
|
+
return { path: `${process.env.APPDATA}\\Cursor\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json`, format: 'json' };
|
|
23
23
|
if (os === 'darwin')
|
|
24
|
-
return join(home, 'Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json');
|
|
25
|
-
return join(home, '.config/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json');
|
|
24
|
+
return { path: join(home, 'Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), format: 'json' };
|
|
25
|
+
return { path: join(home, '.config/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), format: 'json' };
|
|
26
26
|
}
|
|
27
27
|
if (client === 'windsurf') {
|
|
28
28
|
if (os === 'win32')
|
|
29
|
-
return `${process.env.APPDATA}\\Windsurf\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json
|
|
29
|
+
return { path: `${process.env.APPDATA}\\Windsurf\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json`, format: 'json' };
|
|
30
30
|
if (os === 'darwin')
|
|
31
|
-
return join(home, 'Library/Application Support/Windsurf/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json');
|
|
32
|
-
return join(home, '.config/Windsurf/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json');
|
|
31
|
+
return { path: join(home, 'Library/Application Support/Windsurf/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), format: 'json' };
|
|
32
|
+
return { path: join(home, '.config/Windsurf/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), format: 'json' };
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
if (client === 'codex') {
|
|
35
|
+
if (os === 'win32')
|
|
36
|
+
return { path: `${process.env.USERPROFILE}\\.codex\\config.toml`, format: 'toml' };
|
|
37
|
+
return { path: join(home, '.codex', 'config.toml'), format: 'toml' };
|
|
38
|
+
}
|
|
39
|
+
if (client === 'zed') {
|
|
40
|
+
if (os === 'win32')
|
|
41
|
+
return { path: `${process.env.APPDATA}\\Zed\\settings.json`, format: 'json' };
|
|
42
|
+
if (os === 'darwin')
|
|
43
|
+
return { path: join(home, 'Library/Application Support/Zed/settings.json'), format: 'json' };
|
|
44
|
+
return { path: join(home, '.config/zed/settings.json'), format: 'json' };
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Unknown client: ${client}. Supported: claude, cursor, windsurf, codex, zed`);
|
|
35
47
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const configDir = configPath.substring(0, configPath.lastIndexOf('\\') || configPath.lastIndexOf('/'));
|
|
40
|
-
if (!existsSync(configDir)) {
|
|
48
|
+
function installJson(client, configPath, apiKey, apiUrl) {
|
|
49
|
+
const configDir = configPath.substring(0, Math.max(configPath.lastIndexOf('\\'), configPath.lastIndexOf('/')));
|
|
50
|
+
if (!existsSync(configDir))
|
|
41
51
|
mkdirSync(configDir, { recursive: true });
|
|
42
|
-
}
|
|
43
52
|
let config = {};
|
|
44
53
|
if (existsSync(configPath)) {
|
|
45
54
|
try {
|
|
46
55
|
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
47
56
|
}
|
|
48
57
|
catch {
|
|
49
|
-
console.warn('⚠️ Existing config could not be parsed
|
|
58
|
+
console.warn('⚠️ Existing config could not be parsed — starting fresh.');
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
61
|
if (!config.mcpServers)
|
|
53
62
|
config.mcpServers = {};
|
|
54
63
|
config.mcpServers.torqon = {
|
|
55
64
|
command: 'npx',
|
|
56
|
-
args: ['-y', '@torqon/mcp'],
|
|
65
|
+
args: ['-y', '@torqon/mcp@latest'],
|
|
57
66
|
env: {
|
|
67
|
+
TORQON_API_KEY: apiKey,
|
|
58
68
|
TORQON_API_URL: apiUrl,
|
|
59
69
|
},
|
|
60
70
|
};
|
|
61
71
|
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
72
|
+
}
|
|
73
|
+
function installToml(configPath, apiKey, apiUrl) {
|
|
74
|
+
const configDir = configPath.substring(0, Math.max(configPath.lastIndexOf('\\'), configPath.lastIndexOf('/')));
|
|
75
|
+
if (!existsSync(configDir))
|
|
76
|
+
mkdirSync(configDir, { recursive: true });
|
|
77
|
+
// Read existing TOML or start empty
|
|
78
|
+
let existing = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
|
|
79
|
+
// Remove any existing [mcp_servers.torqon] block
|
|
80
|
+
existing = existing.replace(/\[mcp_servers\.torqon\][\s\S]*?(?=\[|\s*$)/g, '').trimEnd();
|
|
81
|
+
const block = `\n\n[mcp_servers.torqon]\ncommand = "npx"\nargs = ["-y", "@torqon/mcp@latest"]\nenv = { TORQON_API_KEY = "${apiKey}", TORQON_API_URL = "${apiUrl}" }\n`;
|
|
82
|
+
writeFileSync(configPath, (existing + block).trimStart(), 'utf8');
|
|
83
|
+
}
|
|
84
|
+
function install(client, apiKey, apiUrl) {
|
|
85
|
+
console.log(`\n🔧 Installing Torqon MCP for ${client}...\n`);
|
|
86
|
+
const { path: configPath, format } = getConfigPath(client);
|
|
87
|
+
if (format === 'toml') {
|
|
88
|
+
installToml(configPath, apiKey, apiUrl);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
installJson(client, configPath, apiKey, apiUrl);
|
|
92
|
+
}
|
|
62
93
|
console.log(`✅ Torqon MCP installed for ${client}`);
|
|
63
94
|
console.log(`📁 Config updated: ${configPath}`);
|
|
64
|
-
console.log(`🌐 API URL: ${apiUrl}`);
|
|
65
95
|
console.log(`\n👉 Restart ${client} to activate Torqon.\n`);
|
|
66
96
|
}
|
|
67
|
-
// Parse args
|
|
97
|
+
// Parse args — supports two formats:
|
|
98
|
+
// add <client> --api-key <key> (new, from Connect page)
|
|
99
|
+
// --client=<client> --api-url=<url> (legacy)
|
|
68
100
|
const args = process.argv.slice(2);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
101
|
+
let clientArg;
|
|
102
|
+
let apiKeyArg;
|
|
103
|
+
let apiUrlArg;
|
|
104
|
+
if (args[0] === 'add') {
|
|
105
|
+
// new format: add cursor --api-key tq-xxx
|
|
106
|
+
clientArg = args[1] ?? 'claude';
|
|
107
|
+
const keyIdx = args.indexOf('--api-key');
|
|
108
|
+
apiKeyArg = keyIdx !== -1 ? args[keyIdx + 1] ?? '' : '';
|
|
109
|
+
const urlIdx = args.indexOf('--api-url');
|
|
110
|
+
apiUrlArg = urlIdx !== -1 ? args[urlIdx + 1] ?? TORQON_API_URL : TORQON_API_URL;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// legacy format: --client=cursor --api-url=https://...
|
|
114
|
+
clientArg = args.find(a => a.startsWith('--client='))?.split('=')[1] ?? 'claude';
|
|
115
|
+
apiKeyArg = args.find(a => a.startsWith('--api-key='))?.split('=')[1] ?? process.env.TORQON_API_KEY ?? '';
|
|
116
|
+
apiUrlArg = args.find(a => a.startsWith('--api-url='))?.split('=')[1] ?? TORQON_API_URL;
|
|
117
|
+
}
|
|
118
|
+
if (!apiKeyArg) {
|
|
119
|
+
console.error('\n✗ No API key provided. Use --api-key <your-key>\n');
|
|
120
|
+
process.exit(1);
|
|
73
121
|
}
|
|
74
|
-
install(clientArg,
|
|
122
|
+
install(clientArg, apiKeyArg, apiUrlArg);
|
package/package.json
CHANGED