feat(pi): port the managed stack to Pi 0.87.1 and Pi Web 0.9.3
This commit is contained in:
+61
-75
@@ -1,10 +1,14 @@
|
||||
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";
|
||||
@@ -12,36 +16,38 @@ 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;
|
||||
let catalogRequest: Promise<ProviderModelConfig[]> | null = null;
|
||||
|
||||
function isOffline(): boolean {
|
||||
const value = process.env.PI_OFFLINE?.toLowerCase();
|
||||
return value === "1" || value === "true" || value === "yes";
|
||||
}
|
||||
|
||||
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(() => {});
|
||||
}
|
||||
return catalogRequest.then((models) => {
|
||||
if (_pi === pi) registerDevinProvider(pi, models);
|
||||
return models;
|
||||
});
|
||||
}
|
||||
|
||||
function registerDevinProvider(pi: ExtensionAPI, models: ProviderModelConfig[]): void {
|
||||
pi.registerProvider(PROVIDER_ID, {
|
||||
@@ -55,9 +61,7 @@ function registerDevinProvider(pi: ExtensionAPI, models: ProviderModelConfig[]):
|
||||
const creds = await loginWithCli();
|
||||
if (_pi) {
|
||||
try {
|
||||
const catalog = await loadCliCatalog();
|
||||
saveCachedCatalog(catalog);
|
||||
registerDevinProvider(_pi, modelsFromCatalog(catalog));
|
||||
await refreshCatalog(_pi);
|
||||
} catch {
|
||||
// keep current models
|
||||
}
|
||||
@@ -90,47 +94,32 @@ function registerDevinProvider(pi: ExtensionAPI, models: ProviderModelConfig[]):
|
||||
|
||||
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;
|
||||
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)}`);
|
||||
}
|
||||
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 () => {
|
||||
if (!isOffline()) {
|
||||
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));
|
||||
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)}`);
|
||||
});
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
} catch {
|
||||
// keep current models
|
||||
// Cached or fallback models stay registered when credential discovery or a cold refresh fails.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pi.registerCommand("devin-status", {
|
||||
description: "Show Devin CLI auth + binary status",
|
||||
@@ -154,10 +143,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
||||
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);
|
||||
const models = await refreshCatalog(pi);
|
||||
ctx.ui.notify(`Devin: loaded ${models.length} families from the local CLI.`, "info");
|
||||
} catch (error) {
|
||||
ctx.ui.notify(
|
||||
@@ -169,6 +155,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", async () => {
|
||||
_pi = null;
|
||||
if (_pi === pi) _pi = null;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user