oh-my-claudecode-opencode 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: version
3
+ description: Check OMCO plugin version and update status
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Version Check Skill
8
+
9
+ Display the current OMCO plugin version and check for updates.
10
+
11
+ ## What to Do
12
+
13
+ When user invokes this skill, run the following checks:
14
+
15
+ ### 1. Check Installed Version
16
+
17
+ ```bash
18
+ # Read version from installed location
19
+ cat ~/.config/opencode/node_modules/oh-my-claudecode-opencode/package.json 2>/dev/null | grep '"version"' | head -1
20
+ ```
21
+
22
+ ### 2. Check Development Version (if in dev directory)
23
+
24
+ ```bash
25
+ # If in oh-my-claudecode-opencode project directory
26
+ cat package.json 2>/dev/null | grep '"version"' | head -1
27
+ ```
28
+
29
+ ### 3. Check Latest Version on npm
30
+
31
+ ```bash
32
+ npm view oh-my-claudecode-opencode version 2>/dev/null
33
+ ```
34
+
35
+ ## Output Format
36
+
37
+ Present the information in a clear table:
38
+
39
+ | Location | Version |
40
+ |----------|---------|
41
+ | Installed | X.X.X |
42
+ | Development | X.X.X (if applicable) |
43
+ | npm Latest | X.X.X |
44
+
45
+ ### Status Indicators
46
+
47
+ - **Up to date**: Installed version matches npm latest
48
+ - **Update available**: npm has newer version
49
+ - **Ahead of npm**: Local version is higher (pre-release or unpublished)
50
+ - **Mismatch**: Development and installed versions differ
51
+
52
+ ## Update Instructions
53
+
54
+ If update is available:
55
+ ```bash
56
+ cd ~/.config/opencode && npm install oh-my-claudecode-opencode@latest
57
+ ```
58
+
59
+ If development version differs from installed:
60
+ ```bash
61
+ # From development directory
62
+ bun run build
63
+ cp -r dist/ ~/.config/opencode/node_modules/oh-my-claudecode-opencode/dist/
64
+ cp package.json ~/.config/opencode/node_modules/oh-my-claudecode-opencode/package.json
65
+ ```
66
+
67
+ ## After Update
68
+
69
+ Remind users to restart OpenCode for changes to take effect.
@@ -22,13 +22,29 @@ export interface ModelResolutionResult {
22
22
  source: "per-agent-override" | "tier-default" | "hardcoded-fallback";
23
23
  originalTier?: ModelTier;
24
24
  }
25
+ /**
26
+ * Default tier-to-model mapping.
27
+ * These use generic names - configure model_mapping.tierDefaults in
28
+ * ~/.config/opencode/oh-my-opencode.json for your specific provider.
29
+ *
30
+ * Example config:
31
+ * {
32
+ * "model_mapping": {
33
+ * "tierDefaults": {
34
+ * "haiku": "google/gemini-2.0-flash",
35
+ * "sonnet": "anthropic/claude-sonnet-4",
36
+ * "opus": "anthropic/claude-opus-4"
37
+ * }
38
+ * }
39
+ * }
40
+ */
25
41
  export declare const HARDCODED_TIER_DEFAULTS: TierModelMapping;
26
42
  /**
27
- * Check if model follows "provider/model-name" pattern
43
+ * Check if model follows "provider/model-name" pattern or is a simple tier name
28
44
  */
29
45
  export declare function isValidModelFormat(model: string): boolean;
30
46
  /**
31
- * Log warning if invalid format
47
+ * Log warning if invalid format (skip for simple tier names)
32
48
  */
33
49
  export declare function validateModelFormat(model: string, context: string): void;
34
50
  export declare class ModelResolver {
package/dist/index.js CHANGED
@@ -9221,7 +9221,7 @@ class Doc {
9221
9221
  var version = {
9222
9222
  major: 4,
9223
9223
  minor: 3,
9224
- patch: 6
9224
+ patch: 5
9225
9225
  };
9226
9226
 
9227
9227
  // node_modules/zod/v4/core/schemas.js
@@ -10507,7 +10507,7 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
10507
10507
  if (keyResult instanceof Promise) {
10508
10508
  throw new Error("Async schemas not supported in object keys currently");
10509
10509
  }
10510
- const checkNumericKey = typeof key === "string" && number.test(key) && keyResult.issues.length;
10510
+ const checkNumericKey = typeof key === "string" && number.test(key) && keyResult.issues.length && keyResult.issues.some((iss) => iss.code === "invalid_type" && iss.expected === "number");
10511
10511
  if (checkNumericKey) {
10512
10512
  const retryResult = def.keyType._zod.run({ value: Number(key), issues: [] }, ctx);
10513
10513
  if (retryResult instanceof Promise) {
@@ -17878,7 +17878,7 @@ function finalize(ctx, schema) {
17878
17878
  }
17879
17879
  }
17880
17880
  }
17881
- if (refSchema.$ref && refSeen.def) {
17881
+ if (refSchema.$ref) {
17882
17882
  for (const key in schema2) {
17883
17883
  if (key === "$ref" || key === "allOf")
17884
17884
  continue;
@@ -21669,9 +21669,10 @@ function generateTaskId() {
21669
21669
  taskCounter++;
21670
21670
  return `bg_${Date.now().toString(36)}_${taskCounter.toString(36)}`;
21671
21671
  }
21672
- function createBackgroundManager(ctx, config2) {
21672
+ function createBackgroundManager(ctx, config2, modelService) {
21673
21673
  const tasks = new Map;
21674
21674
  const defaultConcurrency = config2?.defaultConcurrency ?? 5;
21675
+ const modelCache = new Map;
21675
21676
  const getRunningCount = (parentSessionID) => {
21676
21677
  let count = 0;
21677
21678
  for (const task of tasks.values()) {
@@ -21683,7 +21684,34 @@ function createBackgroundManager(ctx, config2) {
21683
21684
  }
21684
21685
  return count;
21685
21686
  };
21686
- const createTask = async (parentSessionID, description, prompt, agent) => {
21687
+ const getParentSessionModel = async (parentSessionID) => {
21688
+ if (modelCache.has(parentSessionID)) {
21689
+ return modelCache.get(parentSessionID);
21690
+ }
21691
+ try {
21692
+ const messagesResp = await ctx.client.session.messages({
21693
+ path: { id: parentSessionID },
21694
+ query: { directory: ctx.directory }
21695
+ });
21696
+ const messages = messagesResp.data;
21697
+ const assistantMsg = messages?.find((m) => m.info.role === "assistant" && m.info.providerID && m.info.modelID);
21698
+ if (assistantMsg?.info.providerID && assistantMsg?.info.modelID) {
21699
+ const model = {
21700
+ providerID: assistantMsg.info.providerID,
21701
+ modelID: assistantMsg.info.modelID
21702
+ };
21703
+ modelCache.set(parentSessionID, model);
21704
+ log(`Got parent session model from messages`, { parentSessionID, ...model });
21705
+ return model;
21706
+ }
21707
+ log(`Parent session has no assistant messages with model info`, { parentSessionID });
21708
+ return;
21709
+ } catch (err) {
21710
+ log(`Failed to get parent session model`, { parentSessionID, error: String(err) });
21711
+ return;
21712
+ }
21713
+ };
21714
+ const createTask = async (parentSessionID, description, prompt, agent, model) => {
21687
21715
  const runningCount = getRunningCount();
21688
21716
  if (runningCount >= defaultConcurrency) {
21689
21717
  throw new Error(`Max concurrent tasks (${defaultConcurrency}) reached. Wait for some to complete.`);
@@ -21698,6 +21726,14 @@ function createBackgroundManager(ctx, config2) {
21698
21726
  };
21699
21727
  tasks.set(taskId, task);
21700
21728
  log(`Background task created`, { taskId, description, agent });
21729
+ const parentModel = model || await getParentSessionModel(parentSessionID);
21730
+ const resolvedModel = modelService ? modelService.resolveModelForAgent(agent, parentModel) : parentModel;
21731
+ if (resolvedModel && resolvedModel !== parentModel) {
21732
+ log(`[background-manager] Using tier-mapped model for ${agent}`, {
21733
+ providerID: resolvedModel.providerID,
21734
+ modelID: resolvedModel.modelID
21735
+ });
21736
+ }
21701
21737
  (async () => {
21702
21738
  try {
21703
21739
  const sessionResp = await ctx.client.session.create({
@@ -21717,19 +21753,28 @@ function createBackgroundManager(ctx, config2) {
21717
21753
  const fullPrompt = systemPrompt ? `${systemPrompt}
21718
21754
 
21719
21755
  ${prompt}` : prompt;
21720
- await ctx.client.session.prompt({
21756
+ const promptBody = {
21757
+ parts: [{ type: "text", text: fullPrompt }]
21758
+ };
21759
+ if (resolvedModel) {
21760
+ promptBody.model = resolvedModel;
21761
+ log(`Using parent model for subagent`, { taskId, ...resolvedModel });
21762
+ }
21763
+ const promptResp = await ctx.client.session.prompt({
21721
21764
  path: { id: sessionID },
21722
- body: {
21723
- parts: [{ type: "text", text: fullPrompt }]
21724
- },
21765
+ body: promptBody,
21725
21766
  query: { directory: ctx.directory }
21726
21767
  });
21727
- const messagesResp = await ctx.client.session.messages({
21728
- path: { id: sessionID }
21729
- });
21730
- const messages = messagesResp.data ?? [];
21731
- const lastAssistant = [...messages].reverse().find((m) => m.info?.role === "assistant");
21732
- const result = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
21768
+ const promptData = promptResp.data;
21769
+ if (promptResp.error) {
21770
+ throw new Error(`Prompt failed: ${JSON.stringify(promptResp.error)}`);
21771
+ }
21772
+ if (promptData?.info?.error) {
21773
+ const err = promptData.info.error;
21774
+ const errMsg = err.data?.message || err.name || "Unknown error";
21775
+ throw new Error(`[${err.name}] ${errMsg}`);
21776
+ }
21777
+ const result = promptData?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
21733
21778
  `) || "";
21734
21779
  task.result = result;
21735
21780
  task.status = "completed";
@@ -21770,6 +21815,14 @@ ${prompt}` : prompt;
21770
21815
  return false;
21771
21816
  task.status = "cancelled";
21772
21817
  task.completedAt = Date.now();
21818
+ if (task.sessionID) {
21819
+ ctx.client.session.abort({
21820
+ path: { id: task.sessionID },
21821
+ query: { directory: ctx.directory }
21822
+ }).catch((err) => {
21823
+ log(`Failed to abort session for task ${taskId}`, { error: String(err) });
21824
+ });
21825
+ }
21773
21826
  log(`Background task cancelled`, { taskId });
21774
21827
  return true;
21775
21828
  };
@@ -21781,6 +21834,14 @@ ${prompt}` : prompt;
21781
21834
  task.status = "cancelled";
21782
21835
  task.completedAt = Date.now();
21783
21836
  count++;
21837
+ if (task.sessionID) {
21838
+ ctx.client.session.abort({
21839
+ path: { id: task.sessionID },
21840
+ query: { directory: ctx.directory }
21841
+ }).catch((err) => {
21842
+ log(`Failed to abort session for task ${task.id}`, { error: String(err) });
21843
+ });
21844
+ }
21784
21845
  }
21785
21846
  }
21786
21847
  }
@@ -21808,7 +21869,8 @@ ${prompt}` : prompt;
21808
21869
  getTasksByParentSession,
21809
21870
  cancelTask,
21810
21871
  cancelAllTasks,
21811
- waitForTask
21872
+ waitForTask,
21873
+ getParentSessionModel
21812
21874
  };
21813
21875
  }
21814
21876
 
@@ -34201,7 +34263,7 @@ Use \`background_output\` to get results. Prompts MUST be in English.`,
34201
34263
  }
34202
34264
 
34203
34265
  // src/tools/call-omco-agent.ts
34204
- function createCallOmcoAgent(ctx, manager) {
34266
+ function createCallOmcoAgent(ctx, manager, modelService) {
34205
34267
  const agentNames = listAgentNames();
34206
34268
  const agentList = agentNames.map((name) => {
34207
34269
  const agent = getAgent(name);
@@ -34237,8 +34299,16 @@ Prompts MUST be in English. Use \`background_output\` for async results.`,
34237
34299
  ---
34238
34300
 
34239
34301
  ${prompt}`;
34302
+ const parentModel = await manager.getParentSessionModel(context.sessionID);
34303
+ const resolvedModel = modelService ? modelService.resolveModelForAgent(subagent_type, parentModel) : parentModel;
34304
+ if (resolvedModel && resolvedModel !== parentModel) {
34305
+ log(`[call-omco-agent] Using tier-mapped model for ${subagent_type}`, {
34306
+ providerID: resolvedModel.providerID,
34307
+ modelID: resolvedModel.modelID
34308
+ });
34309
+ }
34240
34310
  if (run_in_background) {
34241
- const task = await manager.createTask(context.sessionID, description, enhancedPrompt, subagent_type);
34311
+ const task = await manager.createTask(context.sessionID, description, enhancedPrompt, subagent_type, resolvedModel);
34242
34312
  return JSON.stringify({
34243
34313
  task_id: task.id,
34244
34314
  session_id: task.sessionID,
@@ -34257,19 +34327,36 @@ ${prompt}`;
34257
34327
  const sessionID = sessionResp.data?.id ?? sessionResp.id;
34258
34328
  if (!sessionID)
34259
34329
  throw new Error("Failed to create session");
34260
- await ctx.client.session.prompt({
34330
+ const promptBody = {
34331
+ parts: [{ type: "text", text: enhancedPrompt }]
34332
+ };
34333
+ if (resolvedModel) {
34334
+ promptBody.model = resolvedModel;
34335
+ log(`Using resolved model for sync agent call`, { subagent_type, ...resolvedModel });
34336
+ }
34337
+ const promptResp = await ctx.client.session.prompt({
34261
34338
  path: { id: sessionID },
34262
- body: {
34263
- parts: [{ type: "text", text: enhancedPrompt }]
34264
- },
34339
+ body: promptBody,
34265
34340
  query: { directory: ctx.directory }
34266
34341
  });
34267
- const messagesResp = await ctx.client.session.messages({
34268
- path: { id: sessionID }
34269
- });
34270
- const messages = messagesResp.data ?? [];
34271
- const lastAssistant = [...messages].reverse().find((m) => m.info?.role === "assistant");
34272
- const result = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
34342
+ if (promptResp.error) {
34343
+ return JSON.stringify({
34344
+ session_id: sessionID,
34345
+ status: "failed",
34346
+ error: `Prompt failed: ${JSON.stringify(promptResp.error)}`
34347
+ });
34348
+ }
34349
+ const promptData = promptResp.data;
34350
+ if (promptData?.info?.error) {
34351
+ const err = promptData.info.error;
34352
+ const errMsg = err.data?.message || err.name || "Unknown error";
34353
+ return JSON.stringify({
34354
+ session_id: sessionID,
34355
+ status: "failed",
34356
+ error: `[${err.name}] ${errMsg}`
34357
+ });
34358
+ }
34359
+ const result = promptData?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
34273
34360
  `) || "";
34274
34361
  return JSON.stringify({
34275
34362
  session_id: sessionID,
@@ -34288,16 +34375,20 @@ ${prompt}`;
34288
34375
 
34289
34376
  // src/config/model-resolver.ts
34290
34377
  var HARDCODED_TIER_DEFAULTS = {
34291
- haiku: "github-copilot/claude-haiku-4",
34292
- sonnet: "github-copilot/claude-sonnet-4",
34293
- opus: "github-copilot/claude-opus-4"
34378
+ haiku: "haiku",
34379
+ sonnet: "sonnet",
34380
+ opus: "opus"
34294
34381
  };
34382
+ var SIMPLE_TIER_NAMES = new Set(["haiku", "sonnet", "opus"]);
34295
34383
  function isValidModelFormat(model) {
34384
+ if (SIMPLE_TIER_NAMES.has(model.toLowerCase())) {
34385
+ return true;
34386
+ }
34296
34387
  return /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+$/.test(model);
34297
34388
  }
34298
34389
  function validateModelFormat(model, context) {
34299
34390
  if (!isValidModelFormat(model)) {
34300
- warn(`[model-resolver] [${context}] Model "${model}" does not follow "provider/model-name" format`);
34391
+ warn(`[model-resolver] [${context}] Model "${model}" does not follow "provider/model-name" format. Configure model_mapping.tierDefaults in oh-my-opencode.json`);
34301
34392
  }
34302
34393
  }
34303
34394
 
@@ -34361,6 +34452,51 @@ class ModelResolver {
34361
34452
  }
34362
34453
  }
34363
34454
 
34455
+ // src/tools/model-resolution-service.ts
34456
+ function parseModelString(model) {
34457
+ if (!model.includes("/")) {
34458
+ return;
34459
+ }
34460
+ const [providerID, ...rest] = model.split("/");
34461
+ const modelID = rest.join("/");
34462
+ if (!providerID || !modelID) {
34463
+ return;
34464
+ }
34465
+ return { providerID, modelID };
34466
+ }
34467
+ function createModelResolutionService(modelMappingConfig, agentOverrides) {
34468
+ const resolver = new ModelResolver(modelMappingConfig);
34469
+ const debugLogging = modelMappingConfig?.debugLogging ?? false;
34470
+ const tierDefaults = resolver.getTierDefaults();
34471
+ const hasConfiguredTiers = Object.values(tierDefaults).some((m) => m.includes("/"));
34472
+ const resolveModelForAgent = (agentName, fallbackModel) => {
34473
+ const agentDef = getAgent(agentName);
34474
+ const agentTier = agentDef?.model;
34475
+ const agentOverride = agentOverrides?.[agentName];
34476
+ const resolution = resolver.resolve(agentName, agentTier, agentOverride);
34477
+ const modelConfig = parseModelString(resolution.model);
34478
+ if (modelConfig) {
34479
+ if (debugLogging) {
34480
+ log(`[model-resolution] Resolved ${agentName}: ${resolution.model} (source: ${resolution.source})`);
34481
+ }
34482
+ return modelConfig;
34483
+ }
34484
+ if (debugLogging) {
34485
+ log(`[model-resolution] ${agentName}: No provider mapping for "${resolution.model}", using fallback`, {
34486
+ fallback: fallbackModel ? `${fallbackModel.providerID}/${fallbackModel.modelID}` : "none"
34487
+ });
34488
+ }
34489
+ return fallbackModel;
34490
+ };
34491
+ const isTierMappingConfigured = () => {
34492
+ return hasConfiguredTiers;
34493
+ };
34494
+ return {
34495
+ resolveModelForAgent,
34496
+ isTierMappingConfigured
34497
+ };
34498
+ }
34499
+
34364
34500
  // src/skills/index.ts
34365
34501
  import { join as join4, dirname as dirname2 } from "path";
34366
34502
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -38229,13 +38365,30 @@ function getAgentEmoji(agentName) {
38229
38365
  return emojiMap[agentName] || "\uD83E\uDD16";
38230
38366
  }
38231
38367
 
38368
+ // src/state/session-pause-state.ts
38369
+ var pauseStates = new Map;
38370
+ function isSessionPaused(sessionId) {
38371
+ return pauseStates.get(sessionId)?.isPaused ?? false;
38372
+ }
38373
+ function resumeSession(sessionId) {
38374
+ const state = pauseStates.get(sessionId);
38375
+ if (state?.isPaused) {
38376
+ log(`Session resumed`, { sessionId, wasPausedFor: Date.now() - (state.pausedAt ?? 0) });
38377
+ }
38378
+ pauseStates.delete(sessionId);
38379
+ }
38380
+
38232
38381
  // src/index.ts
38233
38382
  var OmoOmcsPlugin = async (ctx) => {
38234
38383
  const pluginConfig = loadConfig(ctx.directory);
38235
38384
  console.log("[omco] Config loaded:", pluginConfig);
38236
- const backgroundManager = createBackgroundManager(ctx, pluginConfig.background_task);
38385
+ const modelService = createModelResolutionService(pluginConfig.model_mapping, pluginConfig.agents);
38386
+ if (modelService.isTierMappingConfigured()) {
38387
+ log("[omco] Model tier mapping configured - agents will use tier-specific models");
38388
+ }
38389
+ const backgroundManager = createBackgroundManager(ctx, pluginConfig.background_task, modelService);
38237
38390
  const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
38238
- const callOmcoAgent = createCallOmcoAgent(ctx, backgroundManager);
38391
+ const callOmcoAgent = createCallOmcoAgent(ctx, backgroundManager, modelService);
38239
38392
  const systemPromptInjector = createSystemPromptInjector(ctx);
38240
38393
  const skillInjector = createSkillInjector(ctx);
38241
38394
  const ralphLoop = createRalphLoopHook(ctx, {
@@ -38335,6 +38488,10 @@ var OmoOmcsPlugin = async (ctx) => {
38335
38488
  } else {
38336
38489
  systemPromptInjector.clearSkillInjection(input.sessionID);
38337
38490
  }
38491
+ if (isSessionPaused(input.sessionID)) {
38492
+ resumeSession(input.sessionID);
38493
+ log("[session] Auto-resumed on user prompt", { sessionID: input.sessionID });
38494
+ }
38338
38495
  await autopilot["chat.message"](input, output);
38339
38496
  await ultraqaLoop["chat.message"](input, output);
38340
38497
  },
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared session pause state for coordinating abort handling across hooks.
3
+ * When user presses ESC (triggers MessageAbortedError), the session is paused
4
+ * and remains paused until explicitly resumed or user sends a new prompt.
5
+ */
6
+ interface SessionPauseState {
7
+ isPaused: boolean;
8
+ pausedAt: number | null;
9
+ pauseReason: 'user_abort' | 'error' | 'explicit' | null;
10
+ }
11
+ /**
12
+ * Pause a session, preventing automatic continuations.
13
+ * Called when MessageAbortedError is detected (user pressed ESC).
14
+ */
15
+ export declare function pauseSession(sessionId: string, reason?: SessionPauseState['pauseReason']): void;
16
+ /**
17
+ * Check if a session is currently paused.
18
+ */
19
+ export declare function isSessionPaused(sessionId: string): boolean;
20
+ /**
21
+ * Get the pause state for a session.
22
+ */
23
+ export declare function getSessionPauseState(sessionId: string): SessionPauseState | undefined;
24
+ /**
25
+ * Resume a session, allowing automatic continuations.
26
+ * Called when user sends a new prompt or explicitly resumes.
27
+ */
28
+ export declare function resumeSession(sessionId: string): void;
29
+ /**
30
+ * Clear session state when session is deleted.
31
+ */
32
+ export declare function clearSessionPauseState(sessionId: string): void;
33
+ export {};
@@ -1,5 +1,6 @@
1
1
  import type { PluginInput } from "@opencode-ai/plugin";
2
2
  import type { BackgroundTaskConfig } from "../config";
3
+ import type { ModelResolutionService } from "./model-resolution-service";
3
4
  export interface BackgroundTask {
4
5
  id: string;
5
6
  status: "running" | "completed" | "failed" | "cancelled";
@@ -11,12 +12,17 @@ export interface BackgroundTask {
11
12
  startedAt: number;
12
13
  completedAt?: number;
13
14
  }
15
+ export interface ModelConfig {
16
+ providerID: string;
17
+ modelID: string;
18
+ }
14
19
  export interface BackgroundManager {
15
- createTask: (parentSessionID: string, description: string, prompt: string, agent: string) => Promise<BackgroundTask>;
20
+ createTask: (parentSessionID: string, description: string, prompt: string, agent: string, model?: ModelConfig) => Promise<BackgroundTask>;
16
21
  getTask: (taskId: string) => BackgroundTask | undefined;
17
22
  getTasksByParentSession: (sessionID: string) => BackgroundTask[];
18
23
  cancelTask: (taskId: string) => boolean;
19
24
  cancelAllTasks: (parentSessionID?: string) => number;
20
25
  waitForTask: (taskId: string, timeoutMs?: number) => Promise<BackgroundTask>;
26
+ getParentSessionModel: (parentSessionID: string) => Promise<ModelConfig | undefined>;
21
27
  }
22
- export declare function createBackgroundManager(ctx: PluginInput, config?: BackgroundTaskConfig): BackgroundManager;
28
+ export declare function createBackgroundManager(ctx: PluginInput, config?: BackgroundTaskConfig, modelService?: ModelResolutionService): BackgroundManager;
@@ -1,3 +1,4 @@
1
1
  import { type PluginInput, type ToolDefinition } from "@opencode-ai/plugin";
2
2
  import type { BackgroundManager } from "./background-manager";
3
- export declare function createCallOmcoAgent(ctx: PluginInput, manager: BackgroundManager): ToolDefinition;
3
+ import type { ModelResolutionService } from "./model-resolution-service";
4
+ export declare function createCallOmcoAgent(ctx: PluginInput, manager: BackgroundManager, modelService?: ModelResolutionService): ToolDefinition;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Model Resolution Service
3
+ *
4
+ * Centralized model resolution for agent invocations.
5
+ * Connects ModelResolver (config-based tier mapping) to runtime tool calls.
6
+ *
7
+ * Priority chain:
8
+ * 1. Per-agent config model override
9
+ * 2. Per-agent config tier → tierDefaults
10
+ * 3. Agent definition tier → tierDefaults
11
+ * 4. Fallback to sonnet tier
12
+ * 5. If all else fails, use parent session model
13
+ */
14
+ import { type AgentModelConfig, type ModelMappingConfig } from "../config/model-resolver";
15
+ export interface ModelConfig {
16
+ providerID: string;
17
+ modelID: string;
18
+ }
19
+ export interface ModelResolutionService {
20
+ /**
21
+ * Resolve model for an agent based on tier configuration
22
+ * @param agentName - Name of the agent (canonical or alias)
23
+ * @param fallbackModel - Parent session model to use if resolution fails
24
+ * @returns Resolved ModelConfig or undefined if should use fallback
25
+ */
26
+ resolveModelForAgent(agentName: string, fallbackModel?: ModelConfig): ModelConfig | undefined;
27
+ /**
28
+ * Check if tier mapping is configured (tierDefaults has provider/model format)
29
+ */
30
+ isTierMappingConfigured(): boolean;
31
+ }
32
+ /**
33
+ * Create a ModelResolutionService instance
34
+ *
35
+ * @param modelMappingConfig - Config from omco.json model_mapping section
36
+ * @param agentOverrides - Per-agent config overrides from omco.json agents section
37
+ */
38
+ export declare function createModelResolutionService(modelMappingConfig?: ModelMappingConfig, agentOverrides?: Record<string, AgentModelConfig>): ModelResolutionService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-claudecode-opencode",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "OpenCode port of oh-my-claudecode - Multi-agent orchestration plugin (omco)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,6 +28,9 @@
28
28
  "prepublishOnly": "bun run clean && bun run build",
29
29
  "typecheck": "tsc --noEmit",
30
30
  "test": "bun test",
31
+ "test:e2e": "vitest run --config tests/e2e/vitest.config.ts",
32
+ "test:e2e:server": "vitest run --config tests/e2e/vitest.config.ts --testNamePattern 'Server'",
33
+ "test:all": "bun test && vitest run --config tests/e2e/vitest.config.ts",
31
34
  "lint": "eslint src --ext .ts"
32
35
  },
33
36
  "keywords": [