refactor(pi-devin): switch from npm+patch to vendored local fork

- 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.
This commit is contained in:
Shujakuinkuraudo
2026-09-15 12:08:30 -04:00
committed by shujakuin
parent 851c361a84
commit 654221119d
22 changed files with 690 additions and 1187 deletions
+72 -58
View File
@@ -1,14 +1,10 @@
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 {
type CachedDevinCatalog,
isUsableCatalog,
isCatalogCacheFresh,
readCatalogCache,
writeCatalogCache,
} from "../src/catalog-cache.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";
@@ -16,39 +12,37 @@ import { streamDevin } from "../src/stream.js";
const PROVIDER_ID = "devin";
const PLACEHOLDER_BASE_URL = "https://server.codeium.com";
let _pi: ExtensionAPI | null = null;
let catalogRequest: Promise<ProviderModelConfig[]> | null = null;
const CACHE_PATH = join(homedir(), ".pi", "agent", "devin-catalog-cache.json");
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
function isOffline(): boolean {
const value = process.env.PI_OFFLINE?.toLowerCase();
return value === "1" || value === "true" || value === "yes";
interface CachedCatalog {
catalog: unknown;
timestamp: number;
}
function refreshCatalog(pi: ExtensionAPI): Promise<ProviderModelConfig[]> {
if (!catalogRequest) {
const pending = loadCliCatalog().then((catalog) => {
if (!isUsableCatalog(catalog)) {
throw new Error("Devin CLI returned no usable model families");
}
const models = modelsFromCatalog(catalog);
try {
writeCatalogCache(catalog);
} catch (error) {
console.warn(`Devin: failed to cache model catalog: ${error instanceof Error ? error.message : String(error)}`);
}
return models;
});
catalogRequest = pending;
void pending.finally(() => {
if (catalogRequest === pending) catalogRequest = null;
}).catch(() => {});
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;
}
return catalogRequest.then((models) => {
if (_pi === pi) registerDevinProvider(pi, models);
return models;
});
}
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",
@@ -61,7 +55,9 @@ function registerDevinProvider(pi: ExtensionAPI, models: ProviderModelConfig[]):
const creds = await loginWithCli();
if (_pi) {
try {
await refreshCatalog(_pi);
const catalog = await loadCliCatalog();
saveCachedCatalog(catalog);
registerDevinProvider(_pi, modelsFromCatalog(catalog));
} catch {
// keep current models
}
@@ -94,33 +90,48 @@ function registerDevinProvider(pi: ExtensionAPI, models: ProviderModelConfig[]):
export default async function (pi: ExtensionAPI): Promise<void> {
_pi = pi;
let cached: CachedDevinCatalog | null | undefined;
let initialModels = FALLBACK_MODELS;
try {
cached = readCatalogCache();
if (cached) initialModels = modelsFromCatalog(cached.catalog);
} catch (error) {
cached = null;
console.warn(`Devin: failed to read model catalog cache: ${error instanceof Error ? error.message : String(error)}`);
}
// 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);
if (!isOffline()) {
try {
if (readCredentials()) {
if (!cached) {
await refreshCatalog(pi);
} else if (!isCatalogCacheFresh(cached)) {
void refreshCatalog(pi).catch((error) => {
console.warn(`Devin: background model catalog refresh failed: ${error instanceof Error ? error.message : String(error)}`);
});
try {
if (readCredentials()) {
// Refresh in background without blocking startup
loadCliCatalog().then((catalog) => {
if (catalog) {
saveCachedCatalog(catalog);
if (_pi) registerDevinProvider(_pi, modelsFromCatalog(catalog));
}
}
} catch {
// Cached or fallback models stay registered when credential discovery or a cold refresh fails.
}).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) => {
@@ -143,7 +154,10 @@ export default async function (pi: ExtensionAPI): Promise<void> {
description: "Refresh Devin Local model catalog from `devin models list`",
handler: async (_args, ctx) => {
try {
const models = await refreshCatalog(pi);
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(
@@ -155,6 +169,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
});
pi.on("session_shutdown", async () => {
if (_pi === pi) _pi = null;
_pi = null;
});
}