From e9fc403b942b1399f520191da1f38b04dcf483bd Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 10 Mar 2026 01:12:54 +0000 Subject: [PATCH] fix(adapters/gemini-local): inject skills into ~/.gemini/ instead of tmpdir GEMINI_CLI_HOME pointed to a tmpdir which broke OAuth auth since the CLI couldn't find credentials in the real home directory. Instead, inject Paperclip skills directly into ~/.gemini/skills/ (matching the pattern used by cursor, codex, pi, and opencode adapters). This lets the Gemini CLI find both auth credentials and skills in their natural location without any GEMINI_CLI_HOME override. Co-Authored-By: Claude Opus 4.6 --- .../gemini-local/src/server/execute.ts | 100 +++++++++++------- 1 file changed, 63 insertions(+), 37 deletions(-) diff --git a/packages/adapters/gemini-local/src/server/execute.ts b/packages/adapters/gemini-local/src/server/execute.ts index f9495a4b..23377fc9 100644 --- a/packages/adapters/gemini-local/src/server/execute.ts +++ b/packages/adapters/gemini-local/src/server/execute.ts @@ -67,27 +67,60 @@ async function resolvePaperclipSkillsDir(): Promise { return null; } +function geminiSkillsHome(): string { + return path.join(os.homedir(), ".gemini", "skills"); +} + /** - * Create a tmpdir with `.gemini/skills/` containing symlinks to skills from - * the repo's `skills/` directory, so `GEMINI_CLI_HOME` makes Gemini CLI discover - * them as proper registered skills. + * Inject Paperclip skills directly into `~/.gemini/skills/` via symlinks. + * This avoids needing GEMINI_CLI_HOME overrides, so the CLI naturally finds + * both its auth credentials and the injected skills in the real home directory. */ -async function buildSkillsDir(): Promise { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-gemini-skills-")); - const target = path.join(tmp, ".gemini", "skills"); - await fs.mkdir(target, { recursive: true }); +async function ensureGeminiSkillsInjected( + onLog: AdapterExecutionContext["onLog"], +): Promise { const skillsDir = await resolvePaperclipSkillsDir(); - if (!skillsDir) return tmp; - const entries = await fs.readdir(skillsDir, { withFileTypes: true }); + if (!skillsDir) return; + + const skillsHome = geminiSkillsHome(); + try { + await fs.mkdir(skillsHome, { recursive: true }); + } catch (err) { + await onLog( + "stderr", + `[paperclip] Failed to prepare Gemini skills directory ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`, + ); + return; + } + + let entries: Dirent[]; + try { + entries = await fs.readdir(skillsDir, { withFileTypes: true }); + } catch (err) { + await onLog( + "stderr", + `[paperclip] Failed to read Paperclip skills from ${skillsDir}: ${err instanceof Error ? err.message : String(err)}\n`, + ); + return; + } + for (const entry of entries) { - if (entry.isDirectory()) { - await fs.symlink( - path.join(skillsDir, entry.name), - path.join(target, entry.name), + if (!entry.isDirectory()) continue; + const source = path.join(skillsDir, entry.name); + const target = path.join(skillsHome, entry.name); + const existing = await fs.lstat(target).catch(() => null); + if (existing) continue; + + try { + await fs.symlink(source, target); + await onLog("stderr", `[paperclip] Linked Gemini skill: ${entry.name}\n`); + } catch (err) { + await onLog( + "stderr", + `[paperclip] Failed to link Gemini skill "${entry.name}": ${err instanceof Error ? err.message : String(err)}\n`, ); } } - return tmp; } export async function execute(ctx: AdapterExecutionContext): Promise { @@ -118,14 +151,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0; const env: Record = { ...buildPaperclipEnv(agent) }; env.PAPERCLIP_RUN_ID = runId; - if (tmpHome) env.GEMINI_CLI_HOME = tmpHome; const wakeTaskId = (typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) || (typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) || @@ -370,26 +402,20 @@ export async function execute(ctx: AdapterExecutionContext): Promise { }); - } + const initial = await runAttempt(sessionId); + if ( + sessionId && + !initial.proc.timedOut && + (initial.proc.exitCode ?? 0) !== 0 && + isGeminiUnknownSessionError(initial.proc.stdout, initial.proc.stderr) + ) { + await onLog( + "stderr", + `[paperclip] Gemini resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`, + ); + const retry = await runAttempt(null); + return toResult(retry, true, true); } + + return toResult(initial); }