Skip to content

Commit 3288ff6

Browse files
committed
Add OpenCode OAuth status flow
1 parent 4c4566f commit 3288ff6

5 files changed

Lines changed: 268 additions & 33 deletions

File tree

README.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@ Your agent remembers what you tell it - across sessions, across projects.
1212
bunx opencode-supermemory@latest install
1313
```
1414

15-
Then get your API key from [app.supermemory.ai](https://app.supermemory.ai/?view=integrations) and set it:
15+
Then authenticate with Supermemory:
1616

1717
```bash
18-
export SUPERMEMORY_API_KEY="sm_..."
18+
bunx opencode-supermemory@latest login
19+
```
20+
21+
Check the connection any time:
22+
23+
```bash
24+
bunx opencode-supermemory@latest status
1925
```
2026

2127
**Or let your agent do it** - paste this into OpenCode:
@@ -77,17 +83,15 @@ If not, add it manually:
7783
}
7884
```
7985

80-
#### Step 3: Configure API key
81-
82-
Ask the user to get their API key from [app.supermemory.ai](https://app.supermemory.ai/?view=integrations).
86+
#### Step 3: Authenticate
8387

84-
Then set it via environment variable:
88+
Run the browser authentication flow:
8589

8690
```bash
87-
export SUPERMEMORY_API_KEY="sm_..."
91+
bunx opencode-supermemory@latest login
8892
```
8993

90-
Or create `~/.config/opencode/supermemory.jsonc`:
94+
For headless environments, users can still set `SUPERMEMORY_API_KEY` manually or create `~/.config/opencode/supermemory.jsonc`:
9195

9296
```jsonc
9397
{
@@ -100,12 +104,12 @@ Or create `~/.config/opencode/supermemory.jsonc`:
100104
Tell the user to restart OpenCode and run:
101105

102106
```bash
103-
opencode -c
107+
bunx opencode-supermemory@latest status
104108
```
105109

106-
They should see `supermemory` in the tools list. If not, check:
110+
If it is not connected, check:
107111

108-
1. Is `SUPERMEMORY_API_KEY` set?
112+
1. Is the user authenticated, or is `SUPERMEMORY_API_KEY` set?
109113
2. Is the plugin in `opencode.jsonc`?
110114
3. Check logs: `tail ~/.opencode-supermemory.log`
111115

src/cli.ts

Lines changed: 170 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ 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";
8-
import { writeInstallDefaults, CONFIG_FILE } from "./config.js";
7+
import { startAuthFlow, clearCredentials, loadCredentials, CREDENTIALS_FILE } from "./services/auth.js";
8+
import { CONFIG, CONFIG_FILE, SUPERMEMORY_API_KEY, getApiBaseUrl, isConfigured, writeInstallDefaults } from "./config.js";
9+
import { SupermemoryClient } from "./services/client.js";
10+
import { getTags } from "./services/tags.js";
911

1012
const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode");
1113
const OPENCODE_COMMAND_DIR = join(OPENCODE_CONFIG_DIR, "command");
@@ -204,6 +206,23 @@ This will remove the saved credentials from ~/.supermemory-opencode/credentials.
204206
Inform the user whether logout succeeded and that they'll need to run /supermemory-login to re-authenticate.
205207
`;
206208

209+
const SUPERMEMORY_STATUS_COMMAND = `---
210+
description: Show Supermemory connection status
211+
---
212+
213+
# Supermemory Status
214+
215+
Run this command to check whether OpenCode is connected to Supermemory:
216+
217+
\`\`\`bash
218+
bunx opencode-supermemory@latest status
219+
\`\`\`
220+
221+
Report the connection status, credential source, API URL, and account information if available.
222+
223+
Never print the full API key.
224+
`;
225+
207226
function createReadline(): readline.Interface {
208227
return readline.createInterface({
209228
input: process.stdin,
@@ -318,6 +337,10 @@ function createCommands(): boolean {
318337
writeFileSync(logoutPath, SUPERMEMORY_LOGOUT_COMMAND);
319338
console.log(`✓ Created /supermemory-logout command`);
320339

340+
const statusPath = join(OPENCODE_COMMAND_DIR, "supermemory-status.md");
341+
writeFileSync(statusPath, SUPERMEMORY_STATUS_COMMAND);
342+
console.log(`✓ Created /supermemory-status command`);
343+
321344
return true;
322345
}
323346

@@ -411,7 +434,7 @@ async function install(options: InstallOptions): Promise<number> {
411434
}
412435

413436
// Step 2: Create commands
414-
console.log("\nStep 2: Create /supermemory-init, /supermemory-login, and /supermemory-logout commands");
437+
console.log("\nStep 2: Create /supermemory-init, /supermemory-login, /supermemory-logout, and /supermemory-status commands");
415438
if (options.tui) {
416439
const shouldCreate = await confirm(rl!, "Add supermemory commands?");
417440
if (!shouldCreate) {
@@ -486,12 +509,152 @@ async function login(): Promise<number> {
486509
}
487510
}
488511

512+
function maskKey(key: string | undefined): string {
513+
if (!key) return "not set";
514+
if (key.length <= 12) return `${key.slice(0, 4)}...`;
515+
return `${key.slice(0, 6)}...${key.slice(-4)}`;
516+
}
517+
518+
function getConfiguredApiKeyFromFile(): string | undefined {
519+
try {
520+
if (!existsSync(DEFAULT_CONFIG_FILE)) return undefined;
521+
const parsed = JSON.parse(readFileSync(DEFAULT_CONFIG_FILE, "utf-8")) as { apiKey?: string };
522+
return parsed.apiKey;
523+
} catch {
524+
return undefined;
525+
}
526+
}
527+
528+
function getKeySource(): string {
529+
if (process.env.SUPERMEMORY_API_KEY) return "SUPERMEMORY_API_KEY env var";
530+
if (getConfiguredApiKeyFromFile()) return DEFAULT_CONFIG_FILE;
531+
if (loadCredentials()) return CREDENTIALS_FILE;
532+
return "not configured";
533+
}
534+
535+
function getDevTlsHint(apiUrl: string): string | null {
536+
if (!apiUrl.includes(".dev.supermemory.ai")) return null;
537+
if (process.env.NODE_EXTRA_CA_CERTS) return null;
538+
return "Dev API TLS: set NODE_EXTRA_CA_CERTS to your Portless CA before starting OpenCode.";
539+
}
540+
541+
async function fetchJson(apiUrl: string, path: string): Promise<unknown | null> {
542+
if (!SUPERMEMORY_API_KEY) return null;
543+
try {
544+
const response = await fetch(`${apiUrl}${path}`, {
545+
headers: {
546+
Authorization: `Bearer ${SUPERMEMORY_API_KEY}`,
547+
"x-sm-source": "opencode",
548+
},
549+
});
550+
if (!response.ok) return null;
551+
return await response.json();
552+
} catch {
553+
return null;
554+
}
555+
}
556+
557+
function findAccountInfo(value: unknown): { email?: string; name?: string; userId?: string; orgName?: string } {
558+
const seen = new Set<unknown>();
559+
const stack = [value];
560+
const result: { email?: string; name?: string; userId?: string; orgName?: string } = {};
561+
562+
while (stack.length > 0) {
563+
const item = stack.pop();
564+
if (!item || typeof item !== "object" || seen.has(item)) continue;
565+
seen.add(item);
566+
567+
const record = item as Record<string, unknown>;
568+
for (const [key, raw] of Object.entries(record)) {
569+
const lower = key.toLowerCase();
570+
if (!result.email && lower === "email" && typeof raw === "string") result.email = raw;
571+
if (!result.name && lower === "name" && typeof raw === "string") result.name = raw;
572+
if (!result.userId && (lower === "userid" || lower === "user_id") && typeof raw === "string") result.userId = raw;
573+
if (!result.orgName && (lower === "organizationname" || lower === "orgname") && typeof raw === "string") result.orgName = raw;
574+
575+
if (raw && typeof raw === "object") stack.push(raw);
576+
}
577+
}
578+
579+
return result;
580+
}
581+
582+
async function getAccountInfo(apiUrl: string): Promise<{ email?: string; name?: string; userId?: string; orgName?: string }> {
583+
for (const path of ["/v3/auth/account/memberships", "/v3/account/memberships", "/v3/me"]) {
584+
const data = await fetchJson(apiUrl, path);
585+
const info = findAccountInfo(data);
586+
if (info.email || info.name || info.userId || info.orgName) return info;
587+
}
588+
return {};
589+
}
590+
591+
async function status(): Promise<number> {
592+
const apiUrl = getApiBaseUrl();
593+
const tags = getTags(process.cwd());
594+
const lines: string[] = [];
595+
596+
lines.push("supermemory status");
597+
lines.push("");
598+
lines.push(`Connected: ${isConfigured() ? "checking..." : "no"}`);
599+
lines.push(`API key: ${maskKey(SUPERMEMORY_API_KEY)} (${getKeySource()})`);
600+
lines.push(`API URL: ${apiUrl}`);
601+
lines.push("Memory scope: current project + user profile");
602+
lines.push(`Recall mode: ${CONFIG.autoRecallEveryPrompt ? "auto-recall on every prompt" : "session/event based"}`);
603+
lines.push(`Capture cadence: ${CONFIG.captureEveryNTurns > 0 ? `every ${CONFIG.captureEveryNTurns} turn${CONFIG.captureEveryNTurns === 1 ? "" : "s"} + session end` : "session end only"}`);
604+
lines.push(`Project tag: ${tags.project}`);
605+
lines.push(`User tag: ${tags.user}`);
606+
607+
if (!isConfigured()) {
608+
lines.push("");
609+
lines.push("Run /supermemory-login to connect, or set SUPERMEMORY_API_KEY.");
610+
console.log(lines.join("\n"));
611+
return 0;
612+
}
613+
614+
const client = new SupermemoryClient();
615+
const [profileResult, accountInfo] = await Promise.all([
616+
client.getProfile(tags.user),
617+
getAccountInfo(apiUrl),
618+
]);
619+
620+
lines[2] = profileResult.success ? "Connected: yes" : "Connected: no";
621+
622+
if (accountInfo.email || accountInfo.name || accountInfo.userId || accountInfo.orgName) {
623+
lines.push("");
624+
lines.push("Account:");
625+
if (accountInfo.email) lines.push(`Email: ${accountInfo.email}`);
626+
if (accountInfo.name) lines.push(`Name: ${accountInfo.name}`);
627+
if (accountInfo.userId) lines.push(`User ID: ${accountInfo.userId}`);
628+
if (accountInfo.orgName) lines.push(`Organization: ${accountInfo.orgName}`);
629+
} else {
630+
lines.push("");
631+
lines.push("Account: authenticated API key (account details unavailable from API key)");
632+
}
633+
634+
if (!profileResult.success) {
635+
lines.push("");
636+
lines.push(`Connection check failed: ${profileResult.error}`);
637+
const devTlsHint = getDevTlsHint(apiUrl);
638+
if (devTlsHint) lines.push(devTlsHint);
639+
}
640+
641+
console.log(lines.join("\n"));
642+
return 0;
643+
}
644+
489645
function logout(): number {
490646
if (clearCredentials()) {
491647
console.log("✓ Logged out. Credentials cleared.");
648+
console.log("This only logs out this local OpenCode install. To revoke the account-level OpenCode integration key, disconnect it from the Supermemory integrations page.");
649+
if (process.env.SUPERMEMORY_API_KEY) {
650+
console.log("SUPERMEMORY_API_KEY is still set in this shell, so memory may remain active until you unset it or restart OpenCode.");
651+
}
492652
return 0;
493653
} else {
494654
console.log("No credentials found.");
655+
if (process.env.SUPERMEMORY_API_KEY) {
656+
console.log("SUPERMEMORY_API_KEY is still set in this shell.");
657+
}
495658
return 0;
496659
}
497660
}
@@ -506,11 +669,13 @@ Commands:
506669
--disable-context-recovery Disable Oh My OpenCode's context hook
507670
login Authenticate with Supermemory (opens browser)
508671
logout Clear stored credentials
672+
status Show Supermemory connection status
509673
510674
Examples:
511675
bunx opencode-supermemory@latest install
512676
bunx opencode-supermemory@latest login
513677
bunx opencode-supermemory@latest logout
678+
bunx opencode-supermemory@latest status
514679
`);
515680
}
516681

@@ -534,6 +699,8 @@ if (args[0] === "install") {
534699
login().then((code) => process.exit(code));
535700
} else if (args[0] === "logout") {
536701
process.exit(logout());
702+
} else if (args[0] === "status") {
703+
status().then((code) => process.exit(code));
537704
} else {
538705
console.error(`Unknown command: ${args[0]}`);
539706
printHelp();

src/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ function getApiKey(): string | undefined {
101101
}
102102

103103
export const SUPERMEMORY_API_KEY = getApiKey();
104+
export function getApiBaseUrl(): string {
105+
return (
106+
process.env.SUPERMEMORY_API_URL ||
107+
process.env.SUPERMEMORY_BASE_URL ||
108+
loadCredentials()?.apiBaseUrl ||
109+
"https://api.supermemory.ai"
110+
);
111+
}
104112
export const CONFIG_FILE = CONFIG_FILES[1];
105113
const DEFAULT_CONFIG_FILE = CONFIG_FILE ?? join(CONFIG_DIR, "supermemory.json");
106114

0 commit comments

Comments
 (0)