import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent"; import type { Api, Model, OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai"; import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; import { authStatus, loginWithCli, readCredentials } from "../src/credentials.js"; import { whichDevin, devinVersion } from "../src/cli.js"; import { FALLBACK_MODELS, loadCliCatalog, modelsFromCatalog } from "../src/models.js"; import { CLIENT_IDE, CLIENT_VERSION } from "../src/metadata.js"; import { streamDevin } from "../src/stream.js"; const PROVIDER_ID = "devin"; const PLACEHOLDER_BASE_URL = "https://server.codeium.com"; const CACHE_PATH = join(homedir(), ".pi", "agent", "devin-catalog-cache.json"); const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours interface CachedCatalog { catalog: unknown; timestamp: number; } function loadCachedCatalog(): unknown | null { try { if (!existsSync(CACHE_PATH)) return null; const raw = readFileSync(CACHE_PATH, "utf-8"); const cached = JSON.parse(raw) as CachedCatalog; if (Date.now() - cached.timestamp > CACHE_TTL_MS) return null; return cached.catalog; } catch { return null; } } function saveCachedCatalog(catalog: unknown): void { try { mkdirSync(join(homedir(), ".pi", "agent"), { recursive: true }); writeFileSync(CACHE_PATH, JSON.stringify({ catalog, timestamp: Date.now() })); } catch { // ignore cache write failures } } let _pi: ExtensionAPI | null = null; function registerDevinProvider(pi: ExtensionAPI, models: ProviderModelConfig[]): void { pi.registerProvider(PROVIDER_ID, { name: "Devin Local", api: "devin-local", baseUrl: PLACEHOLDER_BASE_URL, models, oauth: { name: "Devin CLI", async login(_callbacks: OAuthLoginCallbacks): Promise { const creds = await loginWithCli(); if (_pi) { try { const catalog = await loadCliCatalog(); saveCachedCatalog(catalog); registerDevinProvider(_pi, modelsFromCatalog(catalog)); } catch { // keep current models } } return { refresh: "", access: creds.apiKey, expires: Date.now() + 365 * 24 * 60 * 60 * 1000, }; }, async refreshToken(credentials: OAuthCredentials): Promise { const creds = readCredentials(); if (!creds) return credentials; return { refresh: "", access: creds.apiKey, expires: Date.now() + 365 * 24 * 60 * 60 * 1000, }; }, getApiKey(credentials: OAuthCredentials): string { return readCredentials()?.apiKey || credentials.access; }, modifyModels(models: Model[], _credentials: OAuthCredentials): Model[] { return models; }, }, streamSimple: streamDevin, }); } export default async function (pi: ExtensionAPI): Promise { _pi = pi; // Synchronously load cached catalog so subagent processes have the full // model list immediately (they may not live long enough for the async // refresh to complete). const cached = loadCachedCatalog(); const initialModels = cached ? modelsFromCatalog(cached as Parameters[0]) : FALLBACK_MODELS; registerDevinProvider(pi, initialModels); try { if (readCredentials()) { // Refresh in background without blocking startup loadCliCatalog().then((catalog) => { if (catalog) { saveCachedCatalog(catalog); if (_pi) registerDevinProvider(_pi, modelsFromCatalog(catalog)); } }).catch(() => {}); } } catch { // fallback models already registered } pi.on("session_start", async () => { try { if (!_pi || !readCredentials()) return; const cached = loadCachedCatalog(); if (cached) { registerDevinProvider(_pi, modelsFromCatalog(cached as Parameters[0])); } loadCliCatalog().then((catalog) => { if (catalog) { saveCachedCatalog(catalog); if (_pi) registerDevinProvider(_pi, modelsFromCatalog(catalog)); } }).catch(() => {}); } catch { // keep current models } }); pi.registerCommand("devin-status", { description: "Show Devin CLI auth + binary status", handler: async (_args, ctx) => { const bin = await whichDevin(); const version = await devinVersion(); const status = await authStatus(); ctx.ui.notify( [ bin ? `CLI: ${bin}` : "CLI: not found", version ? `CLI version: ${version}` : "CLI version: unknown", `Client identity: ${CLIENT_IDE} ${CLIENT_VERSION}`, status.loggedIn ? "Auth: signed in via Devin CLI" : "Auth: not signed in. Run /login devin or `devin auth login`", ].join("\n"), status.loggedIn && bin ? "info" : "warning", ); }, }); pi.registerCommand("devin-refresh", { description: "Refresh Devin Local model catalog from `devin models list`", handler: async (_args, ctx) => { try { const catalog = await loadCliCatalog(); saveCachedCatalog(catalog); const models = modelsFromCatalog(catalog); registerDevinProvider(pi, models); ctx.ui.notify(`Devin: loaded ${models.length} families from the local CLI.`, "info"); } catch (error) { ctx.ui.notify( `Devin refresh failed: ${error instanceof Error ? error.message : String(error)}`, "error", ); } }, }); pi.on("session_shutdown", async () => { _pi = null; }); }