- Vendor the patched pi-devin@0.1.2 tree at apps/pi-devin/upstream/ (third-party archive -> upstream_fork); git history replaces the patch layer as the change archive. Includes the devin-desktop identity fix and normalized catalog variant mapping from the previous patch. - build-pi-package.sh now verifies source.sha256 against host-local-packages.json and npm-packs the vendored tree; drop the npm fetch + patch stage. - install-pi-package.sh strips the npm-pack package/ prefix to match the playbook's unarchive behavior. - Deployed to a100 via deploy_pi_devin_package (--limit a100, R2 then R3); remote verified to carry the fork source.
175 lines
5.7 KiB
TypeScript
175 lines
5.7 KiB
TypeScript
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<OAuthCredentials> {
|
|
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<OAuthCredentials> {
|
|
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<Api>[], _credentials: OAuthCredentials): Model<Api>[] {
|
|
return models;
|
|
},
|
|
},
|
|
streamSimple: streamDevin,
|
|
});
|
|
}
|
|
|
|
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
_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<typeof modelsFromCatalog>[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<typeof modelsFromCatalog>[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;
|
|
});
|
|
}
|