oh-my-claudecode-opencode 0.6.2 → 0.6.3

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;
@@ -21672,6 +21672,7 @@ function generateTaskId() {
21672
21672
  function createBackgroundManager(ctx, config2) {
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,7 @@ function createBackgroundManager(ctx, config2) {
21698
21726
  };
21699
21727
  tasks.set(taskId, task);
21700
21728
  log(`Background task created`, { taskId, description, agent });
21729
+ const resolvedModel = model || await getParentSessionModel(parentSessionID);
21701
21730
  (async () => {
21702
21731
  try {
21703
21732
  const sessionResp = await ctx.client.session.create({
@@ -21717,19 +21746,28 @@ function createBackgroundManager(ctx, config2) {
21717
21746
  const fullPrompt = systemPrompt ? `${systemPrompt}
21718
21747
 
21719
21748
  ${prompt}` : prompt;
21720
- await ctx.client.session.prompt({
21749
+ const promptBody = {
21750
+ parts: [{ type: "text", text: fullPrompt }]
21751
+ };
21752
+ if (resolvedModel) {
21753
+ promptBody.model = resolvedModel;
21754
+ log(`Using parent model for subagent`, { taskId, ...resolvedModel });
21755
+ }
21756
+ const promptResp = await ctx.client.session.prompt({
21721
21757
  path: { id: sessionID },
21722
- body: {
21723
- parts: [{ type: "text", text: fullPrompt }]
21724
- },
21758
+ body: promptBody,
21725
21759
  query: { directory: ctx.directory }
21726
21760
  });
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(`
21761
+ const promptData = promptResp.data;
21762
+ if (promptResp.error) {
21763
+ throw new Error(`Prompt failed: ${JSON.stringify(promptResp.error)}`);
21764
+ }
21765
+ if (promptData?.info?.error) {
21766
+ const err = promptData.info.error;
21767
+ const errMsg = err.data?.message || err.name || "Unknown error";
21768
+ throw new Error(`[${err.name}] ${errMsg}`);
21769
+ }
21770
+ const result = promptData?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
21733
21771
  `) || "";
21734
21772
  task.result = result;
21735
21773
  task.status = "completed";
@@ -21770,6 +21808,14 @@ ${prompt}` : prompt;
21770
21808
  return false;
21771
21809
  task.status = "cancelled";
21772
21810
  task.completedAt = Date.now();
21811
+ if (task.sessionID) {
21812
+ ctx.client.session.abort({
21813
+ path: { id: task.sessionID },
21814
+ query: { directory: ctx.directory }
21815
+ }).catch((err) => {
21816
+ log(`Failed to abort session for task ${taskId}`, { error: String(err) });
21817
+ });
21818
+ }
21773
21819
  log(`Background task cancelled`, { taskId });
21774
21820
  return true;
21775
21821
  };
@@ -21781,6 +21827,14 @@ ${prompt}` : prompt;
21781
21827
  task.status = "cancelled";
21782
21828
  task.completedAt = Date.now();
21783
21829
  count++;
21830
+ if (task.sessionID) {
21831
+ ctx.client.session.abort({
21832
+ path: { id: task.sessionID },
21833
+ query: { directory: ctx.directory }
21834
+ }).catch((err) => {
21835
+ log(`Failed to abort session for task ${task.id}`, { error: String(err) });
21836
+ });
21837
+ }
21784
21838
  }
21785
21839
  }
21786
21840
  }
@@ -21808,7 +21862,8 @@ ${prompt}` : prompt;
21808
21862
  getTasksByParentSession,
21809
21863
  cancelTask,
21810
21864
  cancelAllTasks,
21811
- waitForTask
21865
+ waitForTask,
21866
+ getParentSessionModel
21812
21867
  };
21813
21868
  }
21814
21869
 
@@ -34237,8 +34292,9 @@ Prompts MUST be in English. Use \`background_output\` for async results.`,
34237
34292
  ---
34238
34293
 
34239
34294
  ${prompt}`;
34295
+ const parentModel = await manager.getParentSessionModel(context.sessionID);
34240
34296
  if (run_in_background) {
34241
- const task = await manager.createTask(context.sessionID, description, enhancedPrompt, subagent_type);
34297
+ const task = await manager.createTask(context.sessionID, description, enhancedPrompt, subagent_type, parentModel);
34242
34298
  return JSON.stringify({
34243
34299
  task_id: task.id,
34244
34300
  session_id: task.sessionID,
@@ -34257,19 +34313,36 @@ ${prompt}`;
34257
34313
  const sessionID = sessionResp.data?.id ?? sessionResp.id;
34258
34314
  if (!sessionID)
34259
34315
  throw new Error("Failed to create session");
34260
- await ctx.client.session.prompt({
34316
+ const promptBody = {
34317
+ parts: [{ type: "text", text: enhancedPrompt }]
34318
+ };
34319
+ if (parentModel) {
34320
+ promptBody.model = parentModel;
34321
+ log(`Using parent model for sync agent call`, { subagent_type, ...parentModel });
34322
+ }
34323
+ const promptResp = await ctx.client.session.prompt({
34261
34324
  path: { id: sessionID },
34262
- body: {
34263
- parts: [{ type: "text", text: enhancedPrompt }]
34264
- },
34325
+ body: promptBody,
34265
34326
  query: { directory: ctx.directory }
34266
34327
  });
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(`
34328
+ if (promptResp.error) {
34329
+ return JSON.stringify({
34330
+ session_id: sessionID,
34331
+ status: "failed",
34332
+ error: `Prompt failed: ${JSON.stringify(promptResp.error)}`
34333
+ });
34334
+ }
34335
+ const promptData = promptResp.data;
34336
+ if (promptData?.info?.error) {
34337
+ const err = promptData.info.error;
34338
+ const errMsg = err.data?.message || err.name || "Unknown error";
34339
+ return JSON.stringify({
34340
+ session_id: sessionID,
34341
+ status: "failed",
34342
+ error: `[${err.name}] ${errMsg}`
34343
+ });
34344
+ }
34345
+ const result = promptData?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
34273
34346
  `) || "";
34274
34347
  return JSON.stringify({
34275
34348
  session_id: sessionID,
@@ -34288,16 +34361,20 @@ ${prompt}`;
34288
34361
 
34289
34362
  // src/config/model-resolver.ts
34290
34363
  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"
34364
+ haiku: "haiku",
34365
+ sonnet: "sonnet",
34366
+ opus: "opus"
34294
34367
  };
34368
+ var SIMPLE_TIER_NAMES = new Set(["haiku", "sonnet", "opus"]);
34295
34369
  function isValidModelFormat(model) {
34370
+ if (SIMPLE_TIER_NAMES.has(model.toLowerCase())) {
34371
+ return true;
34372
+ }
34296
34373
  return /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+$/.test(model);
34297
34374
  }
34298
34375
  function validateModelFormat(model, context) {
34299
34376
  if (!isValidModelFormat(model)) {
34300
- warn(`[model-resolver] [${context}] Model "${model}" does not follow "provider/model-name" format`);
34377
+ warn(`[model-resolver] [${context}] Model "${model}" does not follow "provider/model-name" format. Configure model_mapping.tierDefaults in oh-my-opencode.json`);
34301
34378
  }
34302
34379
  }
34303
34380
 
@@ -38229,6 +38306,19 @@ function getAgentEmoji(agentName) {
38229
38306
  return emojiMap[agentName] || "\uD83E\uDD16";
38230
38307
  }
38231
38308
 
38309
+ // src/state/session-pause-state.ts
38310
+ var pauseStates = new Map;
38311
+ function isSessionPaused(sessionId) {
38312
+ return pauseStates.get(sessionId)?.isPaused ?? false;
38313
+ }
38314
+ function resumeSession(sessionId) {
38315
+ const state = pauseStates.get(sessionId);
38316
+ if (state?.isPaused) {
38317
+ log(`Session resumed`, { sessionId, wasPausedFor: Date.now() - (state.pausedAt ?? 0) });
38318
+ }
38319
+ pauseStates.delete(sessionId);
38320
+ }
38321
+
38232
38322
  // src/index.ts
38233
38323
  var OmoOmcsPlugin = async (ctx) => {
38234
38324
  const pluginConfig = loadConfig(ctx.directory);
@@ -38335,6 +38425,10 @@ var OmoOmcsPlugin = async (ctx) => {
38335
38425
  } else {
38336
38426
  systemPromptInjector.clearSkillInjection(input.sessionID);
38337
38427
  }
38428
+ if (isSessionPaused(input.sessionID)) {
38429
+ resumeSession(input.sessionID);
38430
+ log("[session] Auto-resumed on user prompt", { sessionID: input.sessionID });
38431
+ }
38338
38432
  await autopilot["chat.message"](input, output);
38339
38433
  await ultraqaLoop["chat.message"](input, output);
38340
38434
  },
@@ -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 {};
@@ -11,12 +11,17 @@ export interface BackgroundTask {
11
11
  startedAt: number;
12
12
  completedAt?: number;
13
13
  }
14
+ export interface ModelConfig {
15
+ providerID: string;
16
+ modelID: string;
17
+ }
14
18
  export interface BackgroundManager {
15
- createTask: (parentSessionID: string, description: string, prompt: string, agent: string) => Promise<BackgroundTask>;
19
+ createTask: (parentSessionID: string, description: string, prompt: string, agent: string, model?: ModelConfig) => Promise<BackgroundTask>;
16
20
  getTask: (taskId: string) => BackgroundTask | undefined;
17
21
  getTasksByParentSession: (sessionID: string) => BackgroundTask[];
18
22
  cancelTask: (taskId: string) => boolean;
19
23
  cancelAllTasks: (parentSessionID?: string) => number;
20
24
  waitForTask: (taskId: string, timeoutMs?: number) => Promise<BackgroundTask>;
25
+ getParentSessionModel: (parentSessionID: string) => Promise<ModelConfig | undefined>;
21
26
  }
22
27
  export declare function createBackgroundManager(ctx: PluginInput, config?: BackgroundTaskConfig): BackgroundManager;
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.3",
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": [