trapic-mcp 0.1.0 → 0.1.1

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) {
@@ -879,6 +970,7 @@ switch (command) {
879
970
  Usage:
880
971
  trapic-mcp Start stdio proxy (for Claude Code)
881
972
  trapic-mcp login Login with email/password
973
+ trapic-mcp login --browser Login with GitHub/Google (opens browser)
882
974
  trapic-mcp logout Revoke token and clear credentials
883
975
  trapic-mcp whoami Show current user and token info
884
976
  trapic-mcp init Manual setup with token (--global | --project)
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,38 @@ export default {
283
296
  },
284
297
  });
285
298
  }
299
+ // OAuth social callback — Supabase redirects here after GitHub/Google login
300
+ if (url.pathname === "/oauth/callback-social") {
301
+ const accessToken = url.hash ? new URLSearchParams(url.hash.slice(1)).get("access_token") : url.searchParams.get("access_token");
302
+ const clientId = url.searchParams.get("client_id") || "";
303
+ const redirectUri = url.searchParams.get("redirect_uri") || "";
304
+ const state = url.searchParams.get("state") || "";
305
+ const codeChallenge = url.searchParams.get("code_challenge") || "";
306
+ if (!redirectUri || !codeChallenge) {
307
+ return new Response("Missing OAuth parameters", { status: 400 });
308
+ }
309
+ // Get user from Supabase access token
310
+ const supabase = getSupabase();
311
+ const { data: { user: authUser }, error: authErr } = await supabase.auth.getUser(accessToken || "");
312
+ if (authErr || !authUser) {
313
+ // Redirect back to authorize with error
314
+ const backUrl = new URL(`${origin}/oauth/authorize`);
315
+ backUrl.searchParams.set("client_id", clientId);
316
+ backUrl.searchParams.set("redirect_uri", redirectUri);
317
+ backUrl.searchParams.set("state", state);
318
+ backUrl.searchParams.set("code_challenge", codeChallenge);
319
+ backUrl.searchParams.set("code_challenge_method", "S256");
320
+ backUrl.searchParams.set("error", "Social login failed. Please try again.");
321
+ return new Response(null, { status: 302, headers: { "Location": backUrl.toString() } });
322
+ }
323
+ // Generate signed auth code
324
+ const code = await createAuthCode(env, authUser.id, codeChallenge, redirectUri);
325
+ const redirectUrl = new URL(redirectUri);
326
+ redirectUrl.searchParams.set("code", code);
327
+ if (state)
328
+ redirectUrl.searchParams.set("state", state);
329
+ return new Response(null, { status: 302, headers: { "Location": redirectUrl.toString() } });
330
+ }
286
331
  // OAuth Authorize POST — validates credentials, issues auth code via 302 redirect
287
332
  if (url.pathname === "/oauth/authorize" && request.method === "POST") {
288
333
  // 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.1",
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",