## Summary - Add `fusion-plugin-omp-runtime` so Fusion agents can run through operator-installed **Oh My Pi (`omp`)** over the [Agent Client Protocol](https://omp.sh/docs/acp) (`omp acp`). - Wire staged/bundled install, Settings → Authentication card (enable + binary path), model discovery (`omp models` → `omp-cli/*`), and MCP eligibility for runtime id `omp`. - Forward Fusion `systemPrompt` via ACP `session/new` `_meta.systemPromptOverride`. ## How operators use it 1. Install/auth `omp` (credentials under `~/.omp`). 2. Enable **Oh My Pi — via omp ACP** in Settings → Authentication (optional binary path). 3. Set agent **Runtime Source → OMP Runtime** (`runtimeHint: "omp"`), or pick an `omp-cli/*` model when enabled. ## Known v1 gaps - No Grok-style Fusion `fn_*` loopback tool bridge yet (operator MCP is forwarded; in-process custom tools are not). - Model is fixed at spawn (`omp --model … acp`); no mid-session Fusion model switch. ## Test plan - [x] `pnpm --filter @fusion-plugin-examples/omp-runtime test` (unit + live ACP when `omp` is on PATH) - [x] Auth routes: `POST /api/auth/omp-cli`, `GET /api/providers/omp-cli/status` - [x] Engine `runtimeSupportsMcp("omp")` - [ ] Manual: enable card in dashboard, select OMP runtime on an agent, run a short chat turn <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Oh My Pi (OMP) CLI support as an ACP-backed runtime and model provider, including model discovery and probing. * Added dashboard auth/status controls to enable OMP, check readiness, and configure the local binary path (with validation). * Exposed OMP custom `fn_*` tools via an MCP loopback bridge, plus optional filesystem capabilities and stricter tool permission gating. * **Documentation** * Added/expanded OMP runtime contract and integration docs (including the ACP session/handshake flow). * **Tests** * Added Vitest coverage for settings wiring, provider status, model discovery, runtime sessions, permissions, MCP bridging, and live connectivity. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
100 lines
2.6 KiB
TypeScript
100 lines
2.6 KiB
TypeScript
/**
|
|
* OMP CLI discovery → model-picker mapping, behind a short-TTL single-flight cache.
|
|
*
|
|
* FNXC:OmpAcp 2026-07-13-22:50:
|
|
* Mirrors grok-model-cache / cursor-model-cache. When useOmpCli is true, surface
|
|
* models from `omp models` under provider id `omp-cli`. Never throws; empty on failure.
|
|
*/
|
|
|
|
import { discoverOmpCliModels } from "./runtime-provider-probes.js";
|
|
|
|
export interface OmpPickerModel {
|
|
provider: "omp-cli";
|
|
id: string;
|
|
name: string;
|
|
reasoning: boolean;
|
|
contextWindow: number;
|
|
}
|
|
|
|
export const OMP_PICKER_PROVIDER_ID = "omp-cli" as const;
|
|
|
|
const DEFAULT_TTL_MS = 60_000;
|
|
const EMPTY_RESULT_TTL_MS = 5_000;
|
|
|
|
export function ompDiscoveryToModels(
|
|
models: ReadonlyArray<{ id: string; label?: string }>,
|
|
): OmpPickerModel[] {
|
|
const seen = new Set<string>();
|
|
const result: OmpPickerModel[] = [];
|
|
for (const model of models) {
|
|
const id = model.id?.trim();
|
|
if (!id || seen.has(id)) continue;
|
|
seen.add(id);
|
|
result.push({
|
|
provider: OMP_PICKER_PROVIDER_ID,
|
|
id,
|
|
name: model.label?.trim() || id,
|
|
reasoning: false,
|
|
contextWindow: 0,
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
interface CacheEntry {
|
|
fetchedAt: number;
|
|
models: OmpPickerModel[];
|
|
ttlMs: number;
|
|
}
|
|
|
|
const cache = new Map<string, CacheEntry>();
|
|
const inFlight = new Map<string, Promise<OmpPickerModel[]>>();
|
|
|
|
export function __resetOmpPickerModelsCacheForTests(): void {
|
|
cache.clear();
|
|
inFlight.clear();
|
|
}
|
|
|
|
export interface GetOmpPickerModelsOptions {
|
|
binaryPath?: string;
|
|
ttlMs?: number;
|
|
now?: () => number;
|
|
}
|
|
|
|
export async function getOmpPickerModels(
|
|
opts?: GetOmpPickerModelsOptions,
|
|
): Promise<OmpPickerModel[]> {
|
|
const binaryPath = opts?.binaryPath ?? "omp";
|
|
const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
|
|
const now = opts?.now ?? Date.now;
|
|
const nowMs = now();
|
|
|
|
const cached = cache.get(binaryPath);
|
|
if (cached && nowMs - cached.fetchedAt < cached.ttlMs) {
|
|
return cached.models;
|
|
}
|
|
|
|
const existingInFlight = inFlight.get(binaryPath);
|
|
if (existingInFlight) return existingInFlight;
|
|
|
|
const fetchPromise = (async (): Promise<OmpPickerModel[]> => {
|
|
try {
|
|
const result = await discoverOmpCliModels({ binaryPath });
|
|
if (!result || result.models.length === 0) return [];
|
|
return ompDiscoveryToModels(result.models);
|
|
} catch {
|
|
return [];
|
|
}
|
|
})();
|
|
|
|
inFlight.set(binaryPath, fetchPromise);
|
|
try {
|
|
const models = await fetchPromise;
|
|
const effectiveTtlMs = models.length === 0 ? EMPTY_RESULT_TTL_MS : ttlMs;
|
|
cache.set(binaryPath, { fetchedAt: now(), models, ttlMs: effectiveTtlMs });
|
|
return models;
|
|
} finally {
|
|
inFlight.delete(binaryPath);
|
|
}
|
|
}
|