micode 0.8.6 → 0.9.0

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.
Files changed (42) hide show
  1. package/INSTALL_CLAUDE.md +53 -4
  2. package/LICENSE +21 -0
  3. package/README.md +66 -0
  4. package/dist/index.js +11004 -1830
  5. package/package.json +3 -2
  6. package/src/agents/brainstormer.ts +1 -1
  7. package/src/agents/commander.ts +18 -2
  8. package/src/agents/implementer.ts +16 -0
  9. package/src/agents/index.ts +27 -2
  10. package/src/agents/mindmodel/anti-pattern-detector.ts +95 -0
  11. package/src/agents/mindmodel/code-clusterer.ts +108 -0
  12. package/src/agents/mindmodel/constraint-reviewer.ts +84 -0
  13. package/src/agents/mindmodel/constraint-writer.ts +136 -0
  14. package/src/agents/mindmodel/convention-extractor.ts +102 -0
  15. package/src/agents/mindmodel/dependency-mapper.ts +85 -0
  16. package/src/agents/mindmodel/domain-extractor.ts +77 -0
  17. package/src/agents/mindmodel/example-extractor.ts +87 -0
  18. package/src/agents/mindmodel/index.ts +11 -0
  19. package/src/agents/mindmodel/orchestrator.ts +103 -0
  20. package/src/agents/mindmodel/pattern-discoverer.ts +77 -0
  21. package/src/agents/mindmodel/stack-detector.ts +62 -0
  22. package/src/agents/planner.ts +16 -2
  23. package/src/agents/reviewer.ts +20 -2
  24. package/src/config-loader.ts +158 -39
  25. package/src/hooks/auto-compact.ts +34 -5
  26. package/src/hooks/constraint-reviewer.ts +177 -0
  27. package/src/hooks/context-injector.ts +4 -2
  28. package/src/hooks/context-window-monitor.ts +10 -2
  29. package/src/hooks/fragment-injector.ts +181 -0
  30. package/src/hooks/mindmodel-injector.ts +170 -0
  31. package/src/index.ts +131 -8
  32. package/src/mindmodel/classifier.ts +36 -0
  33. package/src/mindmodel/formatter.ts +18 -0
  34. package/src/mindmodel/index.ts +18 -0
  35. package/src/mindmodel/loader.ts +66 -0
  36. package/src/mindmodel/review.ts +68 -0
  37. package/src/mindmodel/types.ts +87 -0
  38. package/src/tools/batch-read.ts +75 -0
  39. package/src/tools/mindmodel-lookup.ts +87 -0
  40. package/src/tools/spawn-agent.ts +134 -59
  41. package/src/utils/config.ts +23 -3
  42. package/src/utils/model-limits.ts +20 -4
@@ -13,45 +13,80 @@ export interface ProviderInfo {
13
13
  }
14
14
 
15
15
  /**
16
- * Load available models from opencode.json config file (synchronous)
17
- * Returns a Set of "provider/model" strings
16
+ * OpenCode config structure for reading default model and available models
18
17
  */
19
- export function loadAvailableModels(configDir?: string): Set<string> {
20
- const availableModels = new Set<string>();
18
+ interface OpencodeConfig {
19
+ model?: string;
20
+ provider?: Record<string, { models?: Record<string, unknown> }>;
21
+ }
22
+
23
+ /**
24
+ * Load opencode.json config file (synchronous)
25
+ * Returns the parsed config or null if unavailable
26
+ */
27
+ function loadOpencodeConfig(configDir?: string): OpencodeConfig | null {
21
28
  const baseDir = configDir ?? join(homedir(), ".config", "opencode");
22
29
 
23
30
  try {
24
31
  const configPath = join(baseDir, "opencode.json");
25
32
  const content = readFileSync(configPath, "utf-8");
26
- const config = JSON.parse(content) as { provider?: Record<string, { models?: Record<string, unknown> }> };
33
+ return JSON.parse(content) as OpencodeConfig;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
27
38
 
28
- if (config.provider) {
29
- for (const [providerId, providerConfig] of Object.entries(config.provider)) {
30
- if (providerConfig.models) {
31
- for (const modelId of Object.keys(providerConfig.models)) {
32
- availableModels.add(`${providerId}/${modelId}`);
33
- }
39
+ /**
40
+ * Load available models from opencode.json config file (synchronous)
41
+ * Returns a Set of "provider/model" strings
42
+ */
43
+ export function loadAvailableModels(configDir?: string): Set<string> {
44
+ const availableModels = new Set<string>();
45
+ const config = loadOpencodeConfig(configDir);
46
+
47
+ if (config?.provider) {
48
+ for (const [providerId, providerConfig] of Object.entries(config.provider)) {
49
+ if (providerConfig.models) {
50
+ for (const modelId of Object.keys(providerConfig.models)) {
51
+ availableModels.add(`${providerId}/${modelId}`);
34
52
  }
35
53
  }
36
54
  }
37
- } catch {
38
- // Config doesn't exist or can't be parsed - return empty set
39
55
  }
40
56
 
41
57
  return availableModels;
42
58
  }
43
59
 
60
+ /**
61
+ * Load the default model from opencode.json config file (synchronous)
62
+ * Returns the model string in "provider/model" format or null if not set
63
+ */
64
+ export function loadDefaultModel(configDir?: string): string | null {
65
+ const config = loadOpencodeConfig(configDir);
66
+ return config?.model ?? null;
67
+ }
68
+
44
69
  // Safe properties that users can override
45
70
  const SAFE_AGENT_PROPERTIES = ["model", "temperature", "maxTokens"] as const;
46
71
 
72
+ // Built-in OpenCode models that don't require validation (always available)
73
+ const BUILTIN_MODELS = new Set(["opencode/big-pickle"]);
74
+
47
75
  export interface AgentOverride {
48
76
  model?: string;
49
77
  temperature?: number;
50
78
  maxTokens?: number;
51
79
  }
52
80
 
81
+ export interface MicodeFeatures {
82
+ mindmodelInjection?: boolean;
83
+ }
84
+
53
85
  export interface MicodeConfig {
54
86
  agents?: Record<string, AgentOverride>;
87
+ features?: MicodeFeatures;
88
+ compactionThreshold?: number;
89
+ fragments?: Record<string, string[]>;
55
90
  }
56
91
 
57
92
  /**
@@ -67,7 +102,9 @@ export async function loadMicodeConfig(configDir?: string): Promise<MicodeConfig
67
102
  const content = await readFile(configPath, "utf-8");
68
103
  const parsed = JSON.parse(content) as Record<string, unknown>;
69
104
 
70
- // Sanitize the config - only allow safe properties
105
+ const result: MicodeConfig = {};
106
+
107
+ // Sanitize agents - only allow safe properties
71
108
  if (parsed.agents && typeof parsed.agents === "object") {
72
109
  const sanitizedAgents: Record<string, AgentOverride> = {};
73
110
 
@@ -86,67 +123,143 @@ export async function loadMicodeConfig(configDir?: string): Promise<MicodeConfig
86
123
  }
87
124
  }
88
125
 
89
- return { agents: sanitizedAgents };
126
+ result.agents = sanitizedAgents;
127
+ }
128
+
129
+ // Parse features
130
+ if (parsed.features && typeof parsed.features === "object") {
131
+ const features = parsed.features as Record<string, unknown>;
132
+ result.features = {
133
+ mindmodelInjection: features.mindmodelInjection === true,
134
+ };
135
+ }
136
+
137
+ // Parse compactionThreshold (must be number between 0 and 1)
138
+ if (typeof parsed.compactionThreshold === "number") {
139
+ const threshold = parsed.compactionThreshold;
140
+ if (threshold >= 0 && threshold <= 1) {
141
+ result.compactionThreshold = threshold;
142
+ }
143
+ }
144
+
145
+ // Parse fragments
146
+ if (parsed.fragments && typeof parsed.fragments === "object") {
147
+ const fragments = parsed.fragments as Record<string, unknown>;
148
+ const sanitizedFragments: Record<string, string[]> = {};
149
+
150
+ for (const [agentName, fragmentList] of Object.entries(fragments)) {
151
+ if (Array.isArray(fragmentList)) {
152
+ const validFragments = fragmentList.filter((f): f is string => typeof f === "string" && f.trim().length > 0);
153
+ if (validFragments.length > 0) {
154
+ sanitizedFragments[agentName] = validFragments;
155
+ }
156
+ }
157
+ }
158
+
159
+ result.fragments = sanitizedFragments;
90
160
  }
91
161
 
92
- return parsed as MicodeConfig;
162
+ return result;
93
163
  } catch {
94
164
  return null;
95
165
  }
96
166
  }
97
167
 
168
+ /**
169
+ * Load model context limits from opencode.json
170
+ * Returns a Map of "provider/model" -> context limit (tokens)
171
+ */
172
+ export function loadModelContextLimits(configDir?: string): Map<string, number> {
173
+ const limits = new Map<string, number>();
174
+ const baseDir = configDir ?? join(homedir(), ".config", "opencode");
175
+
176
+ try {
177
+ const configPath = join(baseDir, "opencode.json");
178
+ const content = readFileSync(configPath, "utf-8");
179
+ const config = JSON.parse(content) as {
180
+ provider?: Record<string, { models?: Record<string, { limit?: { context?: number } }> }>;
181
+ };
182
+
183
+ if (config.provider) {
184
+ for (const [providerId, providerConfig] of Object.entries(config.provider)) {
185
+ if (providerConfig.models) {
186
+ for (const [modelId, modelConfig] of Object.entries(providerConfig.models)) {
187
+ const contextLimit = modelConfig?.limit?.context;
188
+ if (typeof contextLimit === "number" && contextLimit > 0) {
189
+ limits.set(`${providerId}/${modelId}`, contextLimit);
190
+ }
191
+ }
192
+ }
193
+ }
194
+ }
195
+ } catch {
196
+ // Config doesn't exist or can't be parsed - return empty map
197
+ }
198
+
199
+ return limits;
200
+ }
201
+
98
202
  /**
99
203
  * Merge user config overrides into plugin agent configs
100
204
  * Model overrides are validated against available models from opencode.json
101
205
  * Invalid models are logged and skipped (agent uses opencode default)
206
+ *
207
+ * Model resolution priority:
208
+ * 1. Per-agent override in micode.json (highest)
209
+ * 2. Default model from opencode.json "model" field
210
+ * 3. Plugin default (hardcoded in agent definitions)
102
211
  */
103
212
  export function mergeAgentConfigs(
104
213
  pluginAgents: Record<string, AgentConfig>,
105
214
  userConfig: MicodeConfig | null,
106
215
  availableModels?: Set<string>,
216
+ defaultModel?: string | null,
107
217
  ): Record<string, AgentConfig> {
108
- if (!userConfig?.agents) {
109
- return pluginAgents;
110
- }
111
-
112
218
  const models = availableModels ?? loadAvailableModels();
113
219
  const shouldValidateModels = models.size > 0;
220
+ const opencodeDefaultModel = defaultModel ?? loadDefaultModel();
221
+
222
+ // Helper to validate a model string
223
+ const isValidModel = (model: string): boolean => {
224
+ if (BUILTIN_MODELS.has(model)) return true;
225
+ if (!shouldValidateModels) return true;
226
+ return models.has(model);
227
+ };
114
228
 
115
229
  const merged: Record<string, AgentConfig> = {};
116
230
 
117
231
  for (const [name, agentConfig] of Object.entries(pluginAgents)) {
118
- const userOverride = userConfig.agents[name];
232
+ const userOverride = userConfig?.agents?.[name];
119
233
 
234
+ // Start with the base agent config
235
+ let finalConfig: AgentConfig = { ...agentConfig };
236
+
237
+ // Apply opencode default model if available and valid (overrides plugin default)
238
+ if (opencodeDefaultModel && isValidModel(opencodeDefaultModel)) {
239
+ finalConfig = { ...finalConfig, model: opencodeDefaultModel };
240
+ }
241
+
242
+ // Apply user overrides from micode.json (highest priority)
120
243
  if (userOverride) {
121
- // Validate model if specified
122
244
  if (userOverride.model) {
123
- if (!shouldValidateModels || models.has(userOverride.model)) {
124
- // Model is valid (or validation unavailable) - apply all overrides
125
- merged[name] = {
126
- ...agentConfig,
127
- ...userOverride,
128
- };
245
+ if (isValidModel(userOverride.model)) {
246
+ // Model is valid - apply all overrides including model
247
+ finalConfig = { ...finalConfig, ...userOverride };
129
248
  } else {
130
249
  // Model is invalid - log warning and apply other overrides only
131
250
  console.warn(
132
251
  `[micode] Model "${userOverride.model}" for agent "${name}" is not available. Using opencode default.`,
133
252
  );
134
253
  const { model: _ignored, ...safeOverrides } = userOverride;
135
- merged[name] = {
136
- ...agentConfig,
137
- ...safeOverrides,
138
- };
254
+ finalConfig = { ...finalConfig, ...safeOverrides };
139
255
  }
140
256
  } else {
141
- // No model specified - apply all overrides
142
- merged[name] = {
143
- ...agentConfig,
144
- ...userOverride,
145
- };
257
+ // No model in override - apply other overrides (keep resolved model)
258
+ finalConfig = { ...finalConfig, ...userOverride };
146
259
  }
147
- } else {
148
- merged[name] = agentConfig;
149
260
  }
261
+
262
+ merged[name] = finalConfig;
150
263
  }
151
264
 
152
265
  return merged;
@@ -192,6 +305,12 @@ export function validateAgentModels(userConfig: MicodeConfig, providers: Provide
192
305
  continue;
193
306
  }
194
307
 
308
+ // Skip validation for built-in models
309
+ if (BUILTIN_MODELS.has(trimmedModel)) {
310
+ validatedAgents[agentName] = override;
311
+ continue;
312
+ }
313
+
195
314
  // Parse "provider/model" format
196
315
  const [providerID, ...rest] = trimmedModel.split("/");
197
316
  const modelID = rest.join("/");
@@ -7,6 +7,13 @@ import { config } from "../utils/config";
7
7
  import { extractErrorMessage } from "../utils/errors";
8
8
  import { getContextLimit } from "../utils/model-limits";
9
9
 
10
+ export interface AutoCompactConfig {
11
+ /** Compaction threshold (0-1), defaults to config.compaction.threshold */
12
+ compactionThreshold?: number;
13
+ /** Model context limits loaded from opencode.json */
14
+ modelContextLimits?: Map<string, number>;
15
+ }
16
+
10
17
  interface PendingCompaction {
11
18
  resolve: () => void;
12
19
  reject: (error: Error) => void;
@@ -19,7 +26,10 @@ interface AutoCompactState {
19
26
  pendingCompactions: Map<string, PendingCompaction>;
20
27
  }
21
28
 
22
- export function createAutoCompactHook(ctx: PluginInput) {
29
+ export function createAutoCompactHook(ctx: PluginInput, hookConfig?: AutoCompactConfig) {
30
+ const threshold = hookConfig?.compactionThreshold ?? config.compaction.threshold;
31
+ const modelLimits = hookConfig?.modelContextLimits;
32
+
23
33
  const state: AutoCompactState = {
24
34
  inProgress: new Set(),
25
35
  lastCompactTime: new Map(),
@@ -113,7 +123,7 @@ ${summaryText}
113
123
 
114
124
  try {
115
125
  const usedPercent = Math.round(usageRatio * 100);
116
- const thresholdPercent = Math.round(config.compaction.threshold * 100);
126
+ const thresholdPercent = Math.round(threshold * 100);
117
127
 
118
128
  await ctx.client.tui
119
129
  .showToast({
@@ -149,12 +159,31 @@ ${summaryText}
149
159
  .showToast({
150
160
  body: {
151
161
  title: "Compaction Complete",
152
- message: "Session summarized and ledger updated.",
162
+ message: "Session summarized. Continuing...",
153
163
  variant: "success",
154
164
  duration: config.timeouts.toastSuccessMs,
155
165
  },
156
166
  })
157
167
  .catch(() => {});
168
+
169
+ // Auto-continue after compaction - prompt the agent to resume work
170
+ await ctx.client.session
171
+ .prompt({
172
+ path: { id: sessionID },
173
+ body: {
174
+ parts: [
175
+ {
176
+ type: "text",
177
+ text: "Context was compacted. Continue from where you left off - check the 'In Progress' and 'Next Steps' sections in the summary above.",
178
+ },
179
+ ],
180
+ model: { providerID, modelID },
181
+ },
182
+ query: { directory: ctx.directory },
183
+ })
184
+ .catch(() => {
185
+ // If auto-continue fails, user can manually prompt
186
+ });
158
187
  } catch (e) {
159
188
  const errorMsg = extractErrorMessage(e);
160
189
  await ctx.client.tui
@@ -222,11 +251,11 @@ ${summaryText}
222
251
 
223
252
  const modelID = (info?.modelID as string) || "";
224
253
  const providerID = (info?.providerID as string) || "";
225
- const contextLimit = getContextLimit(modelID);
254
+ const contextLimit = getContextLimit(modelID, providerID, modelLimits);
226
255
  const usageRatio = totalUsed / contextLimit;
227
256
 
228
257
  // Trigger compaction if over threshold
229
- if (usageRatio >= config.compaction.threshold) {
258
+ if (usageRatio >= threshold) {
230
259
  triggerCompaction(sessionID, providerID, modelID, usageRatio);
231
260
  }
232
261
  }
@@ -0,0 +1,177 @@
1
+ // src/hooks/constraint-reviewer.ts
2
+ import type { PluginInput } from "@opencode-ai/plugin";
3
+
4
+ import {
5
+ formatViolationsForRetry,
6
+ formatViolationsForUser,
7
+ type LoadedMindmodel,
8
+ loadMindmodel,
9
+ parseReviewResponse,
10
+ type ReviewResult,
11
+ } from "../mindmodel";
12
+ import { config } from "../utils/config";
13
+ import { log } from "../utils/logger";
14
+
15
+ type ReviewFn = (prompt: string) => Promise<string>;
16
+
17
+ interface ReviewState {
18
+ /** Retry count per file path */
19
+ retryCountByFile: Map<string, number>;
20
+ /** Override active for remainder of turn */
21
+ overrideActive: boolean;
22
+ }
23
+
24
+ export function createConstraintReviewerHook(ctx: PluginInput, reviewFn: ReviewFn) {
25
+ let cachedMindmodel: LoadedMindmodel | null | undefined;
26
+ const sessionState = new Map<string, ReviewState>();
27
+
28
+ async function getMindmodel(): Promise<LoadedMindmodel | null> {
29
+ if (cachedMindmodel === undefined) {
30
+ cachedMindmodel = await loadMindmodel(ctx.directory);
31
+ }
32
+ return cachedMindmodel;
33
+ }
34
+
35
+ function getSessionState(sessionID: string): ReviewState {
36
+ if (!sessionState.has(sessionID)) {
37
+ sessionState.set(sessionID, {
38
+ retryCountByFile: new Map(),
39
+ overrideActive: false,
40
+ });
41
+ }
42
+ return sessionState.get(sessionID)!;
43
+ }
44
+
45
+ function cleanupSession(sessionID: string): void {
46
+ sessionState.delete(sessionID);
47
+ }
48
+
49
+ return {
50
+ "tool.execute.after": async (
51
+ input: { tool: string; sessionID: string; args?: Record<string, unknown> },
52
+ output: { output?: string },
53
+ ) => {
54
+ // Only review Write and Edit operations
55
+ if (!["Write", "Edit"].includes(input.tool)) return;
56
+ if (!config.mindmodel.reviewEnabled) return;
57
+
58
+ const mindmodel = await getMindmodel();
59
+ if (!mindmodel) return;
60
+
61
+ const state = getSessionState(input.sessionID);
62
+
63
+ // Skip if override is active
64
+ if (state.overrideActive) {
65
+ state.overrideActive = false;
66
+ return;
67
+ }
68
+
69
+ const filePath = input.args?.file_path as string | undefined;
70
+ if (!filePath) return;
71
+
72
+ try {
73
+ // Build review prompt
74
+ const reviewPrompt = buildReviewPrompt(output.output || "", filePath, mindmodel);
75
+
76
+ // Call reviewer
77
+ const reviewResponse = await reviewFn(reviewPrompt);
78
+ const result = parseReviewResponse(reviewResponse);
79
+
80
+ if (result.status === "PASS") {
81
+ // Reset retry count for this file on success
82
+ state.retryCountByFile.delete(filePath);
83
+ return;
84
+ }
85
+
86
+ // Handle violations - track retry count per file
87
+ const currentRetryCount = state.retryCountByFile.get(filePath) || 0;
88
+
89
+ if (currentRetryCount < config.mindmodel.reviewMaxRetries) {
90
+ // Trigger retry by modifying output
91
+ state.retryCountByFile.set(filePath, currentRetryCount + 1);
92
+ const violationsText = formatViolationsForRetry(result.violations);
93
+ output.output = `${output.output}\n\n<constraint-violations>\n${violationsText}\n</constraint-violations>`;
94
+ } else {
95
+ // Max retries reached - block
96
+ state.retryCountByFile.delete(filePath);
97
+ const userMessage = formatViolationsForUser(result.violations);
98
+ throw new ConstraintViolationError(userMessage, result);
99
+ }
100
+ } catch (error) {
101
+ if (error instanceof ConstraintViolationError) {
102
+ throw error;
103
+ }
104
+ // Log but don't block on review failures
105
+ log.warn("mindmodel", `Review failed: ${error instanceof Error ? error.message : "unknown"}`);
106
+ }
107
+ },
108
+
109
+ "chat.message": async (input: { sessionID: string }, output: { parts: Array<{ type: string; text?: string }> }) => {
110
+ // Check for override command
111
+ const text = output.parts
112
+ .filter((p) => p.type === "text" && p.text)
113
+ .map((p) => p.text)
114
+ .join(" ");
115
+
116
+ const overrideMatch = text.match(/^override:\s*(.+)$/im);
117
+ if (overrideMatch) {
118
+ const state = getSessionState(input.sessionID);
119
+ state.overrideActive = true;
120
+
121
+ // Log the override
122
+ const reason = overrideMatch[1].trim();
123
+ await logOverride(ctx.directory, reason);
124
+
125
+ log.info("mindmodel", `Override activated: ${reason}`);
126
+ }
127
+ },
128
+
129
+ /** Cleanup session state on session deletion to prevent memory leaks */
130
+ cleanupSession,
131
+ };
132
+ }
133
+
134
+ function buildReviewPrompt(code: string, filePath: string, mindmodel: LoadedMindmodel): string {
135
+ // For now, include all constraints - selective loading can be added later
136
+ const constraintSummary = mindmodel.manifest.categories.map((c) => `- ${c.path}: ${c.description}`).join("\n");
137
+
138
+ return `Review this generated code against project constraints.
139
+
140
+ File: ${filePath}
141
+
142
+ Code:
143
+ \`\`\`
144
+ ${code}
145
+ \`\`\`
146
+
147
+ Available constraints:
148
+ ${constraintSummary}
149
+
150
+ Return JSON with status "PASS" or "BLOCKED" and any violations found.`;
151
+ }
152
+
153
+ async function logOverride(projectDir: string, reason: string): Promise<void> {
154
+ const { appendFile, mkdir } = await import("node:fs/promises");
155
+ const { join } = await import("node:path");
156
+
157
+ const logPath = join(projectDir, ".mindmodel", config.mindmodel.overrideLogFile);
158
+ const timestamp = new Date().toISOString();
159
+ const entry = `${timestamp} | override | reason: "${reason}"\n`;
160
+
161
+ try {
162
+ await mkdir(join(projectDir, ".mindmodel"), { recursive: true });
163
+ await appendFile(logPath, entry);
164
+ } catch {
165
+ // Ignore logging failures
166
+ }
167
+ }
168
+
169
+ export class ConstraintViolationError extends Error {
170
+ constructor(
171
+ message: string,
172
+ public readonly result: ReviewResult,
173
+ ) {
174
+ super(message);
175
+ this.name = "ConstraintViolationError";
176
+ }
177
+ }
@@ -1,6 +1,8 @@
1
- import type { PluginInput } from "@opencode-ai/plugin";
2
1
  import { readFile } from "node:fs/promises";
3
- import { join, dirname, resolve } from "node:path";
2
+ import { dirname, join, resolve } from "node:path";
3
+
4
+ import type { PluginInput } from "@opencode-ai/plugin";
5
+
4
6
  import { config } from "../utils/config";
5
7
 
6
8
  // Tools that trigger directory-aware context injection
@@ -1,13 +1,20 @@
1
1
  import type { PluginInput } from "@opencode-ai/plugin";
2
+
2
3
  import { config } from "../utils/config";
3
4
  import { getContextLimit } from "../utils/model-limits";
4
5
 
6
+ export interface ContextWindowMonitorConfig {
7
+ /** Model context limits loaded from opencode.json */
8
+ modelContextLimits?: Map<string, number>;
9
+ }
10
+
5
11
  interface MonitorState {
6
12
  lastWarningTime: Map<string, number>;
7
13
  lastUsageRatio: Map<string, number>;
8
14
  }
9
15
 
10
- export function createContextWindowMonitorHook(ctx: PluginInput) {
16
+ export function createContextWindowMonitorHook(ctx: PluginInput, hookConfig?: ContextWindowMonitorConfig) {
17
+ const modelLimits = hookConfig?.modelContextLimits;
11
18
  const state: MonitorState = {
12
19
  lastWarningTime: new Map(),
13
20
  lastUsageRatio: new Map(),
@@ -69,7 +76,8 @@ export function createContextWindowMonitorHook(ctx: PluginInput) {
69
76
  const totalUsed = inputTokens + cacheRead;
70
77
 
71
78
  const modelID = (info.modelID as string) || "";
72
- const contextLimit = getContextLimit(modelID);
79
+ const providerID = (info.providerID as string) || "";
80
+ const contextLimit = getContextLimit(modelID, providerID, modelLimits);
73
81
  const usageRatio = totalUsed / contextLimit;
74
82
 
75
83
  state.lastUsageRatio.set(sessionID, usageRatio);