From b1176be9c79f84b918315024ad17573859c20d68 Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Mon, 4 May 2026 10:11:17 -0700 Subject: [PATCH 1/2] added browser auth with fallback --- README.md | 6 +- auth.ts | 218 ++++++++++++++++++++++++++++++++++++++++++++++ commands/cli.ts | 75 ++++++++-------- commands/slash.ts | 4 +- index.ts | 2 +- 5 files changed, 258 insertions(+), 47 deletions(-) create mode 100644 auth.ts diff --git a/README.md b/README.md index 1ea38e6..9205793 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ Restart OpenClaw after installing. ## Setup ```bash -openclaw supermemory setup +openclaw supermemory login ``` -Enter your API key from [app.supermemory.ai](https://app.supermemory.ai/?view=integrations). That's it. +Opens your browser to authenticate with Supermemory. The API key is saved automatically. ### Advanced Setup @@ -61,7 +61,7 @@ The AI uses these tools autonomously. With custom container tags enabled, all to ## CLI Commands ```bash -openclaw supermemory setup # Configure API key +openclaw supermemory login # Authenticate with Supermemory openclaw supermemory setup-advanced # Configure all options openclaw supermemory status # View current configuration openclaw supermemory search # Search memories diff --git a/auth.ts b/auth.ts new file mode 100644 index 0000000..987f683 --- /dev/null +++ b/auth.ts @@ -0,0 +1,218 @@ +import { execFile } from "node:child_process" +import { randomBytes } from "node:crypto" +import * as fs from "node:fs" +import { + createServer, + type IncomingMessage, + type ServerResponse, +} from "node:http" +import type { AddressInfo } from "node:net" +import { arch, homedir, hostname, platform } from "node:os" +import * as path from "node:path" + +const CONFIG_DIR = path.join(homedir(), ".openclaw") +const CONFIG_FILE = path.join(CONFIG_DIR, "openclaw.json") +const AUTH_BASE_URL = + process.env.SUPERMEMORY_AUTH_URL || + "https://console.supermemory.ai/auth/agent-connect" +const AUTH_TIMEOUT = Number(process.env.SUPERMEMORY_AUTH_TIMEOUT) || 60_000 +const API_URL = process.env.SUPERMEMORY_API_URL || "https://api.supermemory.ai" + +export async function validateApiKey( + apiKey: string, +): Promise<{ valid: boolean; error?: string }> { + try { + const res = await fetch(`${API_URL}/v3/session`, { + headers: { Authorization: `Bearer ${apiKey}` }, + signal: AbortSignal.timeout(8000), + }) + if (res.status === 401 || res.status === 403) { + return { valid: false, error: "Invalid API key" } + } + return { valid: true } + } catch { + return { valid: true } // network error — don't block, key format was already checked + } +} + +export function saveApiKey(apiKey: string): void { + let config: Record = {} + if (fs.existsSync(CONFIG_FILE)) { + try { + config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")) + } catch { + config = {} + } + } + + if (!config.plugins) config.plugins = {} + const plugins = config.plugins as Record + if (!plugins.entries) plugins.entries = {} + const entries = plugins.entries as Record + + const existing = + (entries["openclaw-supermemory"] as Record) ?? {} + entries["openclaw-supermemory"] = { + ...existing, + enabled: true, + config: { + ...((existing.config as Record) ?? {}), + apiKey, + }, + } + + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 }) + } + fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { + mode: 0o600, + }) +} + +function openBrowser(url: string): void { + const onError = (err: Error | null) => { + if (err) console.error("Failed to open browser:", err.message) + } + if (process.platform === "win32") { + execFile("explorer.exe", [url], onError) + } else if (process.platform === "darwin") { + execFile("open", [url], onError) + } else { + execFile("xdg-open", [url], onError) + } +} + +export interface AuthResult { + success: boolean + apiKey?: string + error?: string +} + +export function startAuthFlow(): Promise { + return new Promise((resolve) => { + let resolved = false + const stateToken = randomBytes(16).toString("hex") + + const server = createServer((req: IncomingMessage, res: ServerResponse) => { + if (resolved) return + + const url = new URL(req.url || "/", "http://localhost") + + if (url.pathname === "/callback") { + const callbackState = url.searchParams.get("state") + if (callbackState !== stateToken) { + res.writeHead(403, { "Content-Type": "text/html" }) + res.end(errorHtml("Invalid state token")) + return + } + + const apiKey = + url.searchParams.get("apikey") || url.searchParams.get("api_key") + + if (apiKey?.startsWith("sm_")) { + // Validate the key before saving + validateApiKey(apiKey).then(({ valid, error: validationError }) => { + if (!valid) { + res.writeHead(400, { + "Content-Type": "text/html", + "Referrer-Policy": "no-referrer", + }) + res.end(errorHtml(validationError || "Invalid API key")) + resolved = true + clearTimeout(timer) + server.close() + resolve({ + success: false, + error: validationError || "Invalid API key", + }) + return + } + saveApiKey(apiKey) + res.writeHead(200, { + "Content-Type": "text/html", + "Referrer-Policy": "no-referrer", + }) + res.end(successHtml) + resolved = true + clearTimeout(timer) + server.close() + resolve({ success: true, apiKey }) + }) + } else { + res.writeHead(400, { + "Content-Type": "text/html", + "Referrer-Policy": "no-referrer", + }) + res.end(errorHtml("No API key received")) + resolved = true + clearTimeout(timer) + server.close() + resolve({ success: false, error: "No API key received" }) + } + } else { + res.writeHead(404) + res.end("Not Found") + } + }) + + server.on("error", (err: Error) => { + if (!resolved) { + resolved = true + clearTimeout(timer) + resolve({ success: false, error: err.message }) + } + }) + + // Listen on an ephemeral port; embed state token in callback URL so the + // console redirects it back and the CSRF check passes. + server.listen(0, "127.0.0.1", () => { + const { port } = server.address() as AddressInfo + const callbackUrl = `http://localhost:${port}/callback?state=${stateToken}` + const params = new URLSearchParams({ + callback: callbackUrl, + client: "openclaw", + hostname: hostname(), + os: `${platform()}-${arch()}`, + cwd: process.cwd(), + cli_version: "1.0.0", + }) + const authUrl = `${AUTH_BASE_URL}?${params.toString()}` + + console.log("Opening browser for authentication...") + console.log(`If it doesn't open, visit: ${authUrl}`) + openBrowser(authUrl) + }) + + const timer = setTimeout(() => { + if (!resolved) { + resolved = true + server.close() + resolve({ success: false, error: "Authentication timed out" }) + } + }, AUTH_TIMEOUT) + }) +} + +const successHtml = ` + +Success + +
+

āœ“ Connected!

+

You can close this window and return to your terminal.

+
+ +` + +function errorHtml(message: string): string { + return ` + +Error + +
+

āœ— Connection Failed

+

${message}. Please try again.

+
+ +` +} diff --git a/commands/cli.ts b/commands/cli.ts index c38e552..ace41ba 100644 --- a/commands/cli.ts +++ b/commands/cli.ts @@ -3,6 +3,7 @@ import * as os from "node:os" import * as path from "node:path" import * as readline from "node:readline" import type { OpenClawPluginApi } from "openclaw/plugin-sdk" +import { saveApiKey, startAuthFlow, validateApiKey } from "../auth.ts" import type { SupermemoryClient } from "../client.ts" import type { SupermemoryConfig } from "../config.ts" import { log } from "../logger.ts" @@ -16,62 +17,54 @@ export function registerCliSetup(api: OpenClawPluginApi): void { .description("Supermemory long-term memory commands") cmd - .command("setup") - .description("Configure Supermemory API key") + .command("login") + .description("Connect Supermemory via browser login") .action(async () => { - const configDir = path.join(os.homedir(), ".openclaw") - const configPath = path.join(configDir, "openclaw.json") + console.log("\n🧠 Supermemory Login\n") + console.log("Opening browser for authentication...") - console.log("\n🧠 Supermemory Setup\n") - console.log("Get your API key from: https://app.supermemory.ai\n") + const result = await startAuthFlow() + + if (result.success) { + console.log("\nāœ“ Successfully authenticated with Supermemory!") + console.log( + " Restart OpenClaw to apply changes: openclaw gateway --force\n", + ) + return + } + + // Browser auth failed — fall back to manual paste + console.log(`\nBrowser authentication failed (${result.error}).`) + console.log("You can paste your API key manually instead.") + console.log("Get your API key from: https://console.supermemory.ai\n") const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }) - const apiKey = await new Promise((resolve) => { rl.question("Enter your Supermemory API key: ", resolve) }) rl.close() - if (!apiKey.trim()) { - console.log("\nNo API key provided. Setup cancelled.") - return - } - - if (!apiKey.startsWith("sm_")) { - console.log("\nWarning: API key should start with 'sm_'") - } - - let config: Record = {} - if (fs.existsSync(configPath)) { - try { - config = JSON.parse(fs.readFileSync(configPath, "utf-8")) - } catch { - config = {} - } - } - - if (!config.plugins) config.plugins = {} - const plugins = config.plugins as Record - if (!plugins.entries) plugins.entries = {} - const entries = plugins.entries as Record - - entries["openclaw-supermemory"] = { - enabled: true, - config: { - apiKey: apiKey.trim(), - }, + if (!apiKey.trim()?.startsWith("sm_")) { + console.error("\nāœ— Invalid or missing API key.\n") + process.exit(1) } - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }) + console.log("Validating API key...") + const { valid, error: validationError } = await validateApiKey( + apiKey.trim(), + ) + if (!valid) { + console.error( + `\nāœ— ${validationError}. Please check your key and try again.\n`, + ) + process.exit(1) } - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)) - - console.log("\nāœ“ API key saved to ~/.openclaw/openclaw.json") + saveApiKey(apiKey.trim()) + console.log("\nāœ“ API key saved!") console.log( " Restart OpenClaw to apply changes: openclaw gateway --force\n", ) @@ -378,7 +371,7 @@ export function registerCliSetup(api: OpenClawPluginApi): void { if (!apiKeyDisplay) { console.log("āœ— No API key configured") - console.log(" Run: openclaw supermemory setup\n") + console.log(" Run: openclaw supermemory login\n") return } diff --git a/commands/slash.ts b/commands/slash.ts index ff7e74f..5a85492 100644 --- a/commands/slash.ts +++ b/commands/slash.ts @@ -12,7 +12,7 @@ export function registerStubCommands(api: OpenClawPluginApi): void { requireAuth: true, handler: async () => { return { - text: "Supermemory not configured. Run 'openclaw supermemory setup' first.", + text: "Supermemory not configured. Run 'openclaw supermemory login' first.", } }, }) @@ -24,7 +24,7 @@ export function registerStubCommands(api: OpenClawPluginApi): void { requireAuth: true, handler: async () => { return { - text: "Supermemory not configured. Run 'openclaw supermemory setup' first.", + text: "Supermemory not configured. Run 'openclaw supermemory login' first.", } }, }) diff --git a/index.ts b/index.ts index d7e79e7..b4f506f 100644 --- a/index.ts +++ b/index.ts @@ -41,7 +41,7 @@ export default { if (!cfg.apiKey) { api.logger.info( - "supermemory: not configured - run 'openclaw supermemory setup'", + "supermemory: not configured - run 'openclaw supermemory login'", ) registerStubCommands(api) return From 39d6d9d03f39061748c0fc2fd386e58d5d10c3ec Mon Sep 17 00:00:00 2001 From: Sreeram Sreedhar Date: Thu, 7 May 2026 17:04:02 -0700 Subject: [PATCH 2/2] addded browserless flag --- README.md | 11 ++++++++++- commands/cli.ts | 29 ++++++++++++++++------------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9205793..b0cf91f 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,14 @@ openclaw supermemory login Opens your browser to authenticate with Supermemory. The API key is saved automatically. +For headless servers or SSH sessions: + +```bash +openclaw supermemory login --browserless +``` + +Prompts you to paste your API key directly. Get your key from [console.supermemory.ai](https://console.supermemory.ai). + ### Advanced Setup ```bash @@ -61,7 +69,8 @@ The AI uses these tools autonomously. With custom container tags enabled, all to ## CLI Commands ```bash -openclaw supermemory login # Authenticate with Supermemory +openclaw supermemory login # Authenticate via browser +openclaw supermemory login --browserless # Paste API key directly (for SSH/headless) openclaw supermemory setup-advanced # Configure all options openclaw supermemory status # View current configuration openclaw supermemory search # Search memories diff --git a/commands/cli.ts b/commands/cli.ts index ace41ba..51bfe3a 100644 --- a/commands/cli.ts +++ b/commands/cli.ts @@ -19,23 +19,26 @@ export function registerCliSetup(api: OpenClawPluginApi): void { cmd .command("login") .description("Connect Supermemory via browser login") - .action(async () => { + .option("--browserless", "Skip browser auth and paste API key directly") + .action(async (opts: { browserless?: boolean }) => { console.log("\n🧠 Supermemory Login\n") - console.log("Opening browser for authentication...") - const result = await startAuthFlow() + if (!opts.browserless) { + const result = await startAuthFlow() - if (result.success) { - console.log("\nāœ“ Successfully authenticated with Supermemory!") - console.log( - " Restart OpenClaw to apply changes: openclaw gateway --force\n", - ) - return + if (result.success) { + console.log("\nāœ“ Successfully authenticated with Supermemory!") + console.log( + " Restart OpenClaw to apply changes: openclaw gateway --force\n", + ) + return + } + + // Browser auth failed — fall back to manual paste + console.log(`\nBrowser authentication failed (${result.error}).`) } - // Browser auth failed — fall back to manual paste - console.log(`\nBrowser authentication failed (${result.error}).`) - console.log("You can paste your API key manually instead.") + console.log("You can paste your API key manually.") console.log("Get your API key from: https://console.supermemory.ai\n") const rl = readline.createInterface({ @@ -47,7 +50,7 @@ export function registerCliSetup(api: OpenClawPluginApi): void { }) rl.close() - if (!apiKey.trim()?.startsWith("sm_")) { + if (!apiKey.trim().startsWith("sm_")) { console.error("\nāœ— Invalid or missing API key.\n") process.exit(1) }