feat: add OMP runtime support

This commit is contained in:
Jeff Scott Ward
2026-06-26 12:49:58 -04:00
parent 4605a4f1d8
commit 84a485d62e
26 changed files with 639 additions and 83 deletions
+26 -1
View File
@@ -1,7 +1,16 @@
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { AuthService, type AuthChange } from "./authService.js";
const tempDirs: string[] = [];
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});
describe("AuthService", () => {
it("saves API keys and emits a global auth change", () => {
const { auth, authStorage, changes } = createAuthService();
@@ -30,6 +39,16 @@ describe("AuthService", () => {
expect(changes).toEqual([]);
auth.dispose();
});
it("stores credentials in the configured agent directory", async () => {
const agentDir = await tempAgentDir();
const auth = new AuthService({ agentDir });
auth.saveApiKey("anthropic", "sk-omp");
await expect(readFile(join(agentDir, "auth.json"), "utf8")).resolves.toContain("sk-omp");
auth.dispose();
});
});
function createAuthService(data: Parameters<typeof AuthStorage.inMemory>[0] = {}) {
@@ -40,3 +59,9 @@ function createAuthService(data: Parameters<typeof AuthStorage.inMemory>[0] = {}
auth.subscribe((change) => { changes.push(change); });
return { auth, authStorage, changes };
}
async function tempAgentDir(): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), "pi-web-auth-agent-"));
tempDirs.push(dir);
return dir;
}
+8 -1
View File
@@ -1,3 +1,4 @@
import { join } from "node:path";
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
import type { AuthProvidersResponse, AuthType, OAuthFlowState } from "../../shared/apiTypes.js";
import { getLoginProviderOptions, getLogoutProviderOptions } from "./authProviderOptions.js";
@@ -11,17 +12,23 @@ type AuthChangeListener = (change: AuthChange) => void;
type ModelRegistryInstance = ReturnType<typeof ModelRegistry.create>;
export interface AuthServiceDependencies {
agentDir?: string;
modelRegistry?: ModelRegistryInstance;
authFlows?: OAuthLoginFlowService;
}
export function createModelRegistryForAgentDir(agentDir: string): ModelRegistryInstance {
const authStorage = AuthStorage.create(join(agentDir, "auth.json"));
return ModelRegistry.create(authStorage, join(agentDir, "models.json"));
}
export class AuthService {
readonly modelRegistry: ModelRegistryInstance;
private readonly authFlows: OAuthLoginFlowService;
private readonly listeners = new Set<AuthChangeListener>();
constructor(deps: AuthServiceDependencies = {}) {
this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
this.modelRegistry = deps.modelRegistry ?? (deps.agentDir === undefined ? ModelRegistry.create(AuthStorage.create()) : createModelRegistryForAgentDir(deps.agentDir));
this.authFlows = deps.authFlows ?? new OAuthLoginFlowService();
}
@@ -59,6 +59,16 @@ describe("SessionDirResolver", () => {
expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: envDir, usesConfiguredSessionDir: true });
});
it("uses OMP sessionDir environment overrides before settings", async () => {
const envDir = join(tempDir, "omp-env-sessions");
await mkdir(agentDir, { recursive: true });
await writeFile(join(agentDir, "settings.json"), `${JSON.stringify({ sessionDir: join(tempDir, "settings-sessions") }, null, 2)}\n`, "utf8");
const resolver = new SessionDirResolver({ agentDir, env: { OMP_CODING_AGENT_SESSION_DIR: envDir }, sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"] });
expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: envDir, usesConfiguredSessionDir: true });
});
});
describe("Pi session manager gateway", () => {
@@ -82,6 +92,21 @@ describe("Pi session manager gateway", () => {
await expect(gateway.listAll()).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: "default-session", cwd }), expect.objectContaining({ id: "env-session", cwd })]));
});
it("includes command-specific env session directories in global listing", async () => {
for (const envKey of ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR"]) {
const envSessionDir = join(tempDir, `${envKey.toLowerCase()}-sessions`);
await writeSessionFile(envSessionDir, `${envKey.toLowerCase()}-session`, cwd);
const gateway = createPiSessionManagerGateway({
agentDir,
env: { [envKey]: envSessionDir },
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"],
});
if (gateway.listAll === undefined) throw new Error("Expected legacy listing support");
await expect(gateway.listAll()).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: `${envKey.toLowerCase()}-session`, cwd })]));
}
});
it("lists only sessions for the requested cwd when a custom Pi sessionDir is shared", async () => {
const sharedSessionDir = join(tempDir, "shared-sessions");
const otherCwd = join(tempDir, "other-workspace");
+11 -4
View File
@@ -19,15 +19,18 @@ export interface SessionDirResolution {
export interface SessionDirResolverOptions {
agentDir?: string;
env?: NodeJS.ProcessEnv;
sessionDirEnvKeys?: readonly string[];
}
export class SessionDirResolver {
private readonly agentDir: string;
private readonly env: NodeJS.ProcessEnv;
private readonly sessionDirEnvKeys: readonly string[];
constructor(options: SessionDirResolverOptions = {}) {
this.agentDir = options.agentDir ?? getAgentDir();
this.env = options.env ?? process.env;
this.sessionDirEnvKeys = options.sessionDirEnvKeys ?? [PI_SESSION_DIR_ENV];
}
defaultSessionsRoot(): string {
@@ -35,15 +38,15 @@ export class SessionDirResolver {
}
globalEnvSessionDir(): string | undefined {
const envSessionDir = this.env[PI_SESSION_DIR_ENV];
if (envSessionDir === undefined || envSessionDir === "") return undefined;
const envSessionDir = this.envSessionDir();
if (envSessionDir === undefined) return undefined;
const expanded = expandTildePath(envSessionDir);
return isAbsolute(expanded) ? expanded : undefined;
}
resolve(cwd: string): SessionDirResolution {
const envSessionDir = this.env[PI_SESSION_DIR_ENV];
if (envSessionDir !== undefined && envSessionDir !== "") {
const envSessionDir = this.envSessionDir();
if (envSessionDir !== undefined) {
return { source: "env", sessionDir: resolveConfiguredPath(envSessionDir, cwd), usesConfiguredSessionDir: true };
}
@@ -54,6 +57,10 @@ export class SessionDirResolver {
return { source: "pi-default", sessionDir: defaultPiSessionDir(cwd, this.agentDir), usesConfiguredSessionDir: false };
}
private envSessionDir(): string | undefined {
return this.sessionDirEnvKeys.map((key) => this.env[key]).find((value) => value !== undefined && value !== "");
}
}
export type PiSessionManagerGatewayOptions = SessionDirResolverOptions;
+2 -2
View File
@@ -21,7 +21,7 @@ import { SessionCommandService } from "./sessionCommandService.js";
import { SessionArchiveStore, type ArchivedSessionRecord, type ArchiveSessionInput } from "./sessionArchiveStore.js";
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree, type SessionArchiveTreeCandidate } from "./sessionArchiveTree.js";
import type { ActiveSession } from "./sessionRuntimeStore.js";
import type { AuthChange } from "./authService.js";
import { createModelRegistryForAgentDir, type AuthChange } from "./authService.js";
import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
import { computeEditPreview, type EditPreviewResult } from "./editPreview.js";
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
@@ -340,7 +340,7 @@ export class PiSessionService implements SessionRouteService {
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
this.agentDir = deps.agentDir ?? getAgentDir();
this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir });
this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
this.modelRegistry = deps.modelRegistry ?? createModelRegistryForAgentDir(this.agentDir);
this.spawnTargets = deps.spawnTargets;
this.logger = deps.logger ?? noopLogger;
this.now = deps.now ?? (() => new Date());