trapic-mcp 0.1.0 → 0.1.3

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.
@@ -379,9 +379,16 @@ async function cmdServeDev() {
379
379
  }
380
380
 
381
381
  async function cmdLogin() {
382
+ const useBrowser = process.argv.includes("--browser") || process.argv.includes("-b");
383
+
384
+ if (useBrowser) {
385
+ return cmdLoginBrowser();
386
+ }
387
+
382
388
  const rl = createInterface({ input: process.stdin, output: process.stdout });
383
389
 
384
390
  try {
391
+ console.log("Login with email/password. For GitHub/Google, use: trapic login --browser\n");
385
392
  const serverUrl =
386
393
  (await rl.question(`Server URL [${DEFAULT_URL}]: `)).trim() || DEFAULT_URL;
387
394
  const email = (await rl.question("Email: ")).trim();
@@ -483,6 +490,90 @@ async function cmdLogin() {
483
490
  }
484
491
  }
485
492
 
493
+ async function cmdLoginBrowser() {
494
+ const { exec } = await import("child_process");
495
+
496
+ console.log("Opening browser to create an API token...\n");
497
+
498
+ const tokenUrl = "https://trapic.ai/tokens";
499
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
500
+ exec(`${openCmd} ${tokenUrl}`);
501
+
502
+ console.log("1. Sign in with GitHub, Google, or email in the browser");
503
+ console.log("2. Create a new API token");
504
+ console.log("3. Copy the token and paste it below\n");
505
+
506
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
507
+ const token = (await rl.question("Paste your API token (tr_...): ")).trim();
508
+ rl.close();
509
+
510
+ if (!token || !token.startsWith("tr_")) {
511
+ console.error("Error: Invalid token. Must start with tr_");
512
+ process.exit(1);
513
+ }
514
+
515
+ // Verify token
516
+ const res = await fetch("https://mcp.trapic.ai/auth/whoami", {
517
+ headers: { Authorization: `Bearer ${token}` },
518
+ });
519
+
520
+ if (!res.ok) {
521
+ console.error("Error: Token verification failed. Check your token and try again.");
522
+ process.exit(1);
523
+ }
524
+
525
+ const data = await res.json();
526
+ const mcpUrl = "https://mcp.trapic.ai/mcp";
527
+ await saveCredentials({ url: mcpUrl, token, email: data.user?.email || "" });
528
+ console.log(`\nLogged in as ${data.user?.email || "unknown"}`);
529
+ console.log(`Credentials saved to ${CRED_FILE}`);
530
+
531
+ // Configure Claude Code
532
+ const rl2 = createInterface({ input: process.stdin, output: process.stdout });
533
+ try {
534
+ const setupClaude = (
535
+ await rl2.question("\nConfigure for Claude Code? [Y/n]: ")
536
+ ).trim().toLowerCase();
537
+
538
+ if (setupClaude !== "n" && setupClaude !== "no") {
539
+ const scope = (
540
+ await rl2.question("Scope — global or project? [global]: ")
541
+ ).trim().toLowerCase();
542
+
543
+ const mcpEntry = {
544
+ command: "npx",
545
+ args: ["-y", "trapic-mcp"],
546
+ env: { TRAPIC_TOKEN: token },
547
+ };
548
+
549
+ if (scope === "project" || scope === "p") {
550
+ const projectDir = process.cwd();
551
+ const claudeDir = join(projectDir, ".claude");
552
+ const claudeJson = join(claudeDir, "mcp.json");
553
+ await mkdir(claudeDir, { recursive: true });
554
+ let existing = {};
555
+ try { existing = JSON.parse(await readFile(claudeJson, "utf-8")); } catch {}
556
+ existing.mcpServers = existing.mcpServers || {};
557
+ existing.mcpServers.trapic = mcpEntry;
558
+ await writeFile(claudeJson, JSON.stringify(existing, null, 2) + "\n");
559
+ console.log(`Saved to ${claudeJson}`);
560
+ } else {
561
+ const globalDir = join(homedir(), ".claude");
562
+ const globalJson = join(globalDir, "mcp.json");
563
+ await mkdir(globalDir, { recursive: true });
564
+ let existing = {};
565
+ try { existing = JSON.parse(await readFile(globalJson, "utf-8")); } catch {}
566
+ existing.mcpServers = existing.mcpServers || {};
567
+ existing.mcpServers.trapic = mcpEntry;
568
+ await writeFile(globalJson, JSON.stringify(existing, null, 2) + "\n");
569
+ console.log(`Saved to ${globalJson}`);
570
+ }
571
+ }
572
+ } finally {
573
+ rl2.close();
574
+ }
575
+ }
576
+
486
577
  async function cmdLogout() {
487
578
  const config = await loadConfig();
488
579
  if (!config) {
@@ -873,12 +964,33 @@ switch (command) {
873
964
  case "token":
874
965
  await cmdToken(args[1]);
875
966
  break;
967
+ case "upgrade":
968
+ case "update": {
969
+ const { execSync } = await import("child_process");
970
+ console.log("Checking for updates...");
971
+ try {
972
+ const latest = execSync("npm view trapic-mcp version", { encoding: "utf-8" }).trim();
973
+ const pkg = JSON.parse(await readFile(resolve(__dirname, "../package.json"), "utf-8"));
974
+ if (latest === pkg.version) {
975
+ console.log(`Already on latest version (${pkg.version})`);
976
+ } else {
977
+ console.log(`Upgrading ${pkg.version} → ${latest}...`);
978
+ execSync("npm install -g trapic-mcp@latest", { stdio: "inherit" });
979
+ console.log(`\nUpgraded to ${latest}`);
980
+ }
981
+ } catch (e) {
982
+ console.error("Upgrade failed:", e.message);
983
+ console.log("Try manually: npm install -g trapic-mcp@latest");
984
+ }
985
+ break;
986
+ }
876
987
  default:
877
988
  console.log(`trapic-mcp — CLI & MCP proxy for Trapic
878
989
 
879
990
  Usage:
880
991
  trapic-mcp Start stdio proxy (for Claude Code)
881
992
  trapic-mcp login Login with email/password
993
+ trapic-mcp login --browser Login with GitHub/Google (opens browser)
882
994
  trapic-mcp logout Revoke token and clear credentials
883
995
  trapic-mcp whoami Show current user and token info
884
996
  trapic-mcp init Manual setup with token (--global | --project)
@@ -888,6 +1000,7 @@ Usage:
888
1000
  trapic-mcp token list List API tokens
889
1001
  trapic-mcp token create [name] Create new API token
890
1002
  trapic-mcp token revoke <id> Revoke an API token
1003
+ trapic-mcp upgrade Upgrade to latest version
891
1004
  trapic-mcp serve:dev Start local dev MCP server
892
1005
 
893
1006
  Environment:
package/dist/worker.js CHANGED
@@ -110,7 +110,7 @@ function getCorsOrigin(request) {
110
110
  function isValidRedirectUri(uri) {
111
111
  try {
112
112
  const parsed = new URL(uri);
113
- const allowed = ["localhost", "127.0.0.1", "trapic.ai"];
113
+ const allowed = ["localhost", "127.0.0.1", "trapic.ai", "claude.ai"];
114
114
  return allowed.some(h => parsed.hostname === h || parsed.hostname.endsWith("." + h));
115
115
  }
116
116
  catch {
@@ -249,8 +249,12 @@ export default {
249
249
  label { display: block; font-size: 0.8rem; color: #a3a3a3; margin-bottom: 0.25rem; }
250
250
  input[type="email"], input[type="password"] { width: 100%; padding: 0.6rem 0.75rem; background: #0a0a0a; border: 1px solid #333; border-radius: 8px; color: #e5e5e5; font-size: 0.9rem; margin-bottom: 1rem; outline: none; }
251
251
  input:focus { border-color: #525252; }
252
- button { width: 100%; padding: 0.6rem; background: #fff; color: #000; border: none; border-radius: 9999px; font-size: 0.9rem; font-weight: 600; cursor: pointer; }
253
- button:hover { opacity: 0.9; }
252
+ button, .oauth-btn { width: 100%; padding: 0.6rem; background: #fff; color: #000; border: none; border-radius: 9999px; font-size: 0.9rem; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 0.5rem; text-decoration: none; }
253
+ button:hover, .oauth-btn:hover { opacity: 0.9; }
254
+ .oauth-btn { background: #262626; color: #e5e5e5; border: 1px solid #333; margin-bottom: 0.5rem; }
255
+ .divider { display: flex; align-items: center; gap: 0.75rem; margin: 1.25rem 0; }
256
+ .divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: #333; }
257
+ .divider span { font-size: 0.75rem; color: #525252; }
254
258
  .error { color: #ef4444; font-size: 0.8rem; margin-bottom: 1rem; }
255
259
  </style>
256
260
  </head>
@@ -259,6 +263,15 @@ export default {
259
263
  <div class="logo">Trapic</div>
260
264
  <div class="desc">Sign in to authorize access to your knowledge base</div>
261
265
  ${errorMsg ? `<div class="error">${escapeHtml(errorMsg)}</div>` : ""}
266
+ <a class="oauth-btn" href="${env.SUPABASE_URL}/auth/v1/authorize?provider=github&redirect_to=${encodeURIComponent(origin + '/oauth/callback-social?' + new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, state, code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod }).toString())}">
267
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
268
+ Continue with GitHub
269
+ </a>
270
+ <a class="oauth-btn" href="${env.SUPABASE_URL}/auth/v1/authorize?provider=google&redirect_to=${encodeURIComponent(origin + '/oauth/callback-social?' + new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, state, code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod }).toString())}">
271
+ <svg width="16" height="16" viewBox="0 0 24 24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>
272
+ Continue with Google
273
+ </a>
274
+ <div class="divider"><span>or</span></div>
262
275
  <form method="POST" action="/oauth/authorize">
263
276
  <input type="hidden" name="client_id" value="${escapeHtml(clientId)}" />
264
277
  <input type="hidden" name="redirect_uri" value="${escapeHtml(redirectUri)}" />
@@ -266,7 +279,7 @@ export default {
266
279
  <input type="hidden" name="code_challenge" value="${escapeHtml(codeChallenge)}" />
267
280
  <input type="hidden" name="code_challenge_method" value="${escapeHtml(codeChallengeMethod)}" />
268
281
  <label>Email</label>
269
- <input type="email" name="email" required autocomplete="email" autofocus />
282
+ <input type="email" name="email" required autocomplete="email" />
270
283
  <label>Password</label>
271
284
  <input type="password" name="password" required autocomplete="current-password" />
272
285
  <button type="submit">Sign In</button>
@@ -283,6 +296,66 @@ export default {
283
296
  },
284
297
  });
285
298
  }
299
+ // OAuth social callback — client-side page that reads hash fragment token
300
+ if (url.pathname === "/oauth/callback-social") {
301
+ // Serve a small HTML page that extracts access_token from hash and POSTs to server
302
+ const html = `<!DOCTYPE html><html><head><title>Authenticating...</title></head>
303
+ <body style="background:#0a0a0a;color:#e5e5e5;font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;">
304
+ <div style="text-align:center"><p>Authenticating...</p></div>
305
+ <script>
306
+ (function(){
307
+ var hash = window.location.hash.substring(1);
308
+ var params = new URLSearchParams(hash);
309
+ var accessToken = params.get('access_token');
310
+ var search = new URLSearchParams(window.location.search);
311
+ if (accessToken) {
312
+ // Forward to server endpoint with token in query
313
+ var url = '/oauth/complete-social?access_token=' + encodeURIComponent(accessToken)
314
+ + '&client_id=' + encodeURIComponent(search.get('client_id') || '')
315
+ + '&redirect_uri=' + encodeURIComponent(search.get('redirect_uri') || '')
316
+ + '&state=' + encodeURIComponent(search.get('state') || '')
317
+ + '&code_challenge=' + encodeURIComponent(search.get('code_challenge') || '')
318
+ + '&code_challenge_method=' + encodeURIComponent(search.get('code_challenge_method') || '');
319
+ window.location.replace(url);
320
+ } else {
321
+ document.body.innerHTML = '<div style="text-align:center;padding:2rem;"><p>Authentication failed. Please try again.</p><a href="/" style="color:#60a5fa;">Go back</a></div>';
322
+ }
323
+ })();
324
+ </script></body></html>`;
325
+ return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
326
+ }
327
+ // OAuth social complete — server receives access_token, issues auth code
328
+ if (url.pathname === "/oauth/complete-social") {
329
+ const accessToken = url.searchParams.get("access_token") || "";
330
+ const clientId = url.searchParams.get("client_id") || "";
331
+ const redirectUri = url.searchParams.get("redirect_uri") || "";
332
+ const state = url.searchParams.get("state") || "";
333
+ const codeChallenge = url.searchParams.get("code_challenge") || "";
334
+ if (!redirectUri || !codeChallenge || !accessToken) {
335
+ return new Response("Missing parameters", { status: 400 });
336
+ }
337
+ if (!isValidRedirectUri(redirectUri)) {
338
+ return new Response("Invalid redirect_uri", { status: 400 });
339
+ }
340
+ const supabase = getSupabase();
341
+ const { data: { user: authUser }, error: authErr } = await supabase.auth.getUser(accessToken);
342
+ if (authErr || !authUser) {
343
+ const backUrl = new URL(`${origin}/oauth/authorize`);
344
+ backUrl.searchParams.set("client_id", clientId);
345
+ backUrl.searchParams.set("redirect_uri", redirectUri);
346
+ backUrl.searchParams.set("state", state);
347
+ backUrl.searchParams.set("code_challenge", codeChallenge);
348
+ backUrl.searchParams.set("code_challenge_method", "S256");
349
+ backUrl.searchParams.set("error", "Social login failed. Please try again.");
350
+ return new Response(null, { status: 302, headers: { "Location": backUrl.toString() } });
351
+ }
352
+ const code = await createAuthCode(env, authUser.id, codeChallenge, redirectUri);
353
+ const redirectUrl = new URL(redirectUri);
354
+ redirectUrl.searchParams.set("code", code);
355
+ if (state)
356
+ redirectUrl.searchParams.set("state", state);
357
+ return new Response(null, { status: 302, headers: { "Location": redirectUrl.toString() } });
358
+ }
286
359
  // OAuth Authorize POST — validates credentials, issues auth code via 302 redirect
287
360
  if (url.pathname === "/oauth/authorize" && request.method === "POST") {
288
361
  // Parse form body (native form POST sends application/x-www-form-urlencoded)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trapic-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "AI knowledge management MCP server — automatically capture, search, and connect decisions, facts, and conventions across AI coding sessions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",