From 73135283386bfd4ca947bd206abcd6d2d5993f75 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Wed, 3 Jun 2026 17:23:34 +0530 Subject: [PATCH 1/3] Fix Windows auth URL opener --- src/services/auth.ts | 21 ++++++++------------- src/services/openUrl.ts | 34 ++++++++++++++++++++++++++++++++++ test/unit.mjs | 35 +++++++++++++++++++++++------------ 3 files changed, 65 insertions(+), 25 deletions(-) create mode 100644 src/services/openUrl.ts diff --git a/src/services/auth.ts b/src/services/auth.ts index 9e23455..a140777 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -2,9 +2,9 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { homedir, hostname, platform, arch } from "node:os"; -import { execFile } from "node:child_process"; import { randomBytes } from "node:crypto"; import type { AddressInfo } from "node:net"; +import { openUrl } from "./openUrl.js"; const SUPERMEMORY_DIR = join(homedir(), ".codex", "supermemory"); const CREDENTIALS_FILE = join(SUPERMEMORY_DIR, "credentials.json"); @@ -60,17 +60,6 @@ function saveCredentials(apiKey: string): void { ); } -function openBrowser(url: string): void { - const onError = () => {}; - 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 function startAuthFlow(): Promise { return new Promise((resolve, reject) => { let resolved = false; @@ -122,7 +111,13 @@ export function startAuthFlow(): Promise { cli_version: "1.0.0", }); const authUrl = `${AUTH_BASE_URL}?${params.toString()}`; - openBrowser(authUrl); + openUrl(authUrl).catch((error) => { + if (!resolved) { + clearTimeout(timer); + server.close(); + reject(new Error(`Failed to open browser: ${error.message}`)); + } + }); }); server.on("error", (err) => { diff --git a/src/services/openUrl.ts b/src/services/openUrl.ts new file mode 100644 index 0000000..dae8053 --- /dev/null +++ b/src/services/openUrl.ts @@ -0,0 +1,34 @@ +import { execFile } from "node:child_process"; + +function run(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + execFile(command, args, { windowsHide: true }, (error) => { + if (error) reject(error); + else resolve(); + }); + }); +} + +export async function openUrl(url: string | URL): Promise { + const href = url.toString(); + if (!/^https?:\/\//i.test(href)) { + throw new Error("Refusing to open non-http URL"); + } + + if (process.platform === "win32") { + try { + await run("rundll32.exe", ["url.dll,FileProtocolHandler", href]); + return; + } catch {} + + await run("cmd.exe", ["/c", "start", '""', href]); + return; + } + + if (process.platform === "darwin") { + await run("open", [href]); + return; + } + + await run("xdg-open", [href]); +} diff --git a/test/unit.mjs b/test/unit.mjs index f481f3f..caaf08d 100644 --- a/test/unit.mjs +++ b/test/unit.mjs @@ -8,6 +8,7 @@ import { spawnSync } from "node:child_process"; import { writeFileSync, readFileSync, mkdirSync, rmSync, existsSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; import * as TOML from "@iarna/toml"; // ─── helpers ──────────────────────────────────────────────────────────────── @@ -32,7 +33,7 @@ function setupCodexHome(t) { function runCli(cliBin, cmd, tmpDir) { return spawnSync("node", [cliBin, cmd], { - env: { ...process.env, HOME: tmpDir, SUPERMEMORY_CODEX_API_KEY: "sm_test" }, + env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir, SUPERMEMORY_CODEX_API_KEY: "sm_test" }, encoding: "utf-8", }); } @@ -76,6 +77,16 @@ describe("stripPrivateContent", () => { }); }); +describe("browser auth opener", () => { + test("login bundle uses Windows-safe URL opener", () => { + const content = readFileSync(new URL("../dist/skills/login.js", import.meta.url), "utf-8"); + assert.ok(content.includes("Refusing to open non-http URL")); + assert.ok(content.includes("rundll32.exe")); + assert.ok(content.includes("url.dll,FileProtocolHandler")); + assert.ok(!content.includes("explorer.exe")); + }); +}); + // ─── hooks.json format ────────────────────────────────────────────────────── describe("hooks.json format", () => { @@ -180,7 +191,7 @@ describe("hooks.json format", () => { // through npm. describe("integration: install/uninstall", () => { - const cliBin = new URL("../dist/cli.js", import.meta.url).pathname; + const cliBin = fileURLToPath(new URL("../dist/cli.js", import.meta.url)); test("install copies skill SKILL.md files to ~/.codex/skills/", (t) => { const { tmpDir, codexDir } = setupCodexHome(t); @@ -236,7 +247,7 @@ describe("integration: install/uninstall", () => { // ─── recall hook output envelope ──────────────────────────────────────────── describe("recall hook output envelope", () => { - const recallBin = new URL("../dist/hooks/recall.js", import.meta.url).pathname; + const recallBin = fileURLToPath(new URL("../dist/hooks/recall.js", import.meta.url)); // Helper: run recall hook with an isolated HOME and a short auth timeout so // the first-invocation browser flow times out in 2s rather than 60s. @@ -247,7 +258,7 @@ describe("recall hook output envelope", () => { return spawnSync("node", [recallBin], { input, // Use a 2s auth timeout so the browser flow times out quickly in CI. - env: { ...process.env, HOME: tmpDir, SUPERMEMORY_CODEX_API_KEY: "", SUPERMEMORY_AUTH_TIMEOUT: "2000" }, + env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir, SUPERMEMORY_CODEX_API_KEY: "", SUPERMEMORY_AUTH_TIMEOUT: "2000" }, encoding: "utf-8", timeout: 5_000, }); @@ -292,7 +303,7 @@ describe("recall hook output envelope", () => { const result = spawnSync("node", [recallBin], { input: JSON.stringify({ prompt: "test" }), - env: { ...process.env, HOME: tmpDir, SUPERMEMORY_CODEX_API_KEY: "" }, + env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir, SUPERMEMORY_CODEX_API_KEY: "" }, encoding: "utf-8", }); const parsed = JSON.parse(result.stdout); @@ -309,7 +320,7 @@ describe("recall hook output envelope", () => { const result = spawnSync("node", [recallBin], { input: JSON.stringify({ prompt: "test" }), - env: { ...process.env, HOME: tmpDir, SUPERMEMORY_CODEX_API_KEY: "" }, + env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir, SUPERMEMORY_CODEX_API_KEY: "" }, encoding: "utf-8", }); assert.equal(result.status, 0); @@ -319,7 +330,7 @@ describe("recall hook output envelope", () => { // ─── flush hook — Stop payload handling ────────────────────────────────────── describe("flush hook Stop payload", () => { - const flushBin = new URL("../dist/hooks/flush.js", import.meta.url).pathname; + const flushBin = fileURLToPath(new URL("../dist/hooks/flush.js", import.meta.url)); test("exits 0 with no transcript_path", () => { const result = spawnSync("node", [flushBin], { @@ -392,9 +403,9 @@ describe("flush hook Stop payload", () => { // argument parsing, the unconfigured-fallback message, and clean exit codes. describe("skill scripts: search/save/forget", () => { - const searchBin = new URL("../dist/skills/search-memory.js", import.meta.url).pathname; - const saveBin = new URL("../dist/skills/save-memory.js", import.meta.url).pathname; - const forgetBin = new URL("../dist/skills/forget-memory.js", import.meta.url).pathname; + const searchBin = fileURLToPath(new URL("../dist/skills/search-memory.js", import.meta.url)); + const saveBin = fileURLToPath(new URL("../dist/skills/save-memory.js", import.meta.url)); + const forgetBin = fileURLToPath(new URL("../dist/skills/forget-memory.js", import.meta.url)); // Run a script with a fresh empty $HOME (no config file) and an empty // SUPERMEMORY_CODEX_API_KEY so isConfigured() is false. Returns the spawn result. @@ -403,7 +414,7 @@ describe("skill scripts: search/save/forget", () => { mkdirSync(join(tmpDir, ".codex"), { recursive: true }); t.after(() => rmSync(tmpDir, { recursive: true, force: true })); return spawnSync("node", [bin, ...args], { - env: { PATH: process.env.PATH, HOME: tmpDir, SUPERMEMORY_CODEX_API_KEY: "" }, + env: { PATH: process.env.PATH, HOME: tmpDir, USERPROFILE: tmpDir, SUPERMEMORY_CODEX_API_KEY: "" }, encoding: "utf-8", }); } @@ -415,7 +426,7 @@ describe("skill scripts: search/save/forget", () => { mkdirSync(join(tmpDir, ".codex"), { recursive: true }); t.after(() => rmSync(tmpDir, { recursive: true, force: true })); return spawnSync("node", [bin], { - env: { PATH: process.env.PATH, HOME: tmpDir, SUPERMEMORY_CODEX_API_KEY: "sm_test" }, + env: { PATH: process.env.PATH, HOME: tmpDir, USERPROFILE: tmpDir, SUPERMEMORY_CODEX_API_KEY: "sm_test" }, encoding: "utf-8", }); } From 031f8449ed34a61be3e759ec905b8b52ccf456fd Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Thu, 4 Jun 2026 18:18:53 +0530 Subject: [PATCH 2/3] Add Codex status logout and web auth flow --- README.md | 7 +- build.mjs | 8 +- src/cli.ts | 21 +++- src/config.ts | 11 +- src/hooks/recall.ts | 6 + src/services/auth.ts | 49 ++++++-- src/services/client.ts | 4 +- src/skills/login.ts | 14 ++- src/skills/logout.ts | 66 +++++++++++ src/skills/status.ts | 152 +++++++++++++++++++++++++ src/skills/supermemory-login/SKILL.md | 8 ++ src/skills/supermemory-logout/SKILL.md | 23 ++++ src/skills/supermemory-status/SKILL.md | 23 ++++ test/e2e-battle-test.mjs | 7 +- test/unit.mjs | 69 +++++++++-- 15 files changed, 428 insertions(+), 40 deletions(-) create mode 100644 src/skills/logout.ts create mode 100644 src/skills/status.ts create mode 100644 src/skills/supermemory-logout/SKILL.md create mode 100644 src/skills/supermemory-status/SKILL.md diff --git a/README.md b/README.md index d35cbaf..e35e2f8 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ and the lessons learned across every project — automatically. `~/.codex/hooks.json` for you. - 🪶 **No runtime deps in hooks** — the hook scripts are pre-bundled with esbuild for fast cold starts. -- 🔧 **Fallback skills** — explicit `/supermemory-search`, `/supermemory-save`, and - `/supermemory-forget` commands available when hooks don't cover your use case. +- 🔧 **Fallback skills** — explicit `/supermemory-search`, `/supermemory-save`, + `/supermemory-forget`, `/supermemory-status`, and `/supermemory-logout` commands available when hooks + don't cover your use case. ## Quick start @@ -121,7 +122,9 @@ These Codex skills are available as explicit commands when you need more control | `/supermemory-search` | `/supermemory-search ` | Search memories manually. | | `/supermemory-save` | `/supermemory-save ` | Save a specific memory explicitly. | | `/supermemory-forget` | `/supermemory-forget ` | Remove a memory. | +| `/supermemory-status` | `/supermemory-status` | Show connection and account status. | | `/supermemory-login` | `/supermemory-login` | Re-authenticate with Supermemory. | +| `/supermemory-logout` | `/supermemory-logout` | Remove saved local credentials. | Skills are fallback commands — the hooks handle most use cases automatically. diff --git a/build.mjs b/build.mjs index 4f030ee..7533c17 100644 --- a/build.mjs +++ b/build.mjs @@ -1,5 +1,5 @@ import * as esbuild from "esbuild"; -import { mkdirSync, writeFileSync, chmodSync, copyFileSync } from "node:fs"; +import { mkdirSync, writeFileSync, chmodSync, copyFileSync, rmSync } from "node:fs"; const sharedConfig = { bundle: true, @@ -16,12 +16,14 @@ const entries = [ in: `src/hooks/${n}.ts`, out: `dist/hooks/${n}.js`, })), - ...["search-memory", "save-memory", "forget-memory", "login"].map((n) => ({ + ...["search-memory", "save-memory", "forget-memory", "status", "login", "logout"].map((n) => ({ in: `src/skills/${n}.ts`, out: `dist/skills/${n}.js`, })), ]; +rmSync("dist", { recursive: true, force: true }); + await Promise.all( entries.map((e) => esbuild.build({ @@ -34,7 +36,7 @@ await Promise.all( ); // Copy SKILL.md files to dist -for (const skillName of ["supermemory-search", "supermemory-save", "supermemory-forget", "supermemory-login"]) { +for (const skillName of ["supermemory-search", "supermemory-save", "supermemory-forget", "supermemory-status", "supermemory-login", "supermemory-logout"]) { mkdirSync(`dist/skills/${skillName}`, { recursive: true }); copyFileSync( `src/skills/${skillName}/SKILL.md`, diff --git a/src/cli.ts b/src/cli.ts index ba1949c..e106898 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -42,7 +42,16 @@ const SKILLS = [ { name: "supermemory-search", script: "search-memory.js" }, { name: "supermemory-save", script: "save-memory.js" }, { name: "supermemory-forget", script: "forget-memory.js" }, + { name: "supermemory-status", script: "status.js" }, { name: "supermemory-login", script: "login.js" }, + { name: "supermemory-logout", script: "logout.js" }, +] as const; + +const LEGACY_SUPERMEMORY_SCRIPTS = [ + "capture.js", + "profile-memory.js", + "session-start.js", + "tags.js", ] as const; const SCRIPT_DIR = getScriptDir(); @@ -244,10 +253,10 @@ function install() { copyFileSync(recallSrc, RECALL_SCRIPT); copyFileSync(flushSrc, FLUSH_SCRIPT); - // Remove old capture.js if it exists - const oldCapture = join(SUPERMEMORY_HOOKS_DIR, "capture.js"); - if (existsSync(oldCapture)) { - rmSync(oldCapture); + // Remove script names left by older package layouts. + for (const script of LEGACY_SUPERMEMORY_SCRIPTS) { + const oldScript = join(SUPERMEMORY_HOOKS_DIR, script); + if (existsSync(oldScript)) rmSync(oldScript); } // Copy skill scripts and SKILL.md files @@ -279,7 +288,7 @@ Installation complete! You now have: • Implicit memory — auto-recall on every prompt, incremental capture + final flush on session end - • Explicit memory — supermemory-search, supermemory-save, supermemory-forget, and supermemory-login skills + • Explicit memory — supermemory-search, supermemory-save, supermemory-forget, supermemory-status, supermemory-login, and supermemory-logout skills Next steps: 1. Start Codex — on your first prompt, a browser window will open to @@ -289,7 +298,7 @@ Next steps: /supermemory-login (inside Codex) export SUPERMEMORY_CODEX_API_KEY="sm_..." (in your shell profile) - 2. Get an API key at: https://console.supermemory.ai/keys (if needed) + 2. Get an API key at: https://app.supermemory.ai/?view=integrations (if needed) Optional: Enable debug logging: export SUPERMEMORY_DEBUG=true diff --git a/src/config.ts b/src/config.ts index f753d61..36d2e8d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,7 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; -import { loadCredentials } from "./services/auth.js"; +import { loadCredentialData, loadCredentials } from "./services/auth.js"; const CONFIG_FILE = join(homedir(), ".codex", "supermemory.json"); @@ -140,6 +140,15 @@ export function getApiKeyValue(): string | undefined { return SUPERMEMORY_API_KEY; } +export function getApiBaseUrl(): string { + return ( + process.env.SUPERMEMORY_API_URL || + process.env.SUPERMEMORY_BASE_URL || + loadCredentialData()?.apiBaseUrl || + "https://api.supermemory.ai" + ); +} + export function getSignalConfig(): { enabled: boolean; keywords: string[]; diff --git a/src/hooks/recall.ts b/src/hooks/recall.ts index dbec7d8..95d5498 100644 --- a/src/hooks/recall.ts +++ b/src/hooks/recall.ts @@ -11,6 +11,7 @@ import { captureEntries, resolveTranscriptPath } from "../services/capture.js"; import { getSeenFacts, addSeenFacts } from "../services/factCache.js"; const AUTH_ATTEMPTED_FILE = join(homedir(), ".codex", "supermemory", ".auth-attempted"); +const LOGGED_OUT_FILE = join(homedir(), ".codex", "supermemory", ".logged-out"); interface CodexHookPayload { session_id?: string; @@ -48,6 +49,11 @@ async function main() { } if (!isConfigured()) { + if (existsSync(LOGGED_OUT_FILE)) { + log("recall: logged out marker present, skipping browser auth"); + exitWithContext(""); + } + const alreadyAttempted = existsSync(AUTH_ATTEMPTED_FILE); if (!alreadyAttempted) { diff --git a/src/services/auth.ts b/src/services/auth.ts index a140777..a844035 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -8,10 +8,15 @@ import { openUrl } from "./openUrl.js"; const SUPERMEMORY_DIR = join(homedir(), ".codex", "supermemory"); const CREDENTIALS_FILE = join(SUPERMEMORY_DIR, "credentials.json"); +export interface Credentials { + apiKey?: string; + apiBaseUrl?: string; + savedAt?: string; +} 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; + process.env.SUPERMEMORY_AUTH_URL || "https://app.supermemory.ai/auth/agent-connect"; +const AUTH_TIMEOUT = Number(process.env.SUPERMEMORY_AUTH_TIMEOUT) || 5 * 60_000; const AUTH_SUCCESS_HTML = ` Connected - Supermemory