| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- /**
- * v2 setup orchestration.
- *
- * Returns the `setup(ctx)` function v2 calls via `default.setup`. The setup
- * wraps the existing v1 factory (reusing ALL build logic) and translates the
- * returned v1 `Hooks` into v2 registrations: agent/tool/command transforms,
- * the session context hook (system + message transforms), tool execute hooks,
- * and the event stream. Each bridge is independently try/catch-guarded.
- */
- import { OhMyOpenCodeLite } from '../index';
- import { initLogger, log } from '../utils/logger';
- import { adaptTool, applyAgentToDraft } from './adapters';
- import { buildPluginInput } from './client-shim';
- import type {
- V2Cleanup,
- V2Context,
- V2ToolAfterEvent,
- V2ToolBeforeEvent,
- } from './types';
- export function createV2Setup(): (ctx: V2Context) => Promise<V2Cleanup> {
- return async (ctx: V2Context): Promise<V2Cleanup> => {
- const sessionId = new Date()
- .toISOString()
- .replace(/[-:]/g, '')
- .slice(0, 15);
- initLogger(sessionId);
- log('[v2] setup invoked', { app: ctx.app, cwd: process.cwd() });
- const directory = process.cwd();
- const disposers: Array<() => Promise<void> | void> = [];
- let v1Hooks: Record<string, unknown> | undefined;
- try {
- log('[v2] importing v1 factory...');
- const pluginInput = buildPluginInput(directory);
- log('[v2] calling OhMyOpenCodeLite...');
- v1Hooks = (await OhMyOpenCodeLite(
- pluginInput as never,
- )) as unknown as Record<string, unknown>;
- log('[v2] v1 factory initialized', {
- agents: Object.keys((v1Hooks as { agent?: object }).agent ?? {}).length,
- tools: Object.keys((v1Hooks as { tool?: object }).tool ?? {}).length,
- });
- } catch (err) {
- log('[v2] FATAL: v1 factory init failed', String(err));
- console.error('[oh-my-opencode-slim][v2] factory init failed:', err);
- // Don't hard-fail the whole plugin; register nothing and stay loaded.
- return async () => {};
- }
- if (!v1Hooks) return async () => {};
- // Resolve agents/commands via the v1 config() hook (model resolution etc.).
- let resolvedAgents: Record<string, Record<string, unknown>> | undefined;
- let synthCommands:
- | Record<string, { template?: string; description?: string }>
- | undefined;
- try {
- const synth: Record<string, unknown> = {};
- const configFn = v1Hooks.config as
- | ((c: Record<string, unknown>) => Promise<void>)
- | undefined;
- if (configFn) {
- await configFn(synth);
- if (synth.agent && typeof synth.agent === 'object') {
- resolvedAgents = synth.agent as Record<
- string,
- Record<string, unknown>
- >;
- }
- const cmd = synth.command as
- | Record<string, { template?: string; description?: string }>
- | undefined;
- if (cmd) synthCommands = cmd;
- }
- } catch (err) {
- log(
- '[v2] config() hook failed (continuing with raw agents)',
- String(err),
- );
- }
- if (!resolvedAgents) {
- resolvedAgents =
- (v1Hooks.agent as Record<string, Record<string, unknown>>) ?? {};
- }
- // ── Agents ──
- try {
- const reg = await ctx.agent.transform((draft) => {
- for (const [name, cfg] of Object.entries(resolvedAgents ?? {})) {
- try {
- applyAgentToDraft(draft, name, cfg);
- } catch (err) {
- log('[v2] agent adapt failed', { name, err: String(err) });
- }
- }
- // Make orchestrator the default primary agent.
- if (resolvedAgents?.orchestrator) {
- try {
- draft.default('orchestrator');
- } catch {
- /* default() optional */
- }
- }
- });
- disposers.push(() => reg.dispose());
- log('[v2] agents registered', {
- count: Object.keys(resolvedAgents ?? {}).length,
- });
- } catch (err) {
- log('[v2] agent.transform failed', String(err));
- }
- // ── Tools ──
- try {
- const tools = (v1Hooks.tool ?? {}) as Record<
- string,
- Record<string, unknown>
- >;
- const toolEntries = Object.entries(tools);
- if (toolEntries.length > 0) {
- // Precompute JSON schemas from zod shapes (zod is bundled in v2 build).
- const zod = (await import('zod')) as unknown as {
- object?: (s: unknown) => unknown;
- toJSONSchema?: (s: unknown) => unknown;
- };
- const schemaFor = (def: Record<string, unknown>): unknown => {
- const args = def.args;
- if (!args || typeof args !== 'object') {
- return { type: 'object', properties: {} };
- }
- try {
- const obj = zod.object?.(args);
- if (zod.toJSONSchema && obj) return zod.toJSONSchema(obj);
- } catch {
- /* fall through */
- }
- return { type: 'object', properties: {} };
- };
- const reg = await ctx.tool.transform((draft) => {
- for (const [name, def] of toolEntries) {
- try {
- draft.add(adaptTool(name, def, directory, schemaFor(def)));
- } catch (err) {
- log('[v2] tool adapt failed', { name, err: String(err) });
- }
- }
- });
- disposers.push(() => reg.dispose());
- log('[v2] tools registered', { count: toolEntries.length });
- }
- } catch (err) {
- log('[v2] tool.transform failed', String(err));
- }
- // ── Commands (deepwork / reflect / loop slash commands) ──
- try {
- const entries = Object.entries(synthCommands ?? {});
- if (entries.length > 0) {
- const reg = await ctx.command.transform((draft) => {
- for (const [name, cmd] of entries) {
- try {
- draft.update(name, (c) => {
- c.name = name;
- if (typeof cmd.template === 'string') c.template = cmd.template;
- if (typeof cmd.description === 'string')
- c.description = cmd.description;
- });
- } catch (err) {
- log('[v2] command adapt failed', { name, err: String(err) });
- }
- }
- });
- disposers.push(() => reg.dispose());
- log('[v2] commands registered', { count: entries.length });
- }
- } catch (err) {
- log('[v2] command.transform failed', String(err));
- }
- // ── System + messages transforms (session context hook) ──
- try {
- const systemTransform = v1Hooks['experimental.chat.system.transform'] as
- | ((i: unknown, o: { system: string[] }) => Promise<void>)
- | undefined;
- const messagesTransform = v1Hooks[
- 'experimental.chat.messages.transform'
- ] as
- | ((
- i: unknown,
- o: {
- messages: Array<{ info: { role: string }; parts: unknown[] }>;
- },
- ) => Promise<void>)
- | undefined;
- const chatMessage = v1Hooks['chat.message'] as
- | ((
- i: { sessionID: string; agent?: string },
- o: unknown,
- ) => Promise<void>)
- | undefined;
- if (systemTransform || messagesTransform || chatMessage) {
- const reg = await ctx.session.hook('context', async (event) => {
- // Agent tracking (chat.message equivalent).
- if (chatMessage) {
- try {
- await chatMessage(
- { sessionID: event.sessionID, agent: event.agent },
- undefined,
- );
- } catch (err) {
- log('[v2] chat.message bridge failed', String(err));
- }
- }
- // System transform: v2 SystemPart[] -> v1 string[] -> mutate -> back.
- if (systemTransform && Array.isArray(event.system)) {
- try {
- const sysStrings = event.system.map((s) => s.text ?? '');
- await systemTransform(
- { sessionID: event.sessionID },
- { system: sysStrings },
- );
- event.system = sysStrings.map((text) => ({
- type: 'text' as const,
- text,
- }));
- } catch (err) {
- log('[v2] system transform bridge failed', String(err));
- }
- }
- // Messages transform: v2 Message.content -> v1 {info, parts} -> back.
- if (messagesTransform && Array.isArray(event.messages)) {
- try {
- const v1messages = event.messages.map((m) => ({
- info: { role: m.role },
- parts: m.content,
- }));
- await messagesTransform({}, { messages: v1messages });
- event.messages.forEach((m, i) => {
- m.content = (v1messages[i]?.parts ?? []) as Array<
- Record<string, unknown>
- >;
- });
- } catch (err) {
- log('[v2] messages transform bridge failed', String(err));
- }
- }
- });
- disposers.push(() => reg.dispose());
- log('[v2] session context hook registered');
- }
- } catch (err) {
- log('[v2] session.hook(context) failed', String(err));
- }
- // ── Tool execute hooks ──
- try {
- const before = v1Hooks['tool.execute.before'] as
- | ((
- i: { tool: string; sessionID: string; callID: string },
- o: { args: unknown },
- ) => Promise<void>)
- | undefined;
- const after = v1Hooks['tool.execute.after'] as
- | ((i: unknown, o: unknown) => Promise<void>)
- | undefined;
- if (before) {
- const reg = await ctx.tool.hook('execute.before', async (event) => {
- const e = event as V2ToolBeforeEvent;
- try {
- await before(
- { tool: e.tool, sessionID: e.sessionID, callID: e.id },
- { args: e.input },
- );
- } catch (err) {
- log('[v2] tool.execute.before bridge failed', String(err));
- }
- });
- disposers.push(() => reg.dispose());
- }
- if (after) {
- const reg = await ctx.tool.hook('execute.after', async (event) => {
- const e = event as V2ToolAfterEvent;
- // Map v2 Tool.Result.content (string | Content[]) -> v1 output.output
- // string; the v1 after-hooks (postFileToolNudge, jsonErrorRecovery,
- // taskSessionManagerAfter) read output.output to decide nudges.
- const result = e.result as
- | {
- content?: unknown;
- metadata?: Record<string, unknown>;
- }
- | undefined;
- const rawContent = result?.content;
- const content =
- typeof rawContent === 'string'
- ? rawContent
- : Array.isArray(rawContent)
- ? (rawContent as Array<{ type?: string; text?: string }>)
- .filter((p) => p?.type === 'text')
- .map((p) => p.text ?? '')
- .join('')
- : '';
- try {
- await after(
- {
- tool: e.tool,
- sessionID: e.sessionID,
- callID: e.id,
- args: e.input,
- },
- { output: content, title: '', metadata: result?.metadata ?? {} },
- );
- } catch (err) {
- log('[v2] tool.execute.after bridge failed', String(err));
- }
- });
- disposers.push(() => reg.dispose());
- }
- log('[v2] tool hooks registered', { before: !!before, after: !!after });
- } catch (err) {
- log('[v2] tool.hook registration failed', String(err));
- }
- // ── Event stream ──
- try {
- const eventHook = v1Hooks.event as
- | ((i: { event: Record<string, unknown> }) => Promise<void>)
- | undefined;
- if (eventHook) {
- const iter = ctx.event.subscribe();
- void (async () => {
- try {
- for await (const ev of iter) {
- try {
- await eventHook({ event: ev });
- } catch (err) {
- log('[v2] event handler failed', String(err));
- }
- }
- } catch (err) {
- log('[v2] event stream ended', String(err));
- }
- })();
- log('[v2] event stream subscribed');
- }
- } catch (err) {
- log('[v2] event.subscribe failed', String(err));
- }
- // ── Health check: surface silent zero-registration failures ──
- // Every bridge is fail-soft; without this, a fully broken registration
- // would look like a successful load with an empty session.
- if (disposers.length === 0) {
- console.error(
- '[oh-my-opencode-slim][v2] WARNING: no bridges registered — ' +
- 'the plugin loaded but registered nothing. Check the plugin log.',
- );
- log('[v2] health check: zero bridges registered');
- } else {
- log('[v2] health check passed', { bridges: disposers.length });
- }
- const dispose = v1Hooks.dispose as (() => Promise<void>) | undefined;
- return async () => {
- log('[v2] dispose invoked');
- for (const d of disposers) {
- try {
- await d();
- } catch (err) {
- log('[v2] disposer failed', String(err));
- }
- }
- try {
- await dispose?.();
- } catch (err) {
- log('[v2] v1 dispose failed', String(err));
- }
- };
- };
- }
|