From 5a4915543ad18620a1f283d3ec8ee551bc138152 Mon Sep 17 00:00:00 2001 From: Omkar Chebale Date: Sun, 27 Sep 2026 18:09:29 +0530 Subject: [PATCH 1/3] Keep login waiting when the browser cannot be opened If xdg-open/open fails (e.g. WSL without a browser handler), the auth flow aborted even though it had just printed the URL to visit manually. Log the failure and keep the callback server listening until the existing timeout instead. Fixes #72 --- src/services/auth.test.ts | 21 +++++++++++++++++++++ src/services/auth.ts | 9 +++------ 2 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 src/services/auth.test.ts diff --git a/src/services/auth.test.ts b/src/services/auth.test.ts new file mode 100644 index 0000000..99e0b4d --- /dev/null +++ b/src/services/auth.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test"; + +import { startAuthFlow } from "./auth.js"; + +describe("startAuthFlow", () => { + test("keeps waiting for the callback when the browser cannot be opened", async () => { + let authUrl = ""; + const flow = startAuthFlow(5_000, async (url) => { + authUrl = url.toString(); + throw new Error("spawn xdg-open ENOENT"); + }); + + await new Promise((resolve) => setTimeout(resolve, 50)); + const callback = new URL(new URL(authUrl).searchParams.get("callback")!); + callback.searchParams.set("state", "wrong"); + const response = await fetch(callback); + + expect(response.status).toBe(403); + expect(await flow).toEqual({ success: false, error: "Invalid auth state" }); + }); +}); diff --git a/src/services/auth.ts b/src/services/auth.ts index bacfac8..636bf30 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -65,7 +65,7 @@ export interface AuthResult { error?: string; } -export function startAuthFlow(timeoutMs = AUTH_TIMEOUT): Promise { +export function startAuthFlow(timeoutMs = AUTH_TIMEOUT, open = openUrl): Promise { return new Promise((resolve) => { let resolved = false; const stateToken = randomBytes(16).toString("hex"); @@ -168,12 +168,9 @@ export function startAuthFlow(timeoutMs = AUTH_TIMEOUT): Promise { console.log("Opening browser for authentication..."); console.log(`If it doesn't open, visit: ${authUrl}`); - openUrl(authUrl).catch((error) => { + open(authUrl).catch((error) => { if (!resolved) { - resolved = true; - clearTimeout(timer); - server.close(); - resolve({ success: false, error: `Failed to open browser: ${error.message}` }); + console.log(`Could not open a browser (${error.message}). Open the URL above to continue.`); } }); }); From 787fa0ffde859fc34d7a51baf91dd23b3efc6a39 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Thu, 5 Nov 2026 08:09:43 +0000 Subject: [PATCH 2/3] Open the login URL in the Windows browser from WSL Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- src/services/openUrl.test.ts | 81 ++++++++++++++++++++++++++++++++++++ src/services/openUrl.ts | 33 +++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/services/openUrl.test.ts diff --git a/src/services/openUrl.test.ts b/src/services/openUrl.test.ts new file mode 100644 index 0000000..f22b5d5 --- /dev/null +++ b/src/services/openUrl.test.ts @@ -0,0 +1,81 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { chmodSync, existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { openUrl } from "./openUrl.js"; + +const URL_WITH_SPECIALS = "https://example.com/a?x=1&y=a%20b&q='it'&z=%26"; +const saved = { + PATH: process.env.PATH, + WSL_DISTRO_NAME: process.env.WSL_DISTRO_NAME, + WSL_INTEROP: process.env.WSL_INTEROP, +}; +let bin: string; + +function stub(...names: string[]): void { + for (const name of names) { + const file = join(bin, name); + writeFileSync(file, `#!/bin/sh\nprintf '%s\\n' "$@" > "${file}.args"\n`); + chmodSync(file, 0o755); + } +} + +function opened(name: string): string[] | undefined { + const file = join(bin, `${name}.args`); + return existsSync(file) ? readFileSync(file, "utf-8").trimEnd().split("\n") : undefined; +} + +describe.skipIf(process.platform !== "linux")("openUrl on Linux", () => { + beforeEach(() => { + bin = mkdtempSync(join(tmpdir(), "open-url-")); + process.env.PATH = bin; + delete process.env.WSL_DISTRO_NAME; + delete process.env.WSL_INTEROP; + }); + + afterEach(() => { + for (const [key, value] of Object.entries(saved)) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + }); + + test("prefers wslview on WSL", async () => { + process.env.WSL_DISTRO_NAME = "Ubuntu"; + stub("wslview", "powershell.exe", "xdg-open"); + await openUrl(URL_WITH_SPECIALS); + expect(opened("wslview")).toEqual([URL_WITH_SPECIALS]); + expect(opened("powershell.exe")).toBeUndefined(); + expect(opened("xdg-open")).toBeUndefined(); + }); + + test("passes the URL intact to PowerShell on WSL without wslview", async () => { + process.env.WSL_INTEROP = "/run/WSL/1_interop"; + stub("powershell.exe", "xdg-open"); + await openUrl(URL_WITH_SPECIALS); + const encoded = opened("powershell.exe")!.at(-1)!; + expect(encoded).toMatch(/^[A-Za-z0-9+/=]+$/); + expect(Buffer.from(encoded, "base64").toString("utf16le")).toBe( + `Start-Process '${URL_WITH_SPECIALS.replaceAll("'", "''")}'`, + ); + expect(opened("xdg-open")).toBeUndefined(); + }); + + test("falls back to xdg-open on WSL without Windows openers", async () => { + process.env.WSL_DISTRO_NAME = "Ubuntu"; + stub("xdg-open"); + await openUrl(URL_WITH_SPECIALS); + expect(opened("xdg-open")).toEqual([URL_WITH_SPECIALS]); + }); + + test.skipIf(/microsoft/i.test(readFileSync("/proc/version", "utf-8")))( + "ignores WSL openers outside WSL", + async () => { + stub("wslview", "xdg-open"); + await openUrl(URL_WITH_SPECIALS); + expect(opened("xdg-open")).toEqual([URL_WITH_SPECIALS]); + expect(opened("wslview")).toBeUndefined(); + }, + ); +}); diff --git a/src/services/openUrl.ts b/src/services/openUrl.ts index dae8053..3c0e9c0 100644 --- a/src/services/openUrl.ts +++ b/src/services/openUrl.ts @@ -1,4 +1,5 @@ import { execFile } from "node:child_process"; +import { readFileSync } from "node:fs"; function run(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { @@ -9,6 +10,26 @@ function run(command: string, args: string[]): Promise { }); } +function isWsl(): boolean { + if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true; + try { + return /microsoft/i.test(readFileSync("/proc/version", "utf-8")); + } catch { + return false; + } +} + +// Base64 keeps & and quotes in the URL away from WSL interop and Windows command-line parsing. +function openWithPowerShell(href: string): Promise { + const script = `Start-Process '${href.replaceAll("'", "''")}'`; + return run("powershell.exe", [ + "-NoProfile", + "-NonInteractive", + "-EncodedCommand", + Buffer.from(script, "utf16le").toString("base64"), + ]); +} + export async function openUrl(url: string | URL): Promise { const href = url.toString(); if (!/^https?:\/\//i.test(href)) { @@ -30,5 +51,17 @@ export async function openUrl(url: string | URL): Promise { return; } + if (isWsl()) { + try { + await run("wslview", [href]); + return; + } catch {} + + try { + await openWithPowerShell(href); + return; + } catch {} + } + await run("xdg-open", [href]); } From d310eaaf060e294f2aa25e548028dcd928ec393a Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:21:17 +0000 Subject: [PATCH 3/3] chore(release): 2.1.2 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 5 +++++ package.json | 2 +- src/version.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e737a3..ff5087e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2.1.2 + +- Keep `login` waiting for the callback when no browser can be opened, and print the URL to open instead of failing (#72). +- On WSL, open the login URL in the Windows browser through `wslview` or PowerShell before falling back to `xdg-open`. + ## 2.0.16 - Use the official pinned Supermemory 5.0.1 SDK for hosted content, preserving namespace strings, batch IDs, metadata, search defaults, capture cadence, and permissions. diff --git a/package.json b/package.json index a52228f..7daa459 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-supermemory", - "version": "2.1.1", + "version": "2.1.2", "description": "OpenCode plugin that gives coding agents persistent memory using Supermemory", "type": "module", "main": "dist/index.js", diff --git a/src/version.ts b/src/version.ts index 3b850aa..16d28d6 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ // AUTO-GENERATED by scripts/sync-version.mjs — do not edit by hand. // Sourced from package.json "version" so the two cannot drift. -export const PLUGIN_VERSION = "2.1.1"; +export const PLUGIN_VERSION = "2.1.2";