Skip to content

Commit 807e343

Browse files
authored
Merge pull request #20 from supermemoryai/01-26-enable_oauth_flow_using_new_plugins_auth
enable oauth flow using new plugins auth
2 parents 3af79db + 16dabf4 commit 807e343

4 files changed

Lines changed: 245 additions & 24 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-supermemory",
3-
"version": "0.1.8",
3+
"version": "2.0.0",
44
"description": "OpenCode plugin that gives coding agents persistent memory using Supermemory",
55
"type": "module",
66
"main": "dist/index.js",

src/cli.ts

Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { join } from "node:path";
44
import { homedir } from "node:os";
55
import * as readline from "node:readline";
66
import { stripJsoncComments } from "./services/jsonc.js";
7+
import { startAuthFlow, clearCredentials, loadCredentials } from "./services/auth.js";
78

89
const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode");
910
const OPENCODE_COMMAND_DIR = join(OPENCODE_CONFIG_DIR, "command");
@@ -162,6 +163,31 @@ Then ask: "I've initialized memory with X insights. Want me to continue refining
162163
6. Summarize what was learned and ask if user wants refinement
163164
`;
164165

166+
const SUPERMEMORY_LOGIN_COMMAND = `---
167+
description: Authenticate with Supermemory via browser
168+
---
169+
170+
# Supermemory Login
171+
172+
Run this command to authenticate the user with Supermemory:
173+
174+
\`\`\`bash
175+
bunx opencode-supermemory@latest login
176+
\`\`\`
177+
178+
This will:
179+
1. Start a local server on port 19877
180+
2. Open the browser to Supermemory's authentication page
181+
3. After the user logs in, save credentials to ~/.supermemory-opencode/credentials.json
182+
183+
Wait for the command to complete, then inform the user whether authentication succeeded or failed.
184+
185+
If the user wants to log out instead, run:
186+
\`\`\`bash
187+
bunx opencode-supermemory@latest logout
188+
\`\`\`
189+
`;
190+
165191
function createReadline(): readline.Interface {
166192
return readline.createInterface({
167193
input: process.stdin,
@@ -261,12 +287,17 @@ function createNewConfig(): boolean {
261287
return true;
262288
}
263289

264-
function createCommand(): boolean {
290+
function createCommands(): boolean {
265291
mkdirSync(OPENCODE_COMMAND_DIR, { recursive: true });
266-
const commandPath = join(OPENCODE_COMMAND_DIR, "supermemory-init.md");
267292

268-
writeFileSync(commandPath, SUPERMEMORY_INIT_COMMAND);
293+
const initPath = join(OPENCODE_COMMAND_DIR, "supermemory-init.md");
294+
writeFileSync(initPath, SUPERMEMORY_INIT_COMMAND);
269295
console.log(`✓ Created /supermemory-init command`);
296+
297+
const loginPath = join(OPENCODE_COMMAND_DIR, "supermemory-login.md");
298+
writeFileSync(loginPath, SUPERMEMORY_LOGIN_COMMAND);
299+
console.log(`✓ Created /supermemory-login command`);
300+
270301
return true;
271302
}
272303

@@ -357,17 +388,17 @@ async function install(options: InstallOptions): Promise<number> {
357388
}
358389
}
359390

360-
// Step 2: Create /supermemory-init command
361-
console.log("\nStep 2: Create /supermemory-init command");
391+
// Step 2: Create commands
392+
console.log("\nStep 2: Create /supermemory-init and /supermemory-login commands");
362393
if (options.tui) {
363-
const shouldCreate = await confirm(rl!, "Add /supermemory-init command?");
394+
const shouldCreate = await confirm(rl!, "Add supermemory commands?");
364395
if (!shouldCreate) {
365396
console.log("Skipped.");
366397
} else {
367-
createCommand();
398+
createCommands();
368399
}
369400
} else {
370-
createCommand();
401+
createCommands();
371402
}
372403

373404
// Step 3: Configure Oh My OpenCode (if installed)
@@ -394,34 +425,70 @@ async function install(options: InstallOptions): Promise<number> {
394425
}
395426
}
396427

397-
// Step 4: API key instructions
428+
if (rl) rl.close();
429+
430+
// Step 4: Authenticate
398431
console.log("\n" + "─".repeat(50));
399-
console.log("\n🔑 Final step: Set your API key\n");
400-
console.log("Get your API key from: https://console.supermemory.ai");
401-
console.log("\nThen add to your shell profile:\n");
432+
console.log("\n🔑 Final step: Authenticate with Supermemory\n");
433+
434+
if (options.tui) {
435+
return login();
436+
}
437+
438+
// Non-interactive mode - print instructions
439+
console.log("Run this command to authenticate:");
440+
console.log(" bunx opencode-supermemory@latest login");
441+
console.log("\nOr set your API key manually:");
402442
console.log(' export SUPERMEMORY_API_KEY="sm_..."');
403-
console.log("\nOr create ~/.config/opencode/supermemory.jsonc:\n");
404-
console.log(' { "apiKey": "sm_..." }');
405443
console.log("\n" + "─".repeat(50));
406444
console.log("\n✓ Setup complete! Restart OpenCode to activate.\n");
407-
408-
if (rl) rl.close();
409445
return 0;
410446
}
411447

448+
async function login(): Promise<number> {
449+
const existing = loadCredentials();
450+
if (existing) {
451+
console.log("Already authenticated. Use 'logout' first to re-authenticate.");
452+
return 0;
453+
}
454+
455+
const result = await startAuthFlow();
456+
457+
if (result.success) {
458+
console.log("\n✓ Successfully authenticated with Supermemory!");
459+
console.log("Restart OpenCode to activate.\n");
460+
return 0;
461+
} else {
462+
console.error(`\n✗ Authentication failed: ${result.error}`);
463+
return 1;
464+
}
465+
}
466+
467+
function logout(): number {
468+
if (clearCredentials()) {
469+
console.log("✓ Logged out. Credentials cleared.");
470+
return 0;
471+
} else {
472+
console.log("No credentials found.");
473+
return 0;
474+
}
475+
}
476+
412477
function printHelp(): void {
413478
console.log(`
414479
opencode-supermemory - Persistent memory for OpenCode agents
415480
416481
Commands:
417-
install Install and configure the plugin
418-
--no-tui Run in non-interactive mode (for LLM agents)
419-
--disable-context-recovery Disable Oh My OpenCode's context-window-limit-recovery hook (use with --no-tui)
482+
install Install and configure the plugin
483+
--no-tui Non-interactive mode (for LLM agents)
484+
--disable-context-recovery Disable Oh My OpenCode's context hook
485+
login Authenticate with Supermemory (opens browser)
486+
logout Clear stored credentials
420487
421488
Examples:
422489
bunx opencode-supermemory@latest install
423-
bunx opencode-supermemory@latest install --no-tui
424-
bunx opencode-supermemory@latest install --no-tui --disable-context-recovery
490+
bunx opencode-supermemory@latest login
491+
bunx opencode-supermemory@latest logout
425492
`);
426493
}
427494

@@ -437,11 +504,14 @@ if (args[0] === "install") {
437504
const disableAutoCompact = args.includes("--disable-context-recovery");
438505
install({ tui: !noTui, disableAutoCompact }).then((code) => process.exit(code));
439506
} else if (args[0] === "setup") {
440-
// Backwards compatibility
441507
console.log("Note: 'setup' is deprecated. Use 'install' instead.\n");
442508
const noTui = args.includes("--no-tui");
443509
const disableAutoCompact = args.includes("--disable-context-recovery");
444510
install({ tui: !noTui, disableAutoCompact }).then((code) => process.exit(code));
511+
} else if (args[0] === "login") {
512+
login().then((code) => process.exit(code));
513+
} else if (args[0] === "logout") {
514+
process.exit(logout());
445515
} else {
446516
console.error(`Unknown command: ${args[0]}`);
447517
printHelp();

src/config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
22
import { join } from "node:path";
33
import { homedir } from "node:os";
44
import { stripJsoncComments } from "./services/jsonc.js";
5+
import { loadCredentials } from "./services/auth.js";
56

67
const CONFIG_DIR = join(homedir(), ".config", "opencode");
78
const CONFIG_FILES = [
@@ -87,7 +88,14 @@ function loadConfig(): SupermemoryConfig {
8788

8889
const fileConfig = loadConfig();
8990

90-
export const SUPERMEMORY_API_KEY = fileConfig.apiKey ?? process.env.SUPERMEMORY_API_KEY;
91+
function getApiKey(): string | undefined {
92+
// Priority: env var > config file > OAuth credentials
93+
if (process.env.SUPERMEMORY_API_KEY) return process.env.SUPERMEMORY_API_KEY;
94+
if (fileConfig.apiKey) return fileConfig.apiKey;
95+
return loadCredentials()?.apiKey;
96+
}
97+
98+
export const SUPERMEMORY_API_KEY = getApiKey();
9199

92100
export const CONFIG = {
93101
similarityThreshold: fileConfig.similarityThreshold ?? DEFAULTS.similarityThreshold,

src/services/auth.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3+
import { exec } from "node:child_process";
4+
import { join } from "node:path";
5+
import { homedir } from "node:os";
6+
7+
const CREDENTIALS_DIR = join(homedir(), ".supermemory-opencode");
8+
const CREDENTIALS_FILE = join(CREDENTIALS_DIR, "credentials.json");
9+
const AUTH_PORT = 19877;
10+
const AUTH_BASE_URL = "https://console.supermemory.ai/auth/connect";
11+
const CLIENT_NAME = "opencode";
12+
13+
interface Credentials {
14+
apiKey: string;
15+
createdAt: string;
16+
}
17+
18+
export function loadCredentials(): Credentials | null {
19+
if (!existsSync(CREDENTIALS_FILE)) return null;
20+
try {
21+
const content = readFileSync(CREDENTIALS_FILE, "utf-8");
22+
return JSON.parse(content) as Credentials;
23+
} catch {
24+
return null;
25+
}
26+
}
27+
28+
export function saveCredentials(apiKey: string): void {
29+
mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
30+
const credentials: Credentials = {
31+
apiKey,
32+
createdAt: new Date().toISOString(),
33+
};
34+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 0o600 });
35+
}
36+
37+
export function clearCredentials(): boolean {
38+
if (!existsSync(CREDENTIALS_FILE)) return false;
39+
rmSync(CREDENTIALS_FILE);
40+
return true;
41+
}
42+
43+
function openBrowser(url: string): void {
44+
const platform = process.platform;
45+
46+
const commands: Record<string, string> = {
47+
darwin: `open "${url}"`,
48+
win32: `start "" "${url}"`,
49+
linux: `xdg-open "${url}"`,
50+
};
51+
52+
const cmd = commands[platform] ?? `xdg-open "${url}"`;
53+
exec(cmd, (err) => {
54+
if (err) console.error("Failed to open browser:", err.message);
55+
});
56+
}
57+
58+
export interface AuthResult {
59+
success: boolean;
60+
apiKey?: string;
61+
error?: string;
62+
}
63+
64+
export function startAuthFlow(timeoutMs = 120000): Promise<AuthResult> {
65+
return new Promise((resolve) => {
66+
let resolved = false;
67+
68+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
69+
if (resolved) return;
70+
71+
const url = new URL(req.url || "/", `http://localhost:${AUTH_PORT}`);
72+
73+
if (url.pathname === "/callback") {
74+
const apiKey = url.searchParams.get("apikey");
75+
76+
if (apiKey) {
77+
saveCredentials(apiKey);
78+
res.writeHead(200, { "Content-Type": "text/html" });
79+
res.end(`
80+
<!DOCTYPE html>
81+
<html>
82+
<head><title>Success</title></head>
83+
<body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa;">
84+
<div style="text-align: center;">
85+
<h1 style="color: #22c55e;">✓ Connected!</h1>
86+
<p>You can close this window and return to your terminal.</p>
87+
</div>
88+
</body>
89+
</html>
90+
`);
91+
resolved = true;
92+
server.close();
93+
resolve({ success: true, apiKey });
94+
} else {
95+
res.writeHead(400, { "Content-Type": "text/html" });
96+
res.end(`
97+
<!DOCTYPE html>
98+
<html>
99+
<head><title>Error</title></head>
100+
<body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa;">
101+
<div style="text-align: center;">
102+
<h1 style="color: #ef4444;">✗ Connection Failed</h1>
103+
<p>No API key received. Please try again.</p>
104+
</div>
105+
</body>
106+
</html>
107+
`);
108+
resolved = true;
109+
server.close();
110+
resolve({ success: false, error: "No API key received" });
111+
}
112+
} else {
113+
res.writeHead(404);
114+
res.end("Not Found");
115+
}
116+
});
117+
118+
server.on("error", (err: NodeJS.ErrnoException) => {
119+
if (err.code === "EADDRINUSE") {
120+
resolve({ success: false, error: `Port ${AUTH_PORT} is already in use` });
121+
} else {
122+
resolve({ success: false, error: err.message });
123+
}
124+
});
125+
126+
server.listen(AUTH_PORT, () => {
127+
const callbackUrl = `http://localhost:${AUTH_PORT}/callback`;
128+
const authUrl = `${AUTH_BASE_URL}?callback=${encodeURIComponent(callbackUrl)}&client=${CLIENT_NAME}`;
129+
130+
console.log("Opening browser for authentication...");
131+
console.log(`If it doesn't open, visit: ${authUrl}`);
132+
openBrowser(authUrl);
133+
});
134+
135+
setTimeout(() => {
136+
if (!resolved) {
137+
resolved = true;
138+
server.close();
139+
resolve({ success: false, error: "Authentication timed out" });
140+
}
141+
}, timeoutMs);
142+
});
143+
}

0 commit comments

Comments
 (0)