micode 0.8.3 → 0.8.5

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 (64) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +21020 -0
  3. package/package.json +10 -6
  4. package/src/agents/artifact-searcher.ts +0 -1
  5. package/src/agents/bootstrapper.ts +164 -0
  6. package/src/agents/brainstormer.ts +140 -33
  7. package/src/agents/codebase-analyzer.ts +0 -1
  8. package/src/agents/codebase-locator.ts +0 -1
  9. package/src/agents/commander.ts +99 -10
  10. package/src/agents/executor.ts +18 -1
  11. package/src/agents/implementer.ts +83 -6
  12. package/src/agents/index.ts +29 -19
  13. package/src/agents/ledger-creator.ts +0 -1
  14. package/src/agents/octto.ts +132 -0
  15. package/src/agents/pattern-finder.ts +0 -1
  16. package/src/agents/planner.ts +139 -49
  17. package/src/agents/probe.ts +152 -0
  18. package/src/agents/project-initializer.ts +0 -1
  19. package/src/agents/reviewer.ts +75 -5
  20. package/src/config-loader.test.ts +226 -0
  21. package/src/config-loader.ts +132 -6
  22. package/src/hooks/artifact-auto-index.ts +2 -1
  23. package/src/hooks/auto-compact.ts +14 -21
  24. package/src/hooks/context-injector.ts +6 -13
  25. package/src/hooks/context-window-monitor.ts +8 -13
  26. package/src/hooks/ledger-loader.ts +4 -6
  27. package/src/hooks/token-aware-truncation.ts +11 -17
  28. package/src/index.ts +54 -22
  29. package/src/indexing/milestone-artifact-classifier.ts +26 -0
  30. package/src/indexing/milestone-artifact-ingest.ts +42 -0
  31. package/src/octto/constants.ts +20 -0
  32. package/src/octto/session/browser.ts +32 -0
  33. package/src/octto/session/index.ts +25 -0
  34. package/src/octto/session/server.ts +89 -0
  35. package/src/octto/session/sessions.ts +383 -0
  36. package/src/octto/session/types.ts +305 -0
  37. package/src/octto/session/utils.ts +25 -0
  38. package/src/octto/session/waiter.ts +139 -0
  39. package/src/octto/state/index.ts +5 -0
  40. package/src/octto/state/persistence.ts +65 -0
  41. package/src/octto/state/store.ts +161 -0
  42. package/src/octto/state/types.ts +51 -0
  43. package/src/octto/types.ts +376 -0
  44. package/src/octto/ui/bundle.ts +1650 -0
  45. package/src/octto/ui/index.ts +2 -0
  46. package/src/tools/artifact-index/index.ts +152 -3
  47. package/src/tools/artifact-index/schema.sql +21 -0
  48. package/src/tools/milestone-artifact-search.ts +48 -0
  49. package/src/tools/octto/brainstorm.ts +332 -0
  50. package/src/tools/octto/extractor.ts +95 -0
  51. package/src/tools/octto/factory.ts +89 -0
  52. package/src/tools/octto/formatters.ts +63 -0
  53. package/src/tools/octto/index.ts +27 -0
  54. package/src/tools/octto/processor.ts +165 -0
  55. package/src/tools/octto/questions.ts +508 -0
  56. package/src/tools/octto/responses.ts +135 -0
  57. package/src/tools/octto/session.ts +114 -0
  58. package/src/tools/octto/types.ts +21 -0
  59. package/src/tools/octto/utils.ts +4 -0
  60. package/src/tools/pty/manager.ts +13 -7
  61. package/src/tools/spawn-agent.ts +1 -3
  62. package/src/utils/config.ts +123 -0
  63. package/src/utils/errors.ts +57 -0
  64. package/src/utils/logger.ts +50 -0
@@ -0,0 +1,226 @@
1
+ // src/config-loader.test.ts
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { type MicodeConfig, type ProviderInfo, validateAgentModels } from "./config-loader";
5
+
6
+ // Helper to create a minimal ProviderInfo for testing
7
+ function createProvider(id: string, modelIds: string[]): ProviderInfo {
8
+ const models: Record<string, unknown> = {};
9
+ for (const modelId of modelIds) {
10
+ models[modelId] = { id: modelId };
11
+ }
12
+ return { id, models };
13
+ }
14
+
15
+ describe("validateAgentModels", () => {
16
+ test("returns config unchanged when all models are valid", () => {
17
+ const userConfig: MicodeConfig = {
18
+ agents: {
19
+ commander: { model: "openai/gpt-4" },
20
+ brainstormer: { model: "anthropic/claude-3" },
21
+ },
22
+ };
23
+
24
+ const providers: ProviderInfo[] = [
25
+ createProvider("openai", ["gpt-4", "gpt-3.5"]),
26
+ createProvider("anthropic", ["claude-3", "claude-2"]),
27
+ ];
28
+
29
+ const result = validateAgentModels(userConfig, providers);
30
+
31
+ expect(result.agents?.commander?.model).toBe("openai/gpt-4");
32
+ expect(result.agents?.brainstormer?.model).toBe("anthropic/claude-3");
33
+ });
34
+
35
+ test("removes model override when provider does not exist", () => {
36
+ const userConfig: MicodeConfig = {
37
+ agents: {
38
+ commander: { model: "nonexistent/gpt-4" },
39
+ },
40
+ };
41
+
42
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
43
+
44
+ const result = validateAgentModels(userConfig, providers);
45
+
46
+ // Model should be removed, falling back to default
47
+ expect(result.agents?.commander?.model).toBeUndefined();
48
+ });
49
+
50
+ test("removes model override when model does not exist in provider", () => {
51
+ const userConfig: MicodeConfig = {
52
+ agents: {
53
+ commander: { model: "openai/nonexistent-model" },
54
+ },
55
+ };
56
+
57
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4", "gpt-3.5"])];
58
+
59
+ const result = validateAgentModels(userConfig, providers);
60
+
61
+ // Model should be removed, falling back to default
62
+ expect(result.agents?.commander?.model).toBeUndefined();
63
+ });
64
+
65
+ test("preserves other properties when model is invalid", () => {
66
+ const userConfig: MicodeConfig = {
67
+ agents: {
68
+ commander: {
69
+ model: "nonexistent/model",
70
+ temperature: 0.7,
71
+ maxTokens: 4000,
72
+ },
73
+ },
74
+ };
75
+
76
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
77
+
78
+ const result = validateAgentModels(userConfig, providers);
79
+
80
+ // Model removed but other properties preserved
81
+ expect(result.agents?.commander?.model).toBeUndefined();
82
+ expect(result.agents?.commander?.temperature).toBe(0.7);
83
+ expect(result.agents?.commander?.maxTokens).toBe(4000);
84
+ });
85
+
86
+ test("handles config with no agents", () => {
87
+ const userConfig: MicodeConfig = {};
88
+
89
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
90
+
91
+ const result = validateAgentModels(userConfig, providers);
92
+
93
+ expect(result).toEqual({});
94
+ });
95
+
96
+ test("handles agent override with no model specified", () => {
97
+ const userConfig: MicodeConfig = {
98
+ agents: {
99
+ commander: { temperature: 0.5 },
100
+ },
101
+ };
102
+
103
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
104
+
105
+ const result = validateAgentModels(userConfig, providers);
106
+
107
+ // No model to validate, config unchanged
108
+ expect(result.agents?.commander?.temperature).toBe(0.5);
109
+ expect(result.agents?.commander?.model).toBeUndefined();
110
+ });
111
+
112
+ test("handles empty providers list", () => {
113
+ const userConfig: MicodeConfig = {
114
+ agents: {
115
+ commander: { model: "openai/gpt-4" },
116
+ },
117
+ };
118
+
119
+ const providers: ProviderInfo[] = [];
120
+
121
+ const result = validateAgentModels(userConfig, providers);
122
+
123
+ // No providers available, config should remain unchanged
124
+ expect(result).toEqual(userConfig);
125
+ });
126
+
127
+ test("handles providers with no models", () => {
128
+ const userConfig: MicodeConfig = {
129
+ agents: {
130
+ commander: { model: "openai/gpt-4" },
131
+ },
132
+ };
133
+
134
+ const providers: ProviderInfo[] = [{ id: "openai", models: {} }];
135
+
136
+ const result = validateAgentModels(userConfig, providers);
137
+
138
+ // No provider models available, config should remain unchanged
139
+ expect(result).toEqual(userConfig);
140
+ });
141
+
142
+ test("validates multiple agents with mixed valid/invalid models", () => {
143
+ const userConfig: MicodeConfig = {
144
+ agents: {
145
+ commander: { model: "openai/gpt-4" }, // valid
146
+ brainstormer: { model: "fake/model" }, // invalid provider
147
+ planner: { model: "openai/fake-model" }, // invalid model
148
+ reviewer: { model: "anthropic/claude-3" }, // valid
149
+ },
150
+ };
151
+
152
+ const providers: ProviderInfo[] = [
153
+ createProvider("openai", ["gpt-4", "gpt-3.5"]),
154
+ createProvider("anthropic", ["claude-3"]),
155
+ ];
156
+
157
+ const result = validateAgentModels(userConfig, providers);
158
+
159
+ expect(result.agents?.commander?.model).toBe("openai/gpt-4");
160
+ expect(result.agents?.brainstormer?.model).toBeUndefined();
161
+ expect(result.agents?.planner?.model).toBeUndefined();
162
+ expect(result.agents?.reviewer?.model).toBe("anthropic/claude-3");
163
+ });
164
+
165
+ test("removes empty string model", () => {
166
+ const userConfig: MicodeConfig = {
167
+ agents: {
168
+ commander: { model: "", temperature: 0.5 },
169
+ },
170
+ };
171
+
172
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
173
+
174
+ const result = validateAgentModels(userConfig, providers);
175
+
176
+ // Empty string model should be removed as invalid
177
+ expect(result.agents?.commander?.model).toBeUndefined();
178
+ expect(result.agents?.commander?.temperature).toBe(0.5);
179
+ });
180
+
181
+ test("removes model string without slash (malformed)", () => {
182
+ const userConfig: MicodeConfig = {
183
+ agents: {
184
+ commander: { model: "gpt-4-no-provider" },
185
+ },
186
+ };
187
+
188
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
189
+
190
+ const result = validateAgentModels(userConfig, providers);
191
+
192
+ // Malformed model (no slash) should be removed
193
+ expect(result.agents?.commander?.model).toBeUndefined();
194
+ });
195
+
196
+ test("handles model with multiple slashes in model ID", () => {
197
+ const userConfig: MicodeConfig = {
198
+ agents: {
199
+ commander: { model: "openai/gpt-4/turbo" },
200
+ },
201
+ };
202
+
203
+ // Model ID is "gpt-4/turbo" (contains slash)
204
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4/turbo"])];
205
+
206
+ const result = validateAgentModels(userConfig, providers);
207
+
208
+ // Should be valid - "gpt-4/turbo" is the full model ID
209
+ expect(result.agents?.commander?.model).toBe("openai/gpt-4/turbo");
210
+ });
211
+
212
+ test("returns consistent shape when all agents have invalid models", () => {
213
+ const userConfig: MicodeConfig = {
214
+ agents: {
215
+ commander: { model: "invalid/model" },
216
+ },
217
+ };
218
+
219
+ const providers: ProviderInfo[] = [createProvider("openai", ["gpt-4"])];
220
+
221
+ const result = validateAgentModels(userConfig, providers);
222
+
223
+ // Should return { agents: {} } for consistency, not {}
224
+ expect(result).toEqual({ agents: {} });
225
+ });
226
+ });
@@ -1,9 +1,46 @@
1
1
  // src/config-loader.ts
2
+ import { readFileSync } from "node:fs";
2
3
  import { readFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+
5
7
  import type { AgentConfig } from "@opencode-ai/sdk";
6
8
 
9
+ // Minimal type for provider validation - only what we need
10
+ export interface ProviderInfo {
11
+ id: string;
12
+ models: Record<string, unknown>;
13
+ }
14
+
15
+ /**
16
+ * Load available models from opencode.json config file (synchronous)
17
+ * Returns a Set of "provider/model" strings
18
+ */
19
+ export function loadAvailableModels(configDir?: string): Set<string> {
20
+ const availableModels = new Set<string>();
21
+ const baseDir = configDir ?? join(homedir(), ".config", "opencode");
22
+
23
+ try {
24
+ const configPath = join(baseDir, "opencode.json");
25
+ const content = readFileSync(configPath, "utf-8");
26
+ const config = JSON.parse(content) as { provider?: Record<string, { models?: Record<string, unknown> }> };
27
+
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
+ }
34
+ }
35
+ }
36
+ }
37
+ } catch {
38
+ // Config doesn't exist or can't be parsed - return empty set
39
+ }
40
+
41
+ return availableModels;
42
+ }
43
+
7
44
  // Safe properties that users can override
8
45
  const SAFE_AGENT_PROPERTIES = ["model", "temperature", "maxTokens"] as const;
9
46
 
@@ -60,26 +97,53 @@ export async function loadMicodeConfig(configDir?: string): Promise<MicodeConfig
60
97
 
61
98
  /**
62
99
  * Merge user config overrides into plugin agent configs
63
- * User overrides take precedence for safe properties only
100
+ * Model overrides are validated against available models from opencode.json
101
+ * Invalid models are logged and skipped (agent uses opencode default)
64
102
  */
65
103
  export function mergeAgentConfigs(
66
104
  pluginAgents: Record<string, AgentConfig>,
67
105
  userConfig: MicodeConfig | null,
106
+ availableModels?: Set<string>,
68
107
  ): Record<string, AgentConfig> {
69
108
  if (!userConfig?.agents) {
70
109
  return pluginAgents;
71
110
  }
72
111
 
112
+ const models = availableModels ?? loadAvailableModels();
113
+ const shouldValidateModels = models.size > 0;
114
+
73
115
  const merged: Record<string, AgentConfig> = {};
74
116
 
75
117
  for (const [name, agentConfig] of Object.entries(pluginAgents)) {
76
118
  const userOverride = userConfig.agents[name];
77
119
 
78
120
  if (userOverride) {
79
- merged[name] = {
80
- ...agentConfig,
81
- ...userOverride,
82
- };
121
+ // Validate model if specified
122
+ 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
+ };
129
+ } else {
130
+ // Model is invalid - log warning and apply other overrides only
131
+ console.warn(
132
+ `[micode] Model "${userOverride.model}" for agent "${name}" is not available. Using opencode default.`,
133
+ );
134
+ const { model: _ignored, ...safeOverrides } = userOverride;
135
+ merged[name] = {
136
+ ...agentConfig,
137
+ ...safeOverrides,
138
+ };
139
+ }
140
+ } else {
141
+ // No model specified - apply all overrides
142
+ merged[name] = {
143
+ ...agentConfig,
144
+ ...userOverride,
145
+ };
146
+ }
83
147
  } else {
84
148
  merged[name] = agentConfig;
85
149
  }
@@ -87,3 +151,65 @@ export function mergeAgentConfigs(
87
151
 
88
152
  return merged;
89
153
  }
154
+
155
+ /**
156
+ * Validate that configured models exist in available providers
157
+ * Removes invalid model overrides and logs warnings
158
+ */
159
+ export function validateAgentModels(userConfig: MicodeConfig, providers: ProviderInfo[]): MicodeConfig {
160
+ if (!userConfig.agents) {
161
+ return userConfig;
162
+ }
163
+
164
+ const hasAnyModels = providers.some((provider) => Object.keys(provider.models).length > 0);
165
+ if (!hasAnyModels) {
166
+ return userConfig;
167
+ }
168
+
169
+ // Build lookup map for providers and their models
170
+ const providerMap = new Map<string, Set<string>>();
171
+ for (const provider of providers) {
172
+ providerMap.set(provider.id, new Set(Object.keys(provider.models)));
173
+ }
174
+
175
+ const validatedAgents: Record<string, AgentOverride> = {};
176
+
177
+ for (const [agentName, override] of Object.entries(userConfig.agents)) {
178
+ // No model specified - keep other properties as-is
179
+ if (override.model === undefined) {
180
+ validatedAgents[agentName] = override;
181
+ continue;
182
+ }
183
+
184
+ // Empty or whitespace-only model - treat as invalid
185
+ const trimmedModel = override.model.trim();
186
+ if (!trimmedModel) {
187
+ const { model: _removed, ...otherProps } = override;
188
+ console.warn(`[micode] Empty model for agent "${agentName}". Using default model.`);
189
+ if (Object.keys(otherProps).length > 0) {
190
+ validatedAgents[agentName] = otherProps;
191
+ }
192
+ continue;
193
+ }
194
+
195
+ // Parse "provider/model" format
196
+ const [providerID, ...rest] = trimmedModel.split("/");
197
+ const modelID = rest.join("/");
198
+
199
+ const providerModels = providerMap.get(providerID);
200
+ const isValid = providerModels?.has(modelID) ?? false;
201
+
202
+ if (isValid) {
203
+ validatedAgents[agentName] = override;
204
+ } else {
205
+ // Remove invalid model but keep other properties
206
+ const { model: _removed, ...otherProps } = override;
207
+ console.warn(`[micode] Model "${override.model}" not found for agent "${agentName}". Using default model.`);
208
+ if (Object.keys(otherProps).length > 0) {
209
+ validatedAgents[agentName] = otherProps;
210
+ }
211
+ }
212
+ }
213
+
214
+ return { agents: validatedAgents };
215
+ }
@@ -4,6 +4,7 @@
4
4
  import type { PluginInput } from "@opencode-ai/plugin";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { getArtifactIndex } from "../tools/artifact-index";
7
+ import { log } from "../utils/logger";
7
8
 
8
9
  const LEDGER_PATH_PATTERN = /thoughts\/ledgers\/CONTINUITY_(.+)\.md$/;
9
10
  const PLAN_PATH_PATTERN = /thoughts\/shared\/plans\/(.+)\.md$/;
@@ -102,7 +103,7 @@ export function createArtifactAutoIndexHook(_ctx: PluginInput) {
102
103
  }
103
104
  } catch (e) {
104
105
  // Silent failure - don't interrupt user flow
105
- console.error(`[artifact-auto-index] Error indexing ${filePath}:`, e);
106
+ log.error("artifact-auto-index", `Error indexing ${filePath}`, e);
106
107
  }
107
108
  },
108
109
  };
@@ -1,15 +1,11 @@
1
- import type { PluginInput } from "@opencode-ai/plugin";
2
- import { getContextLimit } from "../utils/model-limits";
3
1
  import { mkdir, writeFile } from "node:fs/promises";
4
2
  import { join } from "node:path";
5
3
 
6
- // Compact when this percentage of context is used
7
- const COMPACT_THRESHOLD = 0.5;
8
-
9
- const LEDGER_DIR = "thoughts/ledgers";
4
+ import type { PluginInput } from "@opencode-ai/plugin";
10
5
 
11
- // Timeout for waiting for compaction to complete (2 minutes)
12
- const COMPACTION_TIMEOUT_MS = 120_000;
6
+ import { config } from "../utils/config";
7
+ import { extractErrorMessage } from "../utils/errors";
8
+ import { getContextLimit } from "../utils/model-limits";
13
9
 
14
10
  interface PendingCompaction {
15
11
  resolve: () => void;
@@ -23,9 +19,6 @@ interface AutoCompactState {
23
19
  pendingCompactions: Map<string, PendingCompaction>;
24
20
  }
25
21
 
26
- // Cooldown between compaction attempts (prevent rapid re-triggering)
27
- const COMPACT_COOLDOWN_MS = 30_000; // 30 seconds
28
-
29
22
  export function createAutoCompactHook(ctx: PluginInput) {
30
23
  const state: AutoCompactState = {
31
24
  inProgress: new Set(),
@@ -65,13 +58,13 @@ export function createAutoCompactHook(ctx: PluginInput) {
65
58
  if (!summaryText.trim()) return;
66
59
 
67
60
  // Create ledger directory if needed
68
- const ledgerDir = join(ctx.directory, LEDGER_DIR);
61
+ const ledgerDir = join(ctx.directory, config.paths.ledgerDir);
69
62
  await mkdir(ledgerDir, { recursive: true });
70
63
 
71
64
  // Write ledger file - summary is already structured (Factory.ai/pi-mono format)
72
65
  const timestamp = new Date().toISOString();
73
66
  const sessionName = sessionID.slice(0, 8); // Use first 8 chars of session ID
74
- const ledgerPath = join(ledgerDir, `CONTINUITY_${sessionName}.md`);
67
+ const ledgerPath = join(ledgerDir, `${config.paths.ledgerPrefix}${sessionName}.md`);
75
68
 
76
69
  // Add metadata header, then the structured summary as-is
77
70
  const ledgerContent = `---
@@ -94,7 +87,7 @@ ${summaryText}
94
87
  const timeoutId = setTimeout(() => {
95
88
  state.pendingCompactions.delete(sessionID);
96
89
  reject(new Error("Compaction timed out"));
97
- }, COMPACTION_TIMEOUT_MS);
90
+ }, config.compaction.timeoutMs);
98
91
 
99
92
  state.pendingCompactions.set(sessionID, { resolve, reject, timeoutId });
100
93
  });
@@ -112,7 +105,7 @@ ${summaryText}
112
105
 
113
106
  // Check cooldown
114
107
  const lastCompact = state.lastCompactTime.get(sessionID) || 0;
115
- if (Date.now() - lastCompact < COMPACT_COOLDOWN_MS) {
108
+ if (Date.now() - lastCompact < config.compaction.cooldownMs) {
116
109
  return;
117
110
  }
118
111
 
@@ -120,7 +113,7 @@ ${summaryText}
120
113
 
121
114
  try {
122
115
  const usedPercent = Math.round(usageRatio * 100);
123
- const thresholdPercent = Math.round(COMPACT_THRESHOLD * 100);
116
+ const thresholdPercent = Math.round(config.compaction.threshold * 100);
124
117
 
125
118
  await ctx.client.tui
126
119
  .showToast({
@@ -128,7 +121,7 @@ ${summaryText}
128
121
  title: "Auto Compacting",
129
122
  message: `Context at ${usedPercent}% (threshold: ${thresholdPercent}%). Summarizing...`,
130
123
  variant: "warning",
131
- duration: 3000,
124
+ duration: config.timeouts.toastWarningMs,
132
125
  },
133
126
  })
134
127
  .catch(() => {});
@@ -158,19 +151,19 @@ ${summaryText}
158
151
  title: "Compaction Complete",
159
152
  message: "Session summarized and ledger updated.",
160
153
  variant: "success",
161
- duration: 3000,
154
+ duration: config.timeouts.toastSuccessMs,
162
155
  },
163
156
  })
164
157
  .catch(() => {});
165
158
  } catch (e) {
166
- const errorMsg = e instanceof Error ? e.message : String(e);
159
+ const errorMsg = extractErrorMessage(e);
167
160
  await ctx.client.tui
168
161
  .showToast({
169
162
  body: {
170
163
  title: "Compaction Failed",
171
164
  message: errorMsg.slice(0, 100),
172
165
  variant: "error",
173
- duration: 5000,
166
+ duration: config.timeouts.toastErrorMs,
174
167
  },
175
168
  })
176
169
  .catch(() => {});
@@ -233,7 +226,7 @@ ${summaryText}
233
226
  const usageRatio = totalUsed / contextLimit;
234
227
 
235
228
  // Trigger compaction if over threshold
236
- if (usageRatio >= COMPACT_THRESHOLD) {
229
+ if (usageRatio >= config.compaction.threshold) {
237
230
  triggerCompaction(sessionID, providerID, modelID, usageRatio);
238
231
  }
239
232
  }
@@ -1,12 +1,7 @@
1
1
  import type { PluginInput } from "@opencode-ai/plugin";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join, dirname, resolve } from "node:path";
4
-
5
- // Files to inject at project root level (AGENTS.md and CLAUDE.md handled by OpenCode natively)
6
- const ROOT_CONTEXT_FILES = ["ARCHITECTURE.md", "CODE_STYLE.md", "README.md"] as const;
7
-
8
- // Files to collect when walking up directories (AGENTS.md handled by OpenCode natively)
9
- const DIRECTORY_CONTEXT_FILES = ["README.md"] as const;
4
+ import { config } from "../utils/config";
10
5
 
11
6
  // Tools that trigger directory-aware context injection
12
7
  const FILE_ACCESS_TOOLS = ["Read", "read", "Edit", "edit"];
@@ -18,8 +13,6 @@ interface ContextCache {
18
13
  lastRootCheck: number;
19
14
  }
20
15
 
21
- const CACHE_TTL = 30_000; // 30 seconds
22
-
23
16
  export function createContextInjectorHook(ctx: PluginInput) {
24
17
  const cache: ContextCache = {
25
18
  rootContent: new Map(),
@@ -30,14 +23,14 @@ export function createContextInjectorHook(ctx: PluginInput) {
30
23
  async function loadRootContextFiles(): Promise<Map<string, string>> {
31
24
  const now = Date.now();
32
25
 
33
- if (now - cache.lastRootCheck < CACHE_TTL && cache.rootContent.size > 0) {
26
+ if (now - cache.lastRootCheck < config.limits.contextCacheTtlMs && cache.rootContent.size > 0) {
34
27
  return cache.rootContent;
35
28
  }
36
29
 
37
30
  cache.rootContent.clear();
38
31
  cache.lastRootCheck = now;
39
32
 
40
- for (const filename of ROOT_CONTEXT_FILES) {
33
+ for (const filename of config.paths.rootContextFiles) {
41
34
  try {
42
35
  const filepath = join(ctx.directory, filename);
43
36
  const content = await readFile(filepath, "utf-8");
@@ -67,7 +60,7 @@ export function createContextInjectorHook(ctx: PluginInput) {
67
60
 
68
61
  // Walk up from file directory to project root
69
62
  while (currentDir.startsWith(projectRoot) || currentDir === projectRoot) {
70
- for (const filename of DIRECTORY_CONTEXT_FILES) {
63
+ for (const filename of config.paths.dirContextFiles) {
71
64
  const contextPath = join(currentDir, filename);
72
65
  const relPath = currentDir.replace(projectRoot, "").replace(/^\//, "") || ".";
73
66
  const key = `${relPath}/${filename}`;
@@ -98,7 +91,7 @@ export function createContextInjectorHook(ctx: PluginInput) {
98
91
  cache.directoryContent.set(cacheKey, collected);
99
92
 
100
93
  // Limit cache size
101
- if (cache.directoryContent.size > 100) {
94
+ if (cache.directoryContent.size > config.limits.contextCacheMaxSize) {
102
95
  const firstKey = cache.directoryContent.keys().next().value;
103
96
  if (firstKey) cache.directoryContent.delete(firstKey);
104
97
  }
@@ -143,7 +136,7 @@ export function createContextInjectorHook(ctx: PluginInput) {
143
136
  ) => {
144
137
  if (!FILE_ACCESS_TOOLS.includes(input.tool)) return;
145
138
 
146
- const filePath = input.args?.file_path as string | undefined;
139
+ const filePath = input.args?.filePath as string | undefined;
147
140
  if (!filePath) return;
148
141
 
149
142
  try {
@@ -1,17 +1,12 @@
1
1
  import type { PluginInput } from "@opencode-ai/plugin";
2
+ import { config } from "../utils/config";
2
3
  import { getContextLimit } from "../utils/model-limits";
3
4
 
4
- // Thresholds for context window warnings
5
- const WARNING_THRESHOLD = 0.7; // 70% - remind there's still room
6
- const CRITICAL_THRESHOLD = 0.85; // 85% - getting tight
7
-
8
5
  interface MonitorState {
9
6
  lastWarningTime: Map<string, number>;
10
7
  lastUsageRatio: Map<string, number>;
11
8
  }
12
9
 
13
- const WARNING_COOLDOWN_MS = 120_000; // 2 minutes between warnings
14
-
15
10
  export function createContextWindowMonitorHook(ctx: PluginInput) {
16
11
  const state: MonitorState = {
17
12
  lastWarningTime: new Map(),
@@ -21,11 +16,11 @@ export function createContextWindowMonitorHook(ctx: PluginInput) {
21
16
  function getEncouragementMessage(usageRatio: number): string {
22
17
  const remaining = Math.round((1 - usageRatio) * 100);
23
18
 
24
- if (usageRatio < WARNING_THRESHOLD) {
19
+ if (usageRatio < config.contextWindow.warningThreshold) {
25
20
  return ""; // No message needed
26
21
  }
27
22
 
28
- if (usageRatio < CRITICAL_THRESHOLD) {
23
+ if (usageRatio < config.contextWindow.criticalThreshold) {
29
24
  return `Context: ${remaining}% remaining. Plenty of room - don't rush.`;
30
25
  }
31
26
 
@@ -40,7 +35,7 @@ export function createContextWindowMonitorHook(ctx: PluginInput) {
40
35
  ) => {
41
36
  const usageRatio = state.lastUsageRatio.get(input.sessionID);
42
37
 
43
- if (usageRatio && usageRatio >= WARNING_THRESHOLD) {
38
+ if (usageRatio && usageRatio >= config.contextWindow.warningThreshold) {
44
39
  const message = getEncouragementMessage(usageRatio);
45
40
  if (message && output.system) {
46
41
  output.system = `${output.system}\n\n<context-status>${message}</context-status>`;
@@ -80,13 +75,13 @@ export function createContextWindowMonitorHook(ctx: PluginInput) {
80
75
  state.lastUsageRatio.set(sessionID, usageRatio);
81
76
 
82
77
  // Show toast warning if threshold crossed
83
- if (usageRatio >= WARNING_THRESHOLD) {
78
+ if (usageRatio >= config.contextWindow.warningThreshold) {
84
79
  const lastWarning = state.lastWarningTime.get(sessionID) || 0;
85
- if (Date.now() - lastWarning > WARNING_COOLDOWN_MS) {
80
+ if (Date.now() - lastWarning > config.contextWindow.warningCooldownMs) {
86
81
  state.lastWarningTime.set(sessionID, Date.now());
87
82
 
88
83
  const remaining = Math.round((1 - usageRatio) * 100);
89
- const variant = usageRatio >= CRITICAL_THRESHOLD ? "warning" : "info";
84
+ const variant = usageRatio >= config.contextWindow.criticalThreshold ? "warning" : "info";
90
85
 
91
86
  await ctx.client.tui
92
87
  .showToast({
@@ -94,7 +89,7 @@ export function createContextWindowMonitorHook(ctx: PluginInput) {
94
89
  title: "Context Window",
95
90
  message: `${remaining}% remaining (${Math.round(totalUsed / 1000)}K / ${Math.round(contextLimit / 1000)}K tokens)`,
96
91
  variant,
97
- duration: 4000,
92
+ duration: config.timeouts.toastWarningMs,
98
93
  },
99
94
  })
100
95
  .catch(() => {});