trapic-mcp 0.1.0

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/bin/trapic-mcp.mjs +902 -0
  4. package/bin/wrapper.sh +5 -0
  5. package/dist/archive.d.ts +7 -0
  6. package/dist/archive.js +116 -0
  7. package/dist/audit.d.ts +5 -0
  8. package/dist/audit.js +16 -0
  9. package/dist/background.d.ts +8 -0
  10. package/dist/background.js +17 -0
  11. package/dist/config.d.ts +46 -0
  12. package/dist/config.js +20 -0
  13. package/dist/conflict.d.ts +14 -0
  14. package/dist/conflict.js +103 -0
  15. package/dist/embedding.d.ts +6 -0
  16. package/dist/embedding.js +74 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +104 -0
  19. package/dist/llm.d.ts +10 -0
  20. package/dist/llm.js +47 -0
  21. package/dist/ollama.d.ts +11 -0
  22. package/dist/ollama.js +63 -0
  23. package/dist/quota.d.ts +7 -0
  24. package/dist/quota.js +16 -0
  25. package/dist/rate-limit.d.ts +5 -0
  26. package/dist/rate-limit.js +38 -0
  27. package/dist/request-context.d.ts +3 -0
  28. package/dist/request-context.js +12 -0
  29. package/dist/supabase.d.ts +2 -0
  30. package/dist/supabase.js +16 -0
  31. package/dist/team-access.d.ts +5 -0
  32. package/dist/team-access.js +35 -0
  33. package/dist/tools/active.d.ts +2 -0
  34. package/dist/tools/active.js +63 -0
  35. package/dist/tools/assert.d.ts +3 -0
  36. package/dist/tools/assert.js +141 -0
  37. package/dist/tools/chain.d.ts +2 -0
  38. package/dist/tools/chain.js +118 -0
  39. package/dist/tools/context.d.ts +7 -0
  40. package/dist/tools/context.js +270 -0
  41. package/dist/tools/create.d.ts +2 -0
  42. package/dist/tools/create.js +126 -0
  43. package/dist/tools/extract.d.ts +2 -0
  44. package/dist/tools/extract.js +95 -0
  45. package/dist/tools/preload.d.ts +10 -0
  46. package/dist/tools/preload.js +112 -0
  47. package/dist/tools/search.d.ts +2 -0
  48. package/dist/tools/search.js +92 -0
  49. package/dist/tools/summary.d.ts +2 -0
  50. package/dist/tools/summary.js +176 -0
  51. package/dist/tools/update.d.ts +2 -0
  52. package/dist/tools/update.js +134 -0
  53. package/dist/worker.d.ts +15 -0
  54. package/dist/worker.js +700 -0
  55. package/package.json +59 -0
@@ -0,0 +1,902 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, resolve, join } from "path";
4
+ import { readFile, writeFile, access, mkdir } from "fs/promises";
5
+ import { createInterface } from "readline/promises";
6
+ import { homedir } from "os";
7
+ import { appendFileSync, mkdirSync } from "fs";
8
+
9
+ // Immediate boot log — written before anything else runs
10
+ try {
11
+ const logDir = join(homedir(), ".trapic-mcp");
12
+ mkdirSync(logDir, { recursive: true });
13
+ appendFileSync(join(logDir, "boot.log"),
14
+ `${new Date().toISOString()} BOOT pid=${process.pid} argv=${JSON.stringify(process.argv)} TRAPIC_TOKEN=${process.env.TRAPIC_TOKEN ? process.env.TRAPIC_TOKEN.slice(0,8)+"..." : "UNSET"}\n`
15
+ );
16
+ } catch(e) { /* ignore */ }
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+
20
+ const CLAUDE_MD_CONTENT = `# Trapic MCP
21
+
22
+ ## Auto-Trace
23
+
24
+ When the trapic-mcp MCP server is connected, automatically call \`trace_create\` to record important knowledge during conversations:
25
+
26
+ **When to create traces:**
27
+ - Decisions made (e.g. "chose Vite over Next.js for static deployment")
28
+ - Problems discovered and their root causes
29
+ - Architecture or design conclusions
30
+ - Bug fixes and why they worked
31
+ - Important user preferences or project constraints learned
32
+
33
+ **How to create traces:**
34
+ - \`claim\`: One clear sentence describing what happened or was decided
35
+ - \`reason\`: Why this decision was made or what caused it
36
+ - \`tags\`: **Always include the current project directory name as the first tag**, then add relevant tags (technology, category)
37
+ - \`confidence\`: "high" for verified facts, "medium" for likely conclusions, "low" for hypotheses
38
+ - \`source\`: "session" for conversation-derived traces
39
+
40
+ **When NOT to create traces:**
41
+ - Trivial operations (reading files, listing directories)
42
+ - Intermediate steps that don't produce conclusions
43
+ - Information already recorded in a previous trace
44
+
45
+ Keep traces minimal and high-signal. Quality over quantity.
46
+
47
+ **After creating a trace:**
48
+ - Do NOT announce or summarize the trace to the user
49
+ - Silently continue the conversation as if nothing happened
50
+ - Traces are background knowledge capture, not user-facing actions
51
+ `;
52
+
53
+ const DEFAULT_URL = "https://mcp.trapic.ai/mcp";
54
+ const CRED_DIR = join(homedir(), ".trapic-mcp");
55
+ const CRED_FILE = join(CRED_DIR, "credentials.json");
56
+
57
+ // ── Helpers ──────────────────────────────────────────────────────────────────
58
+
59
+ async function fileExists(path) {
60
+ try {
61
+ await access(path);
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ async function readJson(path) {
69
+ const raw = await readFile(path, "utf-8");
70
+ return JSON.parse(raw);
71
+ }
72
+
73
+ async function writeJson(path, data) {
74
+ await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
75
+ }
76
+
77
+ function baseUrl(mcpUrl) {
78
+ return mcpUrl.replace(/\/mcp$/, "");
79
+ }
80
+
81
+ // ── Credentials ──────────────────────────────────────────────────────────────
82
+
83
+ async function loadCredentials() {
84
+ if (await fileExists(CRED_FILE)) {
85
+ try {
86
+ return await readJson(CRED_FILE);
87
+ } catch {}
88
+ }
89
+ return null;
90
+ }
91
+
92
+ async function saveCredentials(data) {
93
+ await mkdir(CRED_DIR, { recursive: true });
94
+ await writeJson(CRED_FILE, data);
95
+ }
96
+
97
+ async function clearCredentials() {
98
+ if (await fileExists(CRED_FILE)) {
99
+ await writeFile(CRED_FILE, "{}\n", "utf-8");
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Load config from: credentials file → .mcp.json → ~/.claude/settings.json
105
+ */
106
+ async function loadConfig() {
107
+ // Try CLI credentials first
108
+ const creds = await loadCredentials();
109
+ if (creds?.url && creds?.token) {
110
+ return { url: creds.url, token: creds.token };
111
+ }
112
+
113
+ // Try project-level .mcp.json
114
+ const projectPath = resolve(process.cwd(), ".mcp.json");
115
+ if (await fileExists(projectPath)) {
116
+ try {
117
+ const cfg = await readJson(projectPath);
118
+ const entry = cfg?.mcpServers?.["trapic-mcp"];
119
+ if (entry) {
120
+ const url = entry.url;
121
+ const authHeader = entry.headers?.Authorization || "";
122
+ const token = authHeader.replace(/^Bearer\s+/i, "");
123
+ if (url && token) return { url, token };
124
+ }
125
+ } catch {}
126
+ }
127
+
128
+ // Try global ~/.claude/settings.json
129
+ const globalPath = join(homedir(), ".claude", "settings.json");
130
+ if (await fileExists(globalPath)) {
131
+ try {
132
+ const cfg = await readJson(globalPath);
133
+ const entry = cfg?.mcpServers?.["trapic-mcp"];
134
+ if (entry) {
135
+ const url = entry.url;
136
+ const authHeader = entry.headers?.Authorization || "";
137
+ const token = authHeader.replace(/^Bearer\s+/i, "");
138
+ if (url && token) return { url, token };
139
+ }
140
+ } catch {}
141
+ }
142
+
143
+ return null;
144
+ }
145
+
146
+ /**
147
+ * Require config or exit with error.
148
+ */
149
+ async function requireConfig() {
150
+ const config = await loadConfig();
151
+ if (!config) {
152
+ console.error("Not logged in. Run 'trapic-mcp login' first.");
153
+ process.exit(1);
154
+ }
155
+ return config;
156
+ }
157
+
158
+ // ── Password input ───────────────────────────────────────────────────────────
159
+
160
+ function readPassword(prompt) {
161
+ return new Promise((resolve) => {
162
+ process.stdout.write(prompt);
163
+ if (!process.stdin.isTTY) {
164
+ // Non-interactive: read line normally
165
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
166
+ rl.question("").then((answer) => {
167
+ rl.close();
168
+ resolve(answer);
169
+ });
170
+ return;
171
+ }
172
+ process.stdin.setRawMode(true);
173
+ process.stdin.resume();
174
+ process.stdin.setEncoding("utf-8");
175
+ let password = "";
176
+ const onData = (char) => {
177
+ if (char === "\n" || char === "\r" || char === "\u0004") {
178
+ process.stdin.setRawMode(false);
179
+ process.stdin.pause();
180
+ process.stdin.removeListener("data", onData);
181
+ process.stdout.write("\n");
182
+ resolve(password);
183
+ } else if (char === "\u0003") {
184
+ process.stdin.setRawMode(false);
185
+ process.stdout.write("\n");
186
+ process.exit(0);
187
+ } else if (char === "\u007F" || char === "\b") {
188
+ if (password.length > 0) {
189
+ password = password.slice(0, -1);
190
+ process.stdout.write("\b \b");
191
+ }
192
+ } else {
193
+ password += char;
194
+ process.stdout.write("*");
195
+ }
196
+ };
197
+ process.stdin.on("data", onData);
198
+ });
199
+ }
200
+
201
+ // ── HTTP helpers ─────────────────────────────────────────────────────────────
202
+
203
+ async function authRequest(method, path, token, body = null) {
204
+ const config = token
205
+ ? { url: path, token }
206
+ : await requireConfig();
207
+
208
+ if (!token) {
209
+ const base = baseUrl(config.url);
210
+ path = `${base}${path}`;
211
+ token = config.token;
212
+ }
213
+
214
+ const opts = {
215
+ method,
216
+ headers: {
217
+ "Content-Type": "application/json",
218
+ Authorization: `Bearer ${token}`,
219
+ },
220
+ };
221
+ if (body) opts.body = JSON.stringify(body);
222
+
223
+ const res = await fetch(path, opts);
224
+ const data = await res.json();
225
+ if (!res.ok) {
226
+ throw new Error(data.error || `HTTP ${res.status}`);
227
+ }
228
+ return data;
229
+ }
230
+
231
+ /**
232
+ * Send an MCP JSON-RPC request (initialize + tool call).
233
+ */
234
+ async function mcpToolCall(toolName, toolArgs = {}) {
235
+ const config = await requireConfig();
236
+ const { url, token } = config;
237
+
238
+ const rpc = (method, params, id) =>
239
+ fetch(url, {
240
+ method: "POST",
241
+ headers: {
242
+ "Content-Type": "application/json",
243
+ Authorization: `Bearer ${token}`,
244
+ },
245
+ body: JSON.stringify({ jsonrpc: "2.0", id, method, params }),
246
+ }).then((r) => r.json());
247
+
248
+ // Initialize session
249
+ await rpc(
250
+ "initialize",
251
+ {
252
+ protocolVersion: "2025-03-26",
253
+ capabilities: {},
254
+ clientInfo: { name: "trapic-mcp-cli", version: "1.0.0" },
255
+ },
256
+ 1
257
+ );
258
+
259
+ // Call tool
260
+ const result = await rpc(
261
+ "tools/call",
262
+ { name: toolName, arguments: toolArgs },
263
+ 2
264
+ );
265
+
266
+ const content = result?.result?.content;
267
+ if (!content?.length) return null;
268
+
269
+ const textBlock = content.find((c) => c.type === "text");
270
+ if (!textBlock) return null;
271
+
272
+ try {
273
+ return JSON.parse(textBlock.text);
274
+ } catch {
275
+ return textBlock.text;
276
+ }
277
+ }
278
+
279
+ // ── Formatting helpers ───────────────────────────────────────────────────────
280
+
281
+ function fmtDate(iso) {
282
+ if (!iso) return "—";
283
+ return new Date(iso).toLocaleDateString("en-CA"); // YYYY-MM-DD
284
+ }
285
+
286
+ function fmtRelative(iso) {
287
+ if (!iso) return "never";
288
+ const diff = Date.now() - new Date(iso).getTime();
289
+ const mins = Math.floor(diff / 60000);
290
+ if (mins < 1) return "just now";
291
+ if (mins < 60) return `${mins}m ago`;
292
+ const hours = Math.floor(mins / 60);
293
+ if (hours < 24) return `${hours}h ago`;
294
+ const days = Math.floor(hours / 24);
295
+ return `${days}d ago`;
296
+ }
297
+
298
+ function truncate(str, max = 60) {
299
+ if (!str || str.length <= max) return str || "";
300
+ return str.slice(0, max - 1) + "…";
301
+ }
302
+
303
+ // ── Commands ─────────────────────────────────────────────────────────────────
304
+
305
+ async function cmdServe() {
306
+ // Debug log file
307
+ const debugLog = join(homedir(), ".trapic-mcp", "debug.log");
308
+ await mkdir(dirname(debugLog), { recursive: true }).catch(() => {});
309
+ const logMsg = (msg) => {
310
+ const ts = new Date().toISOString();
311
+ writeFile(debugLog, `${ts} ${msg}\n`, { flag: "a" }).catch(() => {});
312
+ process.stderr.write(`[trapic-mcp] ${msg}\n`);
313
+ };
314
+
315
+ // Load token from env or credentials file
316
+ const token = process.env.TRAPIC_TOKEN || (await loadConfig())?.token;
317
+ const url = process.env.TRAPIC_URL || (await loadConfig())?.url || DEFAULT_URL;
318
+
319
+ logMsg(`serve mode starting, url=${url}, token=${token ? token.slice(0, 8) + "..." : "NONE"}`);
320
+ logMsg(`TRAPIC_TOKEN env: ${process.env.TRAPIC_TOKEN ? process.env.TRAPIC_TOKEN.slice(0, 8) + "..." : "UNSET"}`);
321
+
322
+ if (!token) {
323
+ process.stderr.write(
324
+ "Error: No auth token found.\n" +
325
+ "Set TRAPIC_TOKEN env var or run 'npx trapic-mcp login' first.\n"
326
+ );
327
+ process.exit(1);
328
+ }
329
+
330
+ // Stdio proxy: read JSON-RPC from stdin, forward to remote server, write to stdout
331
+ const rl = createInterface({ input: process.stdin });
332
+ let sessionId = null;
333
+
334
+ rl.on("line", async (line) => {
335
+ if (!line.trim()) return;
336
+ try {
337
+ const message = JSON.parse(line);
338
+ logMsg(`→ ${message.method || "response"} (id=${message.id})`);
339
+ const headers = {
340
+ "Content-Type": "application/json",
341
+ "Accept": "application/json, text/event-stream",
342
+ "Authorization": `Bearer ${token}`,
343
+ };
344
+ if (sessionId) headers["mcp-session-id"] = sessionId;
345
+
346
+ logMsg(`→ sending to ${url} with auth=${token.slice(0,8)}...`);
347
+ const res = await fetch(url, {
348
+ method: "POST",
349
+ headers,
350
+ body: JSON.stringify(message),
351
+ });
352
+
353
+ const newSessionId = res.headers.get("mcp-session-id");
354
+ if (newSessionId) sessionId = newSessionId;
355
+
356
+ logMsg(`← status=${res.status} content-type=${res.headers.get("content-type")}`);
357
+ const contentType = res.headers.get("content-type") || "";
358
+ if (contentType.includes("text/event-stream")) {
359
+ const text = await res.text();
360
+ logMsg(`← SSE body: ${text.slice(0, 300)}`);
361
+ for (const chunk of text.split("\n")) {
362
+ if (chunk.startsWith("data: ") && chunk.slice(6).trim()) {
363
+ process.stdout.write(chunk.slice(6) + "\n");
364
+ }
365
+ }
366
+ } else {
367
+ const json = await res.json();
368
+ logMsg(`← JSON response: ${JSON.stringify(json).slice(0, 300)}`);
369
+ process.stdout.write(JSON.stringify(json) + "\n");
370
+ }
371
+ } catch (err) {
372
+ logMsg(`Proxy error: ${err.message}\n${err.stack}`);
373
+ }
374
+ });
375
+ }
376
+
377
+ async function cmdServeDev() {
378
+ await import(resolve(__dirname, "../dist/index.js"));
379
+ }
380
+
381
+ async function cmdLogin() {
382
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
383
+
384
+ try {
385
+ const serverUrl =
386
+ (await rl.question(`Server URL [${DEFAULT_URL}]: `)).trim() || DEFAULT_URL;
387
+ const email = (await rl.question("Email: ")).trim();
388
+
389
+ if (!email) {
390
+ console.error("Error: Email is required.");
391
+ process.exit(1);
392
+ }
393
+
394
+ // Close readline before raw-mode password input
395
+ rl.close();
396
+ const password = await readPassword("Password: ");
397
+
398
+ if (!password) {
399
+ console.error("Error: Password is required.");
400
+ process.exit(1);
401
+ }
402
+
403
+ const base = baseUrl(serverUrl);
404
+ console.log("\nLogging in...");
405
+
406
+ const res = await fetch(`${base}/auth/login`, {
407
+ method: "POST",
408
+ headers: { "Content-Type": "application/json" },
409
+ body: JSON.stringify({ email, password }),
410
+ });
411
+
412
+ const data = await res.json();
413
+ if (!res.ok) {
414
+ console.error(`Login failed: ${data.error}`);
415
+ process.exit(1);
416
+ }
417
+
418
+ // Save CLI credentials
419
+ const mcpUrl = serverUrl.endsWith("/mcp") ? serverUrl : `${serverUrl}/mcp`;
420
+ await saveCredentials({ url: mcpUrl, token: data.token, email: data.user.email });
421
+ console.log(`Logged in as ${data.user.email}`);
422
+ console.log(`Credentials saved to ${CRED_FILE}`);
423
+
424
+ // Configure Claude Code integration
425
+ const rl2 = createInterface({ input: process.stdin, output: process.stdout });
426
+ try {
427
+ const setupClaude = (
428
+ await rl2.question("\nConfigure for Claude Code? [Y/n]: ")
429
+ ).trim().toLowerCase();
430
+
431
+ if (setupClaude !== "n" && setupClaude !== "no") {
432
+ const scope = (
433
+ await rl2.question("Scope — global or project? [global]: ")
434
+ ).trim().toLowerCase();
435
+
436
+ const mcpEntry = {
437
+ command: "npx",
438
+ args: ["-y", "trapic-mcp"],
439
+ env: { TRAPIC_TOKEN: data.token },
440
+ };
441
+
442
+ if (scope === "project") {
443
+ const mcpJsonPath = resolve(process.cwd(), ".mcp.json");
444
+ let config = {};
445
+ if (await fileExists(mcpJsonPath)) {
446
+ try { config = await readJson(mcpJsonPath); } catch {}
447
+ }
448
+ if (!config.mcpServers) config.mcpServers = {};
449
+ config.mcpServers["trapic"] = mcpEntry;
450
+ await writeJson(mcpJsonPath, config);
451
+ console.log(`Wrote to ${mcpJsonPath}`);
452
+ } else {
453
+ const claudeDir = join(homedir(), ".claude");
454
+ await mkdir(claudeDir, { recursive: true });
455
+ const mcpJsonPath = join(claudeDir, "mcp.json");
456
+ let mcpConfig = {};
457
+ if (await fileExists(mcpJsonPath)) {
458
+ try { mcpConfig = await readJson(mcpJsonPath); } catch {}
459
+ }
460
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
461
+ mcpConfig.mcpServers["trapic"] = mcpEntry;
462
+ await writeJson(mcpJsonPath, mcpConfig);
463
+ console.log(`Wrote to ${mcpJsonPath}`);
464
+ }
465
+
466
+ // Generate CLAUDE.md if missing
467
+ const claudeMdPath = resolve(process.cwd(), "CLAUDE.md");
468
+ if (!(await fileExists(claudeMdPath))) {
469
+ await writeFile(claudeMdPath, CLAUDE_MD_CONTENT, "utf-8");
470
+ console.log(`Generated ${claudeMdPath}`);
471
+ }
472
+ }
473
+ } finally {
474
+ rl2.close();
475
+ }
476
+
477
+ console.log("\nDone! Run 'trapic-mcp whoami' to verify.");
478
+ } catch (err) {
479
+ if (err.code !== "ERR_USE_AFTER_CLOSE") {
480
+ console.error(`Error: ${err.message}`);
481
+ process.exit(1);
482
+ }
483
+ }
484
+ }
485
+
486
+ async function cmdLogout() {
487
+ const config = await loadConfig();
488
+ if (!config) {
489
+ console.log("Not logged in.");
490
+ return;
491
+ }
492
+
493
+ try {
494
+ const base = baseUrl(config.url);
495
+ await fetch(`${base}/auth/logout`, {
496
+ method: "POST",
497
+ headers: {
498
+ "Content-Type": "application/json",
499
+ Authorization: `Bearer ${config.token}`,
500
+ },
501
+ });
502
+ console.log("Token revoked on server.");
503
+ } catch (err) {
504
+ console.error(`Warning: Could not revoke token on server: ${err.message}`);
505
+ }
506
+
507
+ // Clear local credentials
508
+ await clearCredentials();
509
+
510
+ // Clean up .mcp.json
511
+ const mcpJsonPath = resolve(process.cwd(), ".mcp.json");
512
+ if (await fileExists(mcpJsonPath)) {
513
+ try {
514
+ const cfg = await readJson(mcpJsonPath);
515
+ const key = cfg?.mcpServers?.["trapic"] ? "trapic" : "trapic-mcp";
516
+ if (cfg?.mcpServers?.[key]) {
517
+ delete cfg.mcpServers[key];
518
+ await writeJson(mcpJsonPath, cfg);
519
+ console.log(`Removed ${key} from ${mcpJsonPath}`);
520
+ }
521
+ } catch {}
522
+ }
523
+
524
+ // Clean up ~/.claude/mcp.json
525
+ const claudeMcpPath = join(homedir(), ".claude", "mcp.json");
526
+ if (await fileExists(claudeMcpPath)) {
527
+ try {
528
+ const cfg = await readJson(claudeMcpPath);
529
+ const key = cfg?.mcpServers?.["trapic"] ? "trapic" : "trapic-mcp";
530
+ if (cfg?.mcpServers?.[key]) {
531
+ delete cfg.mcpServers[key];
532
+ await writeJson(claudeMcpPath, cfg);
533
+ console.log(`Removed ${key} from ${claudeMcpPath}`);
534
+ }
535
+ } catch {}
536
+ }
537
+
538
+ console.log("Logged out.");
539
+ }
540
+
541
+ async function cmdWhoami() {
542
+ const config = await requireConfig();
543
+ const base = baseUrl(config.url);
544
+
545
+ try {
546
+ const data = await authRequest("GET", `${base}/auth/whoami`, config.token);
547
+
548
+ console.log(`User: ${data.user.email}`);
549
+ console.log(`ID: ${data.user.id}`);
550
+ console.log(`Joined: ${fmtDate(data.user.created_at)}`);
551
+ console.log();
552
+ if (data.token) {
553
+ console.log(`Token: ${data.token.token_prefix}*** (${data.token.name})`);
554
+ console.log(`Created: ${fmtDate(data.token.created_at)}`);
555
+ console.log(`Used: ${fmtRelative(data.token.last_used_at)}`);
556
+ }
557
+ if (data.stats) {
558
+ console.log();
559
+ console.log(`Active traces: ${data.stats.active_traces}`);
560
+ }
561
+ } catch (err) {
562
+ console.error(`Error: ${err.message}`);
563
+ process.exit(1);
564
+ }
565
+ }
566
+
567
+ async function cmdInit() {
568
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
569
+
570
+ try {
571
+ const url = (await rl.question(`MCP server URL [${DEFAULT_URL}]: `)).trim() || DEFAULT_URL;
572
+ const token = (await rl.question("API token (tr_xxx): ")).trim();
573
+
574
+ if (!token) {
575
+ console.error("Error: API token is required.");
576
+ process.exit(1);
577
+ }
578
+
579
+ const scopeFlags = process.argv.slice(3);
580
+ let scope = "project";
581
+ if (scopeFlags.includes("--global")) scope = "global";
582
+ if (scopeFlags.includes("--project")) scope = "project";
583
+
584
+ if (!scopeFlags.includes("--global") && !scopeFlags.includes("--project")) {
585
+ const answer = (
586
+ await rl.question(
587
+ "Scope — global (~/.claude/settings.json) or project (.mcp.json)? [project]: "
588
+ )
589
+ ).trim().toLowerCase();
590
+ if (answer === "global") scope = "global";
591
+ }
592
+
593
+ const mcpEntry = {
594
+ command: "npx",
595
+ args: ["-y", "trapic-mcp"],
596
+ env: { TRAPIC_TOKEN: token },
597
+ };
598
+
599
+ if (scope === "global") {
600
+ const claudeDir = join(homedir(), ".claude");
601
+ await mkdir(claudeDir, { recursive: true });
602
+ const mcpJsonPath = join(claudeDir, "mcp.json");
603
+ let mcpConfig = {};
604
+ if (await fileExists(mcpJsonPath)) {
605
+ try { mcpConfig = await readJson(mcpJsonPath); } catch {}
606
+ }
607
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
608
+ mcpConfig.mcpServers["trapic"] = mcpEntry;
609
+ await writeJson(mcpJsonPath, mcpConfig);
610
+ console.log(`\nWrote trapic config to ${mcpJsonPath}`);
611
+ } else {
612
+ const mcpJsonPath = resolve(process.cwd(), ".mcp.json");
613
+ let config = {};
614
+ if (await fileExists(mcpJsonPath)) {
615
+ try { config = await readJson(mcpJsonPath); } catch {}
616
+ }
617
+ if (!config.mcpServers) config.mcpServers = {};
618
+ config.mcpServers["trapic"] = mcpEntry;
619
+ await writeJson(mcpJsonPath, config);
620
+ console.log(`\nWrote trapic config to ${mcpJsonPath}`);
621
+ }
622
+
623
+ const claudeMdPath = resolve(process.cwd(), "CLAUDE.md");
624
+ if (!(await fileExists(claudeMdPath))) {
625
+ await writeFile(claudeMdPath, CLAUDE_MD_CONTENT, "utf-8");
626
+ console.log(`Generated ${claudeMdPath} with auto-trace instructions.`);
627
+ } else {
628
+ console.log(`CLAUDE.md already exists — skipped generation.`);
629
+ }
630
+
631
+ // Also save to CLI credentials
632
+ const mcpUrl = url.endsWith("/mcp") ? url : `${url}/mcp`;
633
+ await saveCredentials({ url: mcpUrl, token });
634
+
635
+ console.log(`
636
+ Setup complete! Next steps:
637
+ 1. Start a Claude Code session in this project
638
+ 2. The trapic-mcp server will connect automatically
639
+ 3. Traces will be created as you work
640
+
641
+ Run "trapic-mcp whoami" to verify the connection.
642
+ `);
643
+ } finally {
644
+ rl.close();
645
+ }
646
+ }
647
+
648
+ async function cmdStatus() {
649
+ const config = await requireConfig();
650
+ console.log(`Server: ${config.url}\n`);
651
+
652
+ try {
653
+ const data = await mcpToolCall("trace_get_active", { limit: 5 });
654
+ if (!data) {
655
+ console.log("No active traces found.");
656
+ return;
657
+ }
658
+
659
+ const traceList = Array.isArray(data) ? data : data?.traces || data?.data || [];
660
+ const total = data?.total ?? data?.count ?? traceList.length;
661
+
662
+ console.log(`Active traces: ${total}`);
663
+ console.log("─".repeat(50));
664
+
665
+ if (traceList.length === 0) {
666
+ console.log("(no traces to display)");
667
+ return;
668
+ }
669
+
670
+ console.log("\nMost recent:");
671
+ for (const t of traceList.slice(0, 5)) {
672
+ console.log(` ${fmtDate(t.created_at)} ${truncate(t.claim || t.title || "(no claim)", 70)}`);
673
+ }
674
+ console.log();
675
+ } catch (err) {
676
+ console.error(`Error: ${err.message}`);
677
+ process.exit(1);
678
+ }
679
+ }
680
+
681
+ async function cmdSearch(query) {
682
+ if (!query) {
683
+ console.error("Usage: trapic-mcp search <query>");
684
+ process.exit(1);
685
+ }
686
+
687
+ console.log(`Searching: "${query}"\n`);
688
+
689
+ try {
690
+ const data = await mcpToolCall("trace_search", { query, limit: 20 });
691
+ if (!data) {
692
+ console.log("No results found.");
693
+ return;
694
+ }
695
+
696
+ const traces = Array.isArray(data) ? data : data?.traces || data?.data || [];
697
+ if (traces.length === 0) {
698
+ console.log("No results found.");
699
+ return;
700
+ }
701
+
702
+ console.log(`Found ${traces.length} result(s):\n`);
703
+ for (const t of traces) {
704
+ const sim = t.similarity != null ? `[${t.similarity.toFixed(2)}]` : "";
705
+ const tags = t.tags?.length ? `[${t.tags.join(", ")}]` : "";
706
+ const conf = t.confidence || "";
707
+
708
+ console.log(` ${sim.padEnd(7)} ${truncate(t.claim || "(no claim)", 65)}`);
709
+ console.log(` ${tags} ${conf} | ${fmtDate(t.created_at)}`);
710
+ if (t.reason) {
711
+ console.log(` ${truncate(t.reason, 70)}`);
712
+ }
713
+ console.log();
714
+ }
715
+ } catch (err) {
716
+ console.error(`Error: ${err.message}`);
717
+ process.exit(1);
718
+ }
719
+ }
720
+
721
+ async function cmdList() {
722
+ const tagFlag = args.find((a) => a.startsWith("--tag="));
723
+ const tag = tagFlag ? tagFlag.split("=")[1] : undefined;
724
+ const limitFlag = args.find((a) => a.startsWith("--limit="));
725
+ const limit = limitFlag ? parseInt(limitFlag.split("=")[1]) : 20;
726
+
727
+ try {
728
+ const toolArgs = { limit };
729
+ if (tag) toolArgs.tags = [tag];
730
+
731
+ const data = await mcpToolCall("trace_get_active", toolArgs);
732
+ if (!data) {
733
+ console.log("No active traces.");
734
+ return;
735
+ }
736
+
737
+ const traces = Array.isArray(data) ? data : data?.traces || data?.data || [];
738
+ const total = data?.total ?? data?.count ?? traces.length;
739
+
740
+ console.log(`Active traces: ${total}${tag ? ` (tag: ${tag})` : ""}\n`);
741
+
742
+ if (traces.length === 0) {
743
+ console.log("(none)");
744
+ return;
745
+ }
746
+
747
+ for (const t of traces) {
748
+ const tags = t.tags?.length ? `[${t.tags.join(", ")}]` : "";
749
+ const conf = t.confidence ? t.confidence : "";
750
+ console.log(` ${fmtDate(t.created_at)} ${truncate(t.claim || "(no claim)", 60)}`);
751
+ console.log(` ${tags} ${conf}`);
752
+ if (t.reason) {
753
+ console.log(` ${truncate(t.reason, 65)}`);
754
+ }
755
+ console.log();
756
+ }
757
+ } catch (err) {
758
+ console.error(`Error: ${err.message}`);
759
+ process.exit(1);
760
+ }
761
+ }
762
+
763
+ async function cmdToken(subcommand) {
764
+ const config = await requireConfig();
765
+ const base = baseUrl(config.url);
766
+
767
+ switch (subcommand) {
768
+ case "list":
769
+ case "ls":
770
+ case undefined: {
771
+ const data = await authRequest("GET", `${base}/auth/tokens`, config.token);
772
+ const tokens = data.tokens || [];
773
+
774
+ if (tokens.length === 0) {
775
+ console.log("No tokens.");
776
+ return;
777
+ }
778
+
779
+ console.log("API Tokens:\n");
780
+ for (const t of tokens) {
781
+ const current = t.is_current ? " ← current" : "";
782
+ const revoked = t.revoked_at ? " [revoked]" : "";
783
+ const used = t.last_used_at ? fmtRelative(t.last_used_at) : "never";
784
+
785
+ console.log(
786
+ ` ${t.token_prefix}*** ${(t.name || "").padEnd(12)} Created: ${fmtDate(t.created_at)} Used: ${used}${current}${revoked}`
787
+ );
788
+ }
789
+ console.log();
790
+ break;
791
+ }
792
+
793
+ case "create": {
794
+ const name = args[2] || "default";
795
+ try {
796
+ const data = await authRequest(
797
+ "POST",
798
+ `${base}/auth/tokens/create`,
799
+ config.token,
800
+ { name }
801
+ );
802
+ console.log(`Token created: ${data.token}`);
803
+ console.log(`Name: ${data.name}`);
804
+ console.log(`\nSave this token — it won't be shown again.`);
805
+ } catch (err) {
806
+ console.error(`Error: ${err.message}`);
807
+ process.exit(1);
808
+ }
809
+ break;
810
+ }
811
+
812
+ case "revoke": {
813
+ const tokenId = args[2];
814
+ if (!tokenId) {
815
+ console.error("Usage: trapic-mcp token revoke <token-id>");
816
+ console.error("Run 'trapic-mcp token list' to see token IDs.");
817
+ process.exit(1);
818
+ }
819
+
820
+ try {
821
+ await authRequest("POST", `${base}/auth/tokens/revoke`, config.token, {
822
+ token_id: tokenId,
823
+ });
824
+ console.log(`Token ${tokenId} revoked.`);
825
+ } catch (err) {
826
+ console.error(`Error: ${err.message}`);
827
+ process.exit(1);
828
+ }
829
+ break;
830
+ }
831
+
832
+ default:
833
+ console.error(`Unknown token subcommand: ${subcommand}`);
834
+ console.error("Available: list, create [name], revoke <id>");
835
+ process.exit(1);
836
+ }
837
+ }
838
+
839
+ // ── Main ─────────────────────────────────────────────────────────────────────
840
+
841
+ const args = process.argv.slice(2);
842
+ const command = args[0] || "serve";
843
+
844
+ switch (command) {
845
+ case "serve":
846
+ await cmdServe();
847
+ break;
848
+ case "serve:dev":
849
+ await cmdServeDev();
850
+ break;
851
+ case "login":
852
+ await cmdLogin();
853
+ break;
854
+ case "logout":
855
+ await cmdLogout();
856
+ break;
857
+ case "whoami":
858
+ await cmdWhoami();
859
+ break;
860
+ case "init":
861
+ await cmdInit();
862
+ break;
863
+ case "status":
864
+ await cmdStatus();
865
+ break;
866
+ case "search":
867
+ await cmdSearch(args.slice(1).join(" "));
868
+ break;
869
+ case "list":
870
+ case "ls":
871
+ await cmdList();
872
+ break;
873
+ case "token":
874
+ await cmdToken(args[1]);
875
+ break;
876
+ default:
877
+ console.log(`trapic-mcp — CLI & MCP proxy for Trapic
878
+
879
+ Usage:
880
+ trapic-mcp Start stdio proxy (for Claude Code)
881
+ trapic-mcp login Login with email/password
882
+ trapic-mcp logout Revoke token and clear credentials
883
+ trapic-mcp whoami Show current user and token info
884
+ trapic-mcp init Manual setup with token (--global | --project)
885
+ trapic-mcp status Quick overview of active traces
886
+ trapic-mcp search <query> Semantic search traces
887
+ trapic-mcp list [--tag=x] List active traces (--limit=N)
888
+ trapic-mcp token list List API tokens
889
+ trapic-mcp token create [name] Create new API token
890
+ trapic-mcp token revoke <id> Revoke an API token
891
+ trapic-mcp serve:dev Start local dev MCP server
892
+
893
+ Environment:
894
+ TRAPIC_TOKEN API token (overrides credentials file)
895
+ TRAPIC_URL Server URL (default: ${DEFAULT_URL})
896
+ `);
897
+ if (command !== "help" && command !== "--help" && command !== "-h") {
898
+ console.error(`Unknown command: ${command}`);
899
+ process.exit(1);
900
+ }
901
+ break;
902
+ }