Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@ 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.

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

Expand Down Expand Up @@ -61,7 +69,8 @@ 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 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 <query> # Search memories
Expand Down
218 changes: 218 additions & 0 deletions auth.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = {}
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<string, unknown>
if (!plugins.entries) plugins.entries = {}
const entries = plugins.entries as Record<string, unknown>

const existing =
(entries["openclaw-supermemory"] as Record<string, unknown>) ?? {}
entries["openclaw-supermemory"] = {
...existing,
enabled: true,
config: {
...((existing.config as Record<string, unknown>) ?? {}),
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<AuthResult> {
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")
Comment thread
sreedharsreeram marked this conversation as resolved.

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 })
Comment thread
sreedharsreeram marked this conversation as resolved.
}
})

// 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 = `<!DOCTYPE html>
<html>
<head><title>Success</title></head>
<body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa;">
<div style="text-align: center;">
<h1 style="color: #22c55e;">✓ Connected!</h1>
<p>You can close this window and return to your terminal.</p>
</div>
</body>
</html>`

function errorHtml(message: string): string {
return `<!DOCTYPE html>
<html>
<head><title>Error</title></head>
<body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa;">
<div style="text-align: center;">
<h1 style="color: #ef4444;">✗ Connection Failed</h1>
<p>${message}. Please try again.</p>
</div>
</body>
</html>`
}
80 changes: 38 additions & 42 deletions commands/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -16,62 +17,57 @@ export function registerCliSetup(api: OpenClawPluginApi): void {
.description("Supermemory long-term memory commands")

cmd
.command("setup")
.description("Configure Supermemory API key")
.action(async () => {
const configDir = path.join(os.homedir(), ".openclaw")
const configPath = path.join(configDir, "openclaw.json")
.command("login")
Comment thread
sreedharsreeram marked this conversation as resolved.
.description("Connect Supermemory via browser login")
.option("--browserless", "Skip browser auth and paste API key directly")
.action(async (opts: { browserless?: boolean }) => {
console.log("\n🧠 Supermemory Login\n")

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
}

console.log("\n🧠 Supermemory Setup\n")
console.log("Get your API key from: https://app.supermemory.ai\n")
// Browser auth failed — fall back to manual paste
console.log(`\nBrowser authentication failed (${result.error}).`)
}

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({
input: process.stdin,
output: process.stdout,
})

const apiKey = await new Promise<string>((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_'")
if (!apiKey.trim().startsWith("sm_")) {
console.error("\n✗ Invalid or missing API key.\n")
process.exit(1)
}

let config: Record<string, unknown> = {}
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<string, unknown>
if (!plugins.entries) plugins.entries = {}
const entries = plugins.entries as Record<string, unknown>

entries["openclaw-supermemory"] = {
enabled: true,
config: {
apiKey: apiKey.trim(),
},
}

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",
)
Expand Down Expand Up @@ -378,7 +374,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
}

Expand Down
4 changes: 2 additions & 2 deletions commands/slash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
}
},
})
Expand All @@ -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.",
}
},
})
Expand Down
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down