micode 0.8.4 → 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.
- package/dist/index.js +21020 -0
- package/package.json +6 -5
- package/src/agents/artifact-searcher.ts +0 -1
- package/src/agents/bootstrapper.ts +164 -0
- package/src/agents/brainstormer.ts +140 -33
- package/src/agents/codebase-analyzer.ts +0 -1
- package/src/agents/codebase-locator.ts +0 -1
- package/src/agents/commander.ts +99 -10
- package/src/agents/executor.ts +18 -1
- package/src/agents/implementer.ts +83 -6
- package/src/agents/index.ts +29 -19
- package/src/agents/ledger-creator.ts +0 -1
- package/src/agents/octto.ts +132 -0
- package/src/agents/pattern-finder.ts +0 -1
- package/src/agents/planner.ts +139 -49
- package/src/agents/probe.ts +152 -0
- package/src/agents/project-initializer.ts +0 -1
- package/src/agents/reviewer.ts +75 -5
- package/src/config-loader.test.ts +226 -0
- package/src/config-loader.ts +132 -6
- package/src/hooks/artifact-auto-index.ts +2 -1
- package/src/hooks/auto-compact.ts +14 -21
- package/src/hooks/context-injector.ts +6 -13
- package/src/hooks/context-window-monitor.ts +8 -13
- package/src/hooks/ledger-loader.ts +4 -6
- package/src/hooks/token-aware-truncation.ts +11 -17
- package/src/index.ts +54 -22
- package/src/indexing/milestone-artifact-classifier.ts +26 -0
- package/src/indexing/milestone-artifact-ingest.ts +42 -0
- package/src/octto/constants.ts +20 -0
- package/src/octto/session/browser.ts +32 -0
- package/src/octto/session/index.ts +25 -0
- package/src/octto/session/server.ts +89 -0
- package/src/octto/session/sessions.ts +383 -0
- package/src/octto/session/types.ts +305 -0
- package/src/octto/session/utils.ts +25 -0
- package/src/octto/session/waiter.ts +139 -0
- package/src/octto/state/index.ts +5 -0
- package/src/octto/state/persistence.ts +65 -0
- package/src/octto/state/store.ts +161 -0
- package/src/octto/state/types.ts +51 -0
- package/src/octto/types.ts +376 -0
- package/src/octto/ui/bundle.ts +1650 -0
- package/src/octto/ui/index.ts +2 -0
- package/src/tools/artifact-index/index.ts +152 -3
- package/src/tools/artifact-index/schema.sql +21 -0
- package/src/tools/milestone-artifact-search.ts +48 -0
- package/src/tools/octto/brainstorm.ts +332 -0
- package/src/tools/octto/extractor.ts +95 -0
- package/src/tools/octto/factory.ts +89 -0
- package/src/tools/octto/formatters.ts +63 -0
- package/src/tools/octto/index.ts +27 -0
- package/src/tools/octto/processor.ts +165 -0
- package/src/tools/octto/questions.ts +508 -0
- package/src/tools/octto/responses.ts +135 -0
- package/src/tools/octto/session.ts +114 -0
- package/src/tools/octto/types.ts +21 -0
- package/src/tools/octto/utils.ts +4 -0
- package/src/tools/pty/manager.ts +13 -7
- package/src/tools/spawn-agent.ts +1 -3
- package/src/utils/config.ts +123 -0
- package/src/utils/errors.ts +57 -0
- package/src/utils/logger.ts +50 -0
|
@@ -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
|
-
|
|
7
|
-
const COMPACT_THRESHOLD = 0.5;
|
|
8
|
-
|
|
9
|
-
const LEDGER_DIR = "thoughts/ledgers";
|
|
4
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
|
|
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,
|
|
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,
|
|
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
|
-
},
|
|
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 <
|
|
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(
|
|
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:
|
|
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:
|
|
154
|
+
duration: config.timeouts.toastSuccessMs,
|
|
162
155
|
},
|
|
163
156
|
})
|
|
164
157
|
.catch(() => {});
|
|
165
158
|
} catch (e) {
|
|
166
|
-
const errorMsg =
|
|
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:
|
|
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 >=
|
|
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 <
|
|
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
|
|
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
|
|
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 >
|
|
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?.
|
|
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 <
|
|
19
|
+
if (usageRatio < config.contextWindow.warningThreshold) {
|
|
25
20
|
return ""; // No message needed
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
if (usageRatio <
|
|
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 >=
|
|
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 >=
|
|
78
|
+
if (usageRatio >= config.contextWindow.warningThreshold) {
|
|
84
79
|
const lastWarning = state.lastWarningTime.get(sessionID) || 0;
|
|
85
|
-
if (Date.now() - lastWarning >
|
|
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 >=
|
|
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:
|
|
92
|
+
duration: config.timeouts.toastWarningMs,
|
|
98
93
|
},
|
|
99
94
|
})
|
|
100
95
|
.catch(() => {});
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
3
3
|
import { readFile, readdir } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
const LEDGER_DIR = "thoughts/ledgers";
|
|
7
|
-
const LEDGER_PREFIX = "CONTINUITY_";
|
|
5
|
+
import { config } from "../utils/config";
|
|
8
6
|
|
|
9
7
|
export interface LedgerInfo {
|
|
10
8
|
sessionName: string;
|
|
@@ -13,11 +11,11 @@ export interface LedgerInfo {
|
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
export async function findCurrentLedger(directory: string): Promise<LedgerInfo | null> {
|
|
16
|
-
const ledgerDir = join(directory,
|
|
14
|
+
const ledgerDir = join(directory, config.paths.ledgerDir);
|
|
17
15
|
|
|
18
16
|
try {
|
|
19
17
|
const files = await readdir(ledgerDir);
|
|
20
|
-
const ledgerFiles = files.filter((f) => f.startsWith(
|
|
18
|
+
const ledgerFiles = files.filter((f) => f.startsWith(config.paths.ledgerPrefix) && f.endsWith(".md"));
|
|
21
19
|
|
|
22
20
|
if (ledgerFiles.length === 0) return null;
|
|
23
21
|
|
|
@@ -40,7 +38,7 @@ export async function findCurrentLedger(directory: string): Promise<LedgerInfo |
|
|
|
40
38
|
|
|
41
39
|
const filePath = join(ledgerDir, latestFile);
|
|
42
40
|
const content = await readFile(filePath, "utf-8");
|
|
43
|
-
const sessionName = latestFile.replace(
|
|
41
|
+
const sessionName = latestFile.replace(config.paths.ledgerPrefix, "").replace(".md", "");
|
|
44
42
|
|
|
45
43
|
return { sessionName, filePath, content };
|
|
46
44
|
} catch {
|
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import { config } from "../utils/config";
|
|
2
3
|
|
|
3
4
|
// Tools that benefit from truncation
|
|
4
5
|
const TRUNCATABLE_TOOLS = ["grep", "Grep", "glob", "Glob", "ast_grep_search"];
|
|
5
6
|
|
|
6
|
-
// Token estimation (conservative: 4 chars = 1 token)
|
|
7
|
-
const CHARS_PER_TOKEN = 4;
|
|
8
|
-
const DEFAULT_CONTEXT_LIMIT = 200_000;
|
|
9
|
-
const DEFAULT_MAX_OUTPUT_TOKENS = 50_000;
|
|
10
|
-
const SAFETY_MARGIN = 0.5; // Keep 50% headroom
|
|
11
|
-
const PRESERVE_HEADER_LINES = 3;
|
|
12
|
-
|
|
13
7
|
function estimateTokens(text: string): number {
|
|
14
|
-
return Math.ceil(text.length /
|
|
8
|
+
return Math.ceil(text.length / config.tokens.charsPerToken);
|
|
15
9
|
}
|
|
16
10
|
|
|
17
11
|
function truncateToTokenLimit(
|
|
18
12
|
output: string,
|
|
19
13
|
maxTokens: number,
|
|
20
|
-
preserveLines: number =
|
|
14
|
+
preserveLines: number = config.tokens.preserveHeaderLines,
|
|
21
15
|
): string {
|
|
22
16
|
const currentTokens = estimateTokens(output);
|
|
23
17
|
|
|
@@ -85,7 +79,7 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
|
|
|
85
79
|
|
|
86
80
|
const messages = (resp as { data?: unknown[] }).data;
|
|
87
81
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
88
|
-
return { used: 0, limit:
|
|
82
|
+
return { used: 0, limit: config.tokens.defaultContextLimit };
|
|
89
83
|
}
|
|
90
84
|
|
|
91
85
|
// Find last assistant message with usage info
|
|
@@ -96,7 +90,7 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
|
|
|
96
90
|
}) as Record<string, unknown> | undefined;
|
|
97
91
|
|
|
98
92
|
if (!lastAssistant) {
|
|
99
|
-
return { used: 0, limit:
|
|
93
|
+
return { used: 0, limit: config.tokens.defaultContextLimit };
|
|
100
94
|
}
|
|
101
95
|
|
|
102
96
|
const info = lastAssistant.info as Record<string, unknown> | undefined;
|
|
@@ -107,25 +101,25 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
|
|
|
107
101
|
const used = inputTokens + cacheRead;
|
|
108
102
|
|
|
109
103
|
// Get model limit (simplified - use default for now)
|
|
110
|
-
const limit =
|
|
104
|
+
const limit = config.tokens.defaultContextLimit;
|
|
111
105
|
|
|
112
106
|
const result = { used, limit };
|
|
113
107
|
state.sessionTokenUsage.set(sessionID, result);
|
|
114
108
|
return result;
|
|
115
109
|
} catch {
|
|
116
|
-
return state.sessionTokenUsage.get(sessionID) || { used: 0, limit:
|
|
110
|
+
return state.sessionTokenUsage.get(sessionID) || { used: 0, limit: config.tokens.defaultContextLimit };
|
|
117
111
|
}
|
|
118
112
|
}
|
|
119
113
|
|
|
120
114
|
function calculateMaxOutputTokens(used: number, limit: number): number {
|
|
121
115
|
const remaining = limit - used;
|
|
122
|
-
const available = Math.floor(remaining *
|
|
116
|
+
const available = Math.floor(remaining * config.tokens.safetyMargin);
|
|
123
117
|
|
|
124
118
|
if (available <= 0) {
|
|
125
119
|
return 0;
|
|
126
120
|
}
|
|
127
121
|
|
|
128
|
-
return Math.min(available,
|
|
122
|
+
return Math.min(available, config.tokens.defaultMaxOutputTokens);
|
|
129
123
|
}
|
|
130
124
|
|
|
131
125
|
return {
|
|
@@ -180,8 +174,8 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
|
|
|
180
174
|
} catch {
|
|
181
175
|
// On error, apply static truncation as fallback
|
|
182
176
|
const currentTokens = estimateTokens(output.output);
|
|
183
|
-
if (currentTokens >
|
|
184
|
-
output.output = truncateToTokenLimit(output.output,
|
|
177
|
+
if (currentTokens > config.tokens.defaultMaxOutputTokens) {
|
|
178
|
+
output.output = truncateToTokenLimit(output.output, config.tokens.defaultMaxOutputTokens);
|
|
185
179
|
}
|
|
186
180
|
}
|
|
187
181
|
},
|
package/src/index.ts
CHANGED
|
@@ -3,30 +3,28 @@ import type { McpLocalConfig } from "@opencode-ai/sdk";
|
|
|
3
3
|
|
|
4
4
|
// Agents
|
|
5
5
|
import { agents, PRIMARY_AGENT_NAME } from "./agents";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import { btca_ask, checkBtcaAvailable } from "./tools/btca";
|
|
10
|
-
import { look_at } from "./tools/look-at";
|
|
11
|
-
import { artifact_search } from "./tools/artifact-search";
|
|
12
|
-
import { createSpawnAgentTool } from "./tools/spawn-agent";
|
|
13
|
-
|
|
6
|
+
// Config loader
|
|
7
|
+
import { loadMicodeConfig, mergeAgentConfigs } from "./config-loader";
|
|
8
|
+
import { createArtifactAutoIndexHook } from "./hooks/artifact-auto-index";
|
|
14
9
|
// Hooks
|
|
15
10
|
import { createAutoCompactHook } from "./hooks/auto-compact";
|
|
11
|
+
import { createCommentCheckerHook } from "./hooks/comment-checker";
|
|
16
12
|
import { createContextInjectorHook } from "./hooks/context-injector";
|
|
17
|
-
import { createSessionRecoveryHook } from "./hooks/session-recovery";
|
|
18
|
-
import { createTokenAwareTruncationHook } from "./hooks/token-aware-truncation";
|
|
19
13
|
import { createContextWindowMonitorHook } from "./hooks/context-window-monitor";
|
|
20
|
-
import { createCommentCheckerHook } from "./hooks/comment-checker";
|
|
21
|
-
import { createLedgerLoaderHook } from "./hooks/ledger-loader";
|
|
22
|
-
import { createArtifactAutoIndexHook } from "./hooks/artifact-auto-index";
|
|
23
14
|
import { createFileOpsTrackerHook, getFileOps } from "./hooks/file-ops-tracker";
|
|
24
|
-
|
|
15
|
+
import { createLedgerLoaderHook } from "./hooks/ledger-loader";
|
|
16
|
+
import { createSessionRecoveryHook } from "./hooks/session-recovery";
|
|
17
|
+
import { createTokenAwareTruncationHook } from "./hooks/token-aware-truncation";
|
|
18
|
+
import { artifact_search } from "./tools/artifact-search";
|
|
19
|
+
// Tools
|
|
20
|
+
import { ast_grep_replace, ast_grep_search, checkAstGrepAvailable } from "./tools/ast-grep";
|
|
21
|
+
import { btca_ask, checkBtcaAvailable } from "./tools/btca";
|
|
22
|
+
import { look_at } from "./tools/look-at";
|
|
23
|
+
import { milestone_artifact_search } from "./tools/milestone-artifact-search";
|
|
24
|
+
import { createOcttoTools, createSessionStore } from "./tools/octto";
|
|
25
25
|
// PTY System
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
// Config loader
|
|
29
|
-
import { loadMicodeConfig, mergeAgentConfigs } from "./config-loader";
|
|
26
|
+
import { createPtyTools, PTYManager } from "./tools/pty";
|
|
27
|
+
import { createSpawnAgentTool } from "./tools/spawn-agent";
|
|
30
28
|
|
|
31
29
|
// Think mode: detect keywords and enable extended thinking
|
|
32
30
|
const THINK_KEYWORDS = [
|
|
@@ -75,7 +73,7 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
75
73
|
console.warn(`[micode] ${btcaStatus.message}`);
|
|
76
74
|
}
|
|
77
75
|
|
|
78
|
-
// Load user config for model overrides
|
|
76
|
+
// Load user config for temperature/maxTokens overrides (model overrides not supported)
|
|
79
77
|
const userConfig = await loadMicodeConfig();
|
|
80
78
|
|
|
81
79
|
// Think mode state per session
|
|
@@ -99,6 +97,28 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
99
97
|
// Spawn agent tool (for subagents to spawn other subagents)
|
|
100
98
|
const spawn_agent = createSpawnAgentTool(ctx);
|
|
101
99
|
|
|
100
|
+
// Octto (browser-based brainstorming) tools
|
|
101
|
+
const octtoSessionStore = createSessionStore();
|
|
102
|
+
|
|
103
|
+
// Track octto sessions per opencode session for cleanup
|
|
104
|
+
const octtoSessionsMap = new Map<string, Set<string>>();
|
|
105
|
+
|
|
106
|
+
const octtoTools = createOcttoTools(octtoSessionStore, ctx.client, {
|
|
107
|
+
onCreated: (parentSessionId, octtoSessionId) => {
|
|
108
|
+
const sessions = octtoSessionsMap.get(parentSessionId) ?? new Set<string>();
|
|
109
|
+
sessions.add(octtoSessionId);
|
|
110
|
+
octtoSessionsMap.set(parentSessionId, sessions);
|
|
111
|
+
},
|
|
112
|
+
onEnded: (parentSessionId, octtoSessionId) => {
|
|
113
|
+
const sessions = octtoSessionsMap.get(parentSessionId);
|
|
114
|
+
if (!sessions) return;
|
|
115
|
+
sessions.delete(octtoSessionId);
|
|
116
|
+
if (sessions.size === 0) {
|
|
117
|
+
octtoSessionsMap.delete(parentSessionId);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
102
122
|
return {
|
|
103
123
|
// Tools
|
|
104
124
|
tool: {
|
|
@@ -107,8 +127,10 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
107
127
|
btca_ask,
|
|
108
128
|
look_at,
|
|
109
129
|
artifact_search,
|
|
130
|
+
milestone_artifact_search,
|
|
110
131
|
spawn_agent,
|
|
111
132
|
...ptyTools,
|
|
133
|
+
...octtoTools,
|
|
112
134
|
},
|
|
113
135
|
|
|
114
136
|
config: async (config) => {
|
|
@@ -292,12 +314,22 @@ IMPORTANT:
|
|
|
292
314
|
},
|
|
293
315
|
|
|
294
316
|
event: async ({ event }) => {
|
|
295
|
-
// Session cleanup (think mode + PTY)
|
|
317
|
+
// Session cleanup (think mode + PTY + octto)
|
|
296
318
|
if (event.type === "session.deleted") {
|
|
297
319
|
const props = event.properties as { info?: { id?: string } } | undefined;
|
|
298
320
|
if (props?.info?.id) {
|
|
299
|
-
|
|
300
|
-
|
|
321
|
+
const sessionId = props.info.id;
|
|
322
|
+
thinkModeState.delete(sessionId);
|
|
323
|
+
ptyManager.cleanupBySession(sessionId);
|
|
324
|
+
|
|
325
|
+
// Cleanup octto sessions
|
|
326
|
+
const octtoSessions = octtoSessionsMap.get(sessionId);
|
|
327
|
+
if (octtoSessions) {
|
|
328
|
+
for (const octtoSessionId of octtoSessions) {
|
|
329
|
+
await octtoSessionStore.endSession(octtoSessionId).catch(() => {});
|
|
330
|
+
}
|
|
331
|
+
octtoSessionsMap.delete(sessionId);
|
|
332
|
+
}
|
|
301
333
|
}
|
|
302
334
|
}
|
|
303
335
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const MILESTONE_ARTIFACT_TYPES = {
|
|
2
|
+
FEATURE: "feature",
|
|
3
|
+
DECISION: "decision",
|
|
4
|
+
SESSION: "session",
|
|
5
|
+
} as const;
|
|
6
|
+
|
|
7
|
+
export type MilestoneArtifactType = (typeof MILESTONE_ARTIFACT_TYPES)[keyof typeof MILESTONE_ARTIFACT_TYPES];
|
|
8
|
+
|
|
9
|
+
const FEATURE_HINTS = ["requirement", "implementation", "capability", "scope", "spec"];
|
|
10
|
+
const DECISION_HINTS = ["decision", "decided", "trade-off", "rationale", "chosen"];
|
|
11
|
+
const SESSION_HINTS = ["meeting", "status", "discussion", "notes", "update"];
|
|
12
|
+
|
|
13
|
+
const matchesAny = (content: string, hints: string[]) => hints.some((hint) => content.includes(hint));
|
|
14
|
+
|
|
15
|
+
export function classifyMilestoneArtifact(content: string): MilestoneArtifactType {
|
|
16
|
+
const normalized = content.toLowerCase();
|
|
17
|
+
const isFeature = matchesAny(normalized, FEATURE_HINTS);
|
|
18
|
+
const isDecision = matchesAny(normalized, DECISION_HINTS);
|
|
19
|
+
const isSession = matchesAny(normalized, SESSION_HINTS);
|
|
20
|
+
|
|
21
|
+
if (isFeature) return MILESTONE_ARTIFACT_TYPES.FEATURE;
|
|
22
|
+
if (isDecision) return MILESTONE_ARTIFACT_TYPES.DECISION;
|
|
23
|
+
if (isSession) return MILESTONE_ARTIFACT_TYPES.SESSION;
|
|
24
|
+
|
|
25
|
+
return MILESTONE_ARTIFACT_TYPES.SESSION;
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ArtifactIndex, getArtifactIndex } from "../tools/artifact-index";
|
|
2
|
+
import { log } from "../utils/logger";
|
|
3
|
+
import {
|
|
4
|
+
classifyMilestoneArtifact,
|
|
5
|
+
MILESTONE_ARTIFACT_TYPES,
|
|
6
|
+
type MilestoneArtifactType,
|
|
7
|
+
} from "./milestone-artifact-classifier";
|
|
8
|
+
|
|
9
|
+
export interface MilestoneArtifactInput {
|
|
10
|
+
id: string;
|
|
11
|
+
milestoneId: string;
|
|
12
|
+
sourceSessionId?: string;
|
|
13
|
+
createdAt?: string;
|
|
14
|
+
tags?: string[];
|
|
15
|
+
payload: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function ingestMilestoneArtifact(
|
|
19
|
+
input: MilestoneArtifactInput,
|
|
20
|
+
index?: ArtifactIndex,
|
|
21
|
+
classifier: (content: string) => MilestoneArtifactType = classifyMilestoneArtifact,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const artifactIndex = index ?? (await getArtifactIndex());
|
|
24
|
+
let artifactType: MilestoneArtifactType;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
artifactType = classifier(input.payload);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
log.error("milestone-ingest", "Failed to classify milestone artifact, defaulting to session", error);
|
|
30
|
+
artifactType = MILESTONE_ARTIFACT_TYPES.SESSION;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await artifactIndex.indexMilestoneArtifact({
|
|
34
|
+
id: input.id,
|
|
35
|
+
milestoneId: input.milestoneId,
|
|
36
|
+
artifactType,
|
|
37
|
+
sourceSessionId: input.sourceSessionId,
|
|
38
|
+
createdAt: input.createdAt,
|
|
39
|
+
tags: input.tags,
|
|
40
|
+
payload: input.payload,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/octto/constants.ts
|
|
2
|
+
// Re-exports from centralized config for backward compatibility
|
|
3
|
+
// Single source of truth is in src/utils/config.ts
|
|
4
|
+
|
|
5
|
+
import { config } from "../utils/config";
|
|
6
|
+
|
|
7
|
+
/** Default timeout for waiting for user answers (5 minutes) */
|
|
8
|
+
export const DEFAULT_ANSWER_TIMEOUT_MS = config.octto.answerTimeoutMs;
|
|
9
|
+
|
|
10
|
+
/** Default maximum number of follow-up questions per branch */
|
|
11
|
+
export const DEFAULT_MAX_QUESTIONS = config.octto.maxQuestions;
|
|
12
|
+
|
|
13
|
+
/** Default timeout for brainstorm review (10 minutes) */
|
|
14
|
+
export const DEFAULT_REVIEW_TIMEOUT_MS = config.octto.reviewTimeoutMs;
|
|
15
|
+
|
|
16
|
+
/** Maximum number of brainstorm iterations */
|
|
17
|
+
export const MAX_ITERATIONS = config.octto.maxIterations;
|
|
18
|
+
|
|
19
|
+
/** Directory for persisting brainstorm state files */
|
|
20
|
+
export const STATE_DIR = config.octto.stateDir;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/octto/session/browser.ts
|
|
2
|
+
// Cross-platform browser opener
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Opens the default browser to the specified URL.
|
|
6
|
+
* Detects platform and uses appropriate command.
|
|
7
|
+
*/
|
|
8
|
+
export async function openBrowser(url: string): Promise<void> {
|
|
9
|
+
const platform = process.platform;
|
|
10
|
+
|
|
11
|
+
let command: string[];
|
|
12
|
+
|
|
13
|
+
switch (platform) {
|
|
14
|
+
case "darwin":
|
|
15
|
+
command = ["open", url];
|
|
16
|
+
break;
|
|
17
|
+
case "win32":
|
|
18
|
+
command = ["cmd", "/c", "start", url];
|
|
19
|
+
break;
|
|
20
|
+
default:
|
|
21
|
+
// Linux and others
|
|
22
|
+
command = ["xdg-open", url];
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const proc = Bun.spawn(command, {
|
|
27
|
+
stdout: "ignore",
|
|
28
|
+
stderr: "ignore",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await proc.exited;
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/octto/session/index.ts
|
|
2
|
+
export type { SessionStore, SessionStoreOptions } from "./sessions";
|
|
3
|
+
export { createSessionStore } from "./sessions";
|
|
4
|
+
export type {
|
|
5
|
+
Answer,
|
|
6
|
+
AskCodeAnswer,
|
|
7
|
+
AskFileAnswer,
|
|
8
|
+
AskImageAnswer,
|
|
9
|
+
AskTextAnswer,
|
|
10
|
+
BaseConfig,
|
|
11
|
+
ConfirmAnswer,
|
|
12
|
+
EmojiReactAnswer,
|
|
13
|
+
PickManyAnswer,
|
|
14
|
+
PickOneAnswer,
|
|
15
|
+
QuestionAnswers,
|
|
16
|
+
QuestionConfig,
|
|
17
|
+
QuestionType,
|
|
18
|
+
RankAnswer,
|
|
19
|
+
RateAnswer,
|
|
20
|
+
ReviewAnswer,
|
|
21
|
+
ShowOptionsAnswer,
|
|
22
|
+
SliderAnswer,
|
|
23
|
+
ThumbsAnswer,
|
|
24
|
+
} from "./types";
|
|
25
|
+
export { QUESTION_TYPES, QUESTIONS, STATUSES, WS_MESSAGES } from "./types";
|