micode 0.7.0 → 0.7.1
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/package.json +7 -13
- package/src/agents/artifact-searcher.ts +46 -0
- package/src/agents/brainstormer.ts +145 -0
- package/src/agents/codebase-analyzer.ts +75 -0
- package/src/agents/codebase-locator.ts +71 -0
- package/src/agents/commander.ts +138 -0
- package/src/agents/executor.ts +215 -0
- package/src/agents/implementer.ts +99 -0
- package/src/agents/index.ts +44 -0
- package/src/agents/ledger-creator.ts +113 -0
- package/src/agents/pattern-finder.ts +70 -0
- package/src/agents/planner.ts +230 -0
- package/src/agents/project-initializer.ts +264 -0
- package/src/agents/reviewer.ts +102 -0
- package/src/config-loader.ts +89 -0
- package/src/hooks/artifact-auto-index.ts +111 -0
- package/src/hooks/auto-clear-ledger.ts +230 -0
- package/src/hooks/auto-compact.ts +241 -0
- package/src/hooks/comment-checker.ts +120 -0
- package/src/hooks/context-injector.ts +163 -0
- package/src/hooks/context-window-monitor.ts +106 -0
- package/src/hooks/file-ops-tracker.ts +96 -0
- package/src/hooks/ledger-loader.ts +78 -0
- package/src/hooks/preemptive-compaction.ts +183 -0
- package/src/hooks/session-recovery.ts +258 -0
- package/src/hooks/token-aware-truncation.ts +189 -0
- package/src/index.ts +258 -0
- package/src/tools/artifact-index/index.ts +269 -0
- package/src/tools/artifact-index/schema.sql +44 -0
- package/src/tools/artifact-search.ts +49 -0
- package/src/tools/ast-grep/index.ts +189 -0
- package/src/tools/background-task/manager.ts +374 -0
- package/src/tools/background-task/tools.ts +145 -0
- package/src/tools/background-task/types.ts +68 -0
- package/src/tools/btca/index.ts +82 -0
- package/src/tools/look-at.ts +210 -0
- package/src/tools/pty/buffer.ts +49 -0
- package/src/tools/pty/index.ts +34 -0
- package/src/tools/pty/manager.ts +159 -0
- package/src/tools/pty/tools/kill.ts +68 -0
- package/src/tools/pty/tools/list.ts +55 -0
- package/src/tools/pty/tools/read.ts +152 -0
- package/src/tools/pty/tools/spawn.ts +78 -0
- package/src/tools/pty/tools/write.ts +97 -0
- package/src/tools/pty/types.ts +62 -0
- package/src/utils/model-limits.ts +36 -0
- package/dist/agents/artifact-searcher.d.ts +0 -2
- package/dist/agents/brainstormer.d.ts +0 -2
- package/dist/agents/codebase-analyzer.d.ts +0 -2
- package/dist/agents/codebase-locator.d.ts +0 -2
- package/dist/agents/commander.d.ts +0 -3
- package/dist/agents/executor.d.ts +0 -2
- package/dist/agents/implementer.d.ts +0 -2
- package/dist/agents/index.d.ts +0 -15
- package/dist/agents/ledger-creator.d.ts +0 -2
- package/dist/agents/pattern-finder.d.ts +0 -2
- package/dist/agents/planner.d.ts +0 -2
- package/dist/agents/project-initializer.d.ts +0 -2
- package/dist/agents/reviewer.d.ts +0 -2
- package/dist/config-loader.d.ts +0 -20
- package/dist/hooks/artifact-auto-index.d.ts +0 -19
- package/dist/hooks/auto-clear-ledger.d.ts +0 -11
- package/dist/hooks/auto-compact.d.ts +0 -9
- package/dist/hooks/comment-checker.d.ts +0 -9
- package/dist/hooks/context-injector.d.ts +0 -15
- package/dist/hooks/context-window-monitor.d.ts +0 -15
- package/dist/hooks/file-ops-tracker.d.ts +0 -26
- package/dist/hooks/ledger-loader.d.ts +0 -16
- package/dist/hooks/preemptive-compaction.d.ts +0 -9
- package/dist/hooks/session-recovery.d.ts +0 -9
- package/dist/hooks/token-aware-truncation.d.ts +0 -15
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -17089
- package/dist/tools/artifact-index/index.d.ts +0 -38
- package/dist/tools/artifact-search.d.ts +0 -17
- package/dist/tools/ast-grep/index.d.ts +0 -88
- package/dist/tools/background-task/manager.d.ts +0 -27
- package/dist/tools/background-task/tools.d.ts +0 -41
- package/dist/tools/background-task/types.d.ts +0 -53
- package/dist/tools/btca/index.d.ts +0 -19
- package/dist/tools/look-at.d.ts +0 -11
- package/dist/tools/pty/buffer.d.ts +0 -11
- package/dist/tools/pty/index.d.ts +0 -74
- package/dist/tools/pty/manager.d.ts +0 -14
- package/dist/tools/pty/tools/kill.d.ts +0 -12
- package/dist/tools/pty/tools/list.d.ts +0 -6
- package/dist/tools/pty/tools/read.d.ts +0 -18
- package/dist/tools/pty/tools/spawn.d.ts +0 -20
- package/dist/tools/pty/tools/write.d.ts +0 -12
- package/dist/tools/pty/types.d.ts +0 -54
- package/dist/utils/model-limits.d.ts +0 -7
- /package/{dist/tools/background-task/index.d.ts → src/tools/background-task/index.ts} +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
// Patterns that indicate excessive/unnecessary comments
|
|
4
|
+
const EXCESSIVE_COMMENT_PATTERNS = [
|
|
5
|
+
// Obvious comments that explain what code does (not why)
|
|
6
|
+
/\/\/\s*(increment|decrement|add|subtract|set|get|return|call|create|initialize|init)\s+/i,
|
|
7
|
+
/\/\/\s*(the|this|a|an)\s+(following|above|below|next|previous)/i,
|
|
8
|
+
// Section dividers
|
|
9
|
+
/\/\/\s*[-=]{3,}/,
|
|
10
|
+
/\/\/\s*#{3,}/,
|
|
11
|
+
// Empty or whitespace-only comments
|
|
12
|
+
/\/\/\s*$/,
|
|
13
|
+
// "End of" comments
|
|
14
|
+
/\/\/\s*end\s+(of|function|class|method|if|loop|for|while)/i,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// Patterns that are valid and should be ignored
|
|
18
|
+
const VALID_COMMENT_PATTERNS = [
|
|
19
|
+
// TODO/FIXME/NOTE comments
|
|
20
|
+
/\/\/\s*(TODO|FIXME|NOTE|HACK|XXX|BUG|WARN):/i,
|
|
21
|
+
// JSDoc/TSDoc
|
|
22
|
+
/^\s*\*|\/\*\*/,
|
|
23
|
+
// Directive comments (eslint, prettier, ts, etc.)
|
|
24
|
+
/\/\/\s*@|\/\/\s*eslint|\/\/\s*prettier|\/\/\s*ts-|\/\/\s*type:/i,
|
|
25
|
+
// License headers
|
|
26
|
+
/\/\/\s*(copyright|license|spdx)/i,
|
|
27
|
+
// BDD-style comments (describe, it, given, when, then)
|
|
28
|
+
/\/\/\s*(given|when|then|and|but|describe|it|should|expect)/i,
|
|
29
|
+
// URL references
|
|
30
|
+
/\/\/\s*https?:\/\//i,
|
|
31
|
+
// Regex explanations (often necessary)
|
|
32
|
+
/\/\/\s*regex|\/\/\s*pattern/i,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
interface CommentIssue {
|
|
36
|
+
line: number;
|
|
37
|
+
comment: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function analyzeComments(content: string): CommentIssue[] {
|
|
42
|
+
const issues: CommentIssue[] = [];
|
|
43
|
+
const lines = content.split("\n");
|
|
44
|
+
|
|
45
|
+
let consecutiveComments = 0;
|
|
46
|
+
let lastCommentLine = -2;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i];
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
|
|
52
|
+
// Check for comment lines
|
|
53
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
|
|
54
|
+
// Skip valid patterns
|
|
55
|
+
if (VALID_COMMENT_PATTERNS.some((p) => p.test(trimmed))) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check for excessive patterns
|
|
60
|
+
for (const pattern of EXCESSIVE_COMMENT_PATTERNS) {
|
|
61
|
+
if (pattern.test(trimmed)) {
|
|
62
|
+
issues.push({
|
|
63
|
+
line: i + 1,
|
|
64
|
+
comment: trimmed.slice(0, 60) + (trimmed.length > 60 ? "..." : ""),
|
|
65
|
+
reason: "Explains what, not why",
|
|
66
|
+
});
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Track consecutive comments (might indicate over-documentation)
|
|
72
|
+
if (i === lastCommentLine + 1) {
|
|
73
|
+
consecutiveComments++;
|
|
74
|
+
if (consecutiveComments > 5) {
|
|
75
|
+
issues.push({
|
|
76
|
+
line: i + 1,
|
|
77
|
+
comment: trimmed.slice(0, 60),
|
|
78
|
+
reason: "Excessive consecutive comments",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
consecutiveComments = 1;
|
|
83
|
+
}
|
|
84
|
+
lastCommentLine = i;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return issues;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createCommentCheckerHook(_ctx: PluginInput) {
|
|
92
|
+
return {
|
|
93
|
+
// Check after file edits
|
|
94
|
+
"tool.execute.after": async (
|
|
95
|
+
input: { tool: string; args?: Record<string, unknown> },
|
|
96
|
+
output: { output?: string },
|
|
97
|
+
) => {
|
|
98
|
+
// Only check Edit tool
|
|
99
|
+
if (input.tool !== "Edit" && input.tool !== "edit") return;
|
|
100
|
+
|
|
101
|
+
const newString = input.args?.new_string as string | undefined;
|
|
102
|
+
if (!newString) return;
|
|
103
|
+
|
|
104
|
+
const issues = analyzeComments(newString);
|
|
105
|
+
|
|
106
|
+
if (issues.length > 0) {
|
|
107
|
+
const warning = `\n\n⚠️ **Comment Check**: Found ${issues.length} potentially unnecessary comment(s):\n${issues
|
|
108
|
+
.slice(0, 3)
|
|
109
|
+
.map((i) => `- Line ${i.line}: "${i.comment}" (${i.reason})`)
|
|
110
|
+
.join(
|
|
111
|
+
"\n",
|
|
112
|
+
)}${issues.length > 3 ? `\n...and ${issues.length - 3} more` : ""}\n\nComments should explain WHY, not WHAT. Consider removing obvious comments.`;
|
|
113
|
+
|
|
114
|
+
if (output.output) {
|
|
115
|
+
output.output += warning;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join, dirname, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
// Files to inject at project root level
|
|
6
|
+
const ROOT_CONTEXT_FILES = ["ARCHITECTURE.md", "CODE_STYLE.md", "AGENTS.md", "CLAUDE.md", "README.md"] as const;
|
|
7
|
+
|
|
8
|
+
// Files to collect when walking up directories
|
|
9
|
+
const DIRECTORY_CONTEXT_FILES = ["AGENTS.md", "README.md"] as const;
|
|
10
|
+
|
|
11
|
+
// Tools that trigger directory-aware context injection
|
|
12
|
+
const FILE_ACCESS_TOOLS = ["Read", "read", "Edit", "edit"];
|
|
13
|
+
|
|
14
|
+
// Cache for file contents
|
|
15
|
+
interface ContextCache {
|
|
16
|
+
rootContent: Map<string, string>;
|
|
17
|
+
directoryContent: Map<string, Map<string, string>>; // path -> filename -> content
|
|
18
|
+
lastRootCheck: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CACHE_TTL = 30_000; // 30 seconds
|
|
22
|
+
|
|
23
|
+
export function createContextInjectorHook(ctx: PluginInput) {
|
|
24
|
+
const cache: ContextCache = {
|
|
25
|
+
rootContent: new Map(),
|
|
26
|
+
directoryContent: new Map(),
|
|
27
|
+
lastRootCheck: 0,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
async function loadRootContextFiles(): Promise<Map<string, string>> {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
|
|
33
|
+
if (now - cache.lastRootCheck < CACHE_TTL && cache.rootContent.size > 0) {
|
|
34
|
+
return cache.rootContent;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
cache.rootContent.clear();
|
|
38
|
+
cache.lastRootCheck = now;
|
|
39
|
+
|
|
40
|
+
for (const filename of ROOT_CONTEXT_FILES) {
|
|
41
|
+
try {
|
|
42
|
+
const filepath = join(ctx.directory, filename);
|
|
43
|
+
const content = await readFile(filepath, "utf-8");
|
|
44
|
+
if (content.trim()) {
|
|
45
|
+
cache.rootContent.set(filename, content);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// File doesn't exist - skip
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return cache.rootContent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function walkUpForContextFiles(filePath: string): Promise<Map<string, string>> {
|
|
56
|
+
const absPath = resolve(filePath);
|
|
57
|
+
const projectRoot = resolve(ctx.directory);
|
|
58
|
+
|
|
59
|
+
// Check cache
|
|
60
|
+
const cacheKey = dirname(absPath);
|
|
61
|
+
if (cache.directoryContent.has(cacheKey)) {
|
|
62
|
+
return cache.directoryContent.get(cacheKey)!;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const collected = new Map<string, string>();
|
|
66
|
+
let currentDir = dirname(absPath);
|
|
67
|
+
|
|
68
|
+
// Walk up from file directory to project root
|
|
69
|
+
while (currentDir.startsWith(projectRoot) || currentDir === projectRoot) {
|
|
70
|
+
for (const filename of DIRECTORY_CONTEXT_FILES) {
|
|
71
|
+
const contextPath = join(currentDir, filename);
|
|
72
|
+
const relPath = currentDir.replace(projectRoot, "").replace(/^\//, "") || ".";
|
|
73
|
+
const key = `${relPath}/${filename}`;
|
|
74
|
+
|
|
75
|
+
// Skip if already have this file from a closer directory
|
|
76
|
+
if (!collected.has(key)) {
|
|
77
|
+
try {
|
|
78
|
+
const content = await readFile(contextPath, "utf-8");
|
|
79
|
+
if (content.trim()) {
|
|
80
|
+
collected.set(key, content);
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// File doesn't exist - skip
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Stop at project root
|
|
89
|
+
if (currentDir === projectRoot) break;
|
|
90
|
+
|
|
91
|
+
// Move up
|
|
92
|
+
const parent = dirname(currentDir);
|
|
93
|
+
if (parent === currentDir) break; // Reached filesystem root
|
|
94
|
+
currentDir = parent;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Cache for this directory
|
|
98
|
+
cache.directoryContent.set(cacheKey, collected);
|
|
99
|
+
|
|
100
|
+
// Limit cache size
|
|
101
|
+
if (cache.directoryContent.size > 100) {
|
|
102
|
+
const firstKey = cache.directoryContent.keys().next().value;
|
|
103
|
+
if (firstKey) cache.directoryContent.delete(firstKey);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return collected;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatContextBlock(files: Map<string, string>, label: string): string {
|
|
110
|
+
if (files.size === 0) return "";
|
|
111
|
+
|
|
112
|
+
const blocks: string[] = [];
|
|
113
|
+
|
|
114
|
+
for (const [filename, content] of files) {
|
|
115
|
+
blocks.push(`<context file="${filename}">\n${content}\n</context>`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return `\n<${label}>\n${blocks.join("\n\n")}\n</${label}>\n`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
// Inject project root context into every chat
|
|
123
|
+
"chat.params": async (
|
|
124
|
+
_input: { sessionID: string },
|
|
125
|
+
output: { options?: Record<string, unknown>; system?: string },
|
|
126
|
+
) => {
|
|
127
|
+
const files = await loadRootContextFiles();
|
|
128
|
+
if (files.size === 0) return;
|
|
129
|
+
|
|
130
|
+
const contextBlock = formatContextBlock(files, "project-context");
|
|
131
|
+
|
|
132
|
+
if (output.system) {
|
|
133
|
+
output.system = output.system + contextBlock;
|
|
134
|
+
} else {
|
|
135
|
+
output.system = contextBlock;
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// Inject directory-specific context when reading/editing files
|
|
140
|
+
"tool.execute.after": async (
|
|
141
|
+
input: { tool: string; args?: Record<string, unknown> },
|
|
142
|
+
output: { output?: string },
|
|
143
|
+
) => {
|
|
144
|
+
if (!FILE_ACCESS_TOOLS.includes(input.tool)) return;
|
|
145
|
+
|
|
146
|
+
const filePath = input.args?.file_path as string | undefined;
|
|
147
|
+
if (!filePath) return;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const directoryFiles = await walkUpForContextFiles(filePath);
|
|
151
|
+
if (directoryFiles.size === 0) return;
|
|
152
|
+
|
|
153
|
+
const contextBlock = formatContextBlock(directoryFiles, "directory-context");
|
|
154
|
+
|
|
155
|
+
if (output.output) {
|
|
156
|
+
output.output = output.output + contextBlock;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore errors in context injection
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import { getContextLimit } from "../utils/model-limits";
|
|
3
|
+
|
|
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
|
+
interface MonitorState {
|
|
9
|
+
lastWarningTime: Map<string, number>;
|
|
10
|
+
lastUsageRatio: Map<string, number>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const WARNING_COOLDOWN_MS = 120_000; // 2 minutes between warnings
|
|
14
|
+
|
|
15
|
+
export function createContextWindowMonitorHook(ctx: PluginInput) {
|
|
16
|
+
const state: MonitorState = {
|
|
17
|
+
lastWarningTime: new Map(),
|
|
18
|
+
lastUsageRatio: new Map(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getEncouragementMessage(usageRatio: number): string {
|
|
22
|
+
const remaining = Math.round((1 - usageRatio) * 100);
|
|
23
|
+
|
|
24
|
+
if (usageRatio < WARNING_THRESHOLD) {
|
|
25
|
+
return ""; // No message needed
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (usageRatio < CRITICAL_THRESHOLD) {
|
|
29
|
+
return `Context: ${remaining}% remaining. Plenty of room - don't rush.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return `Context: ${remaining}% remaining. Consider wrapping up or compacting soon.`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
// Inject context awareness into chat params
|
|
37
|
+
"chat.params": async (
|
|
38
|
+
input: { sessionID: string },
|
|
39
|
+
output: { system?: string; options?: Record<string, unknown> },
|
|
40
|
+
) => {
|
|
41
|
+
const usageRatio = state.lastUsageRatio.get(input.sessionID);
|
|
42
|
+
|
|
43
|
+
if (usageRatio && usageRatio >= WARNING_THRESHOLD) {
|
|
44
|
+
const message = getEncouragementMessage(usageRatio);
|
|
45
|
+
if (message && output.system) {
|
|
46
|
+
output.system = `${output.system}\n\n<context-status>${message}</context-status>`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
event: async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
|
52
|
+
const props = event.properties as Record<string, unknown> | undefined;
|
|
53
|
+
|
|
54
|
+
// Cleanup on session delete
|
|
55
|
+
if (event.type === "session.deleted") {
|
|
56
|
+
const sessionInfo = props?.info as { id?: string } | undefined;
|
|
57
|
+
if (sessionInfo?.id) {
|
|
58
|
+
state.lastWarningTime.delete(sessionInfo.id);
|
|
59
|
+
state.lastUsageRatio.delete(sessionInfo.id);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Track usage on message updates
|
|
65
|
+
if (event.type === "message.updated") {
|
|
66
|
+
const info = props?.info as Record<string, unknown> | undefined;
|
|
67
|
+
const sessionID = info?.sessionID as string | undefined;
|
|
68
|
+
|
|
69
|
+
if (!sessionID || info?.role !== "assistant") return;
|
|
70
|
+
|
|
71
|
+
const usage = info.usage as Record<string, unknown> | undefined;
|
|
72
|
+
const inputTokens = (usage?.inputTokens as number) || 0;
|
|
73
|
+
const cacheRead = (usage?.cacheReadInputTokens as number) || 0;
|
|
74
|
+
const totalUsed = inputTokens + cacheRead;
|
|
75
|
+
|
|
76
|
+
const modelID = (info.modelID as string) || "";
|
|
77
|
+
const contextLimit = getContextLimit(modelID);
|
|
78
|
+
const usageRatio = totalUsed / contextLimit;
|
|
79
|
+
|
|
80
|
+
state.lastUsageRatio.set(sessionID, usageRatio);
|
|
81
|
+
|
|
82
|
+
// Show toast warning if threshold crossed
|
|
83
|
+
if (usageRatio >= WARNING_THRESHOLD) {
|
|
84
|
+
const lastWarning = state.lastWarningTime.get(sessionID) || 0;
|
|
85
|
+
if (Date.now() - lastWarning > WARNING_COOLDOWN_MS) {
|
|
86
|
+
state.lastWarningTime.set(sessionID, Date.now());
|
|
87
|
+
|
|
88
|
+
const remaining = Math.round((1 - usageRatio) * 100);
|
|
89
|
+
const variant = usageRatio >= CRITICAL_THRESHOLD ? "warning" : "info";
|
|
90
|
+
|
|
91
|
+
await ctx.client.tui
|
|
92
|
+
.showToast({
|
|
93
|
+
body: {
|
|
94
|
+
title: "Context Window",
|
|
95
|
+
message: `${remaining}% remaining (${Math.round(totalUsed / 1000)}K / ${Math.round(contextLimit / 1000)}K tokens)`,
|
|
96
|
+
variant,
|
|
97
|
+
duration: 4000,
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
.catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// src/hooks/file-ops-tracker.ts
|
|
2
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
3
|
+
|
|
4
|
+
interface FileOps {
|
|
5
|
+
read: Set<string>;
|
|
6
|
+
modified: Set<string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Per-session file operation tracking
|
|
10
|
+
const sessionFileOps = new Map<string, FileOps>();
|
|
11
|
+
|
|
12
|
+
function getOrCreateOps(sessionID: string): FileOps {
|
|
13
|
+
let ops = sessionFileOps.get(sessionID);
|
|
14
|
+
if (!ops) {
|
|
15
|
+
ops = { read: new Set(), modified: new Set() };
|
|
16
|
+
sessionFileOps.set(sessionID, ops);
|
|
17
|
+
}
|
|
18
|
+
return ops;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function trackFileOp(sessionID: string, operation: "read" | "write" | "edit", filePath: string): void {
|
|
22
|
+
const ops = getOrCreateOps(sessionID);
|
|
23
|
+
if (operation === "read") {
|
|
24
|
+
ops.read.add(filePath);
|
|
25
|
+
} else {
|
|
26
|
+
// write and edit both modify files
|
|
27
|
+
ops.modified.add(filePath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getFileOps(sessionID: string): FileOps {
|
|
32
|
+
const ops = sessionFileOps.get(sessionID);
|
|
33
|
+
if (!ops) {
|
|
34
|
+
return { read: new Set(), modified: new Set() };
|
|
35
|
+
}
|
|
36
|
+
return ops;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function clearFileOps(sessionID: string): void {
|
|
40
|
+
sessionFileOps.delete(sessionID);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getAndClearFileOps(sessionID: string): FileOps {
|
|
44
|
+
const ops = getFileOps(sessionID);
|
|
45
|
+
// Return copies of the sets before clearing
|
|
46
|
+
const result = {
|
|
47
|
+
read: new Set(ops.read),
|
|
48
|
+
modified: new Set(ops.modified),
|
|
49
|
+
};
|
|
50
|
+
clearFileOps(sessionID);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function formatFileOpsForPrompt(ops: FileOps): string {
|
|
55
|
+
const readPaths = Array.from(ops.read).sort();
|
|
56
|
+
const modifiedPaths = Array.from(ops.modified).sort();
|
|
57
|
+
|
|
58
|
+
let result = "<file-operations>\n";
|
|
59
|
+
result += `Read: ${readPaths.length > 0 ? readPaths.join(", ") : "(none)"}\n`;
|
|
60
|
+
result += `Modified: ${modifiedPaths.length > 0 ? modifiedPaths.join(", ") : "(none)"}\n`;
|
|
61
|
+
result += "</file-operations>";
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createFileOpsTrackerHook(_ctx: PluginInput) {
|
|
67
|
+
return {
|
|
68
|
+
"tool.execute.after": async (
|
|
69
|
+
input: { tool: string; sessionID: string; args?: Record<string, unknown> },
|
|
70
|
+
_output: { output?: string },
|
|
71
|
+
) => {
|
|
72
|
+
const toolName = input.tool.toLowerCase();
|
|
73
|
+
|
|
74
|
+
// Only track read, write, edit tools
|
|
75
|
+
if (!["read", "write", "edit"].includes(toolName)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Extract file path from args
|
|
80
|
+
const filePath = input.args?.filePath as string | undefined;
|
|
81
|
+
if (!filePath) return;
|
|
82
|
+
|
|
83
|
+
trackFileOp(input.sessionID, toolName as "read" | "write" | "edit", filePath);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
event: async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
|
87
|
+
// Clean up on session delete
|
|
88
|
+
if (event.type === "session.deleted") {
|
|
89
|
+
const props = event.properties as { info?: { id?: string } } | undefined;
|
|
90
|
+
if (props?.info?.id) {
|
|
91
|
+
clearFileOps(props.info.id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/hooks/ledger-loader.ts
|
|
2
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
3
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const LEDGER_DIR = "thoughts/ledgers";
|
|
7
|
+
const LEDGER_PREFIX = "CONTINUITY_";
|
|
8
|
+
|
|
9
|
+
export interface LedgerInfo {
|
|
10
|
+
sessionName: string;
|
|
11
|
+
filePath: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function findCurrentLedger(directory: string): Promise<LedgerInfo | null> {
|
|
16
|
+
const ledgerDir = join(directory, LEDGER_DIR);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const files = await readdir(ledgerDir);
|
|
20
|
+
const ledgerFiles = files.filter((f) => f.startsWith(LEDGER_PREFIX) && f.endsWith(".md"));
|
|
21
|
+
|
|
22
|
+
if (ledgerFiles.length === 0) return null;
|
|
23
|
+
|
|
24
|
+
// Get most recently modified ledger
|
|
25
|
+
let latestFile = ledgerFiles[0];
|
|
26
|
+
let latestMtime = 0;
|
|
27
|
+
|
|
28
|
+
for (const file of ledgerFiles) {
|
|
29
|
+
const filePath = join(ledgerDir, file);
|
|
30
|
+
try {
|
|
31
|
+
const stat = await Bun.file(filePath).stat();
|
|
32
|
+
if (stat && stat.mtime.getTime() > latestMtime) {
|
|
33
|
+
latestMtime = stat.mtime.getTime();
|
|
34
|
+
latestFile = file;
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Skip files we can't stat
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const filePath = join(ledgerDir, latestFile);
|
|
42
|
+
const content = await readFile(filePath, "utf-8");
|
|
43
|
+
const sessionName = latestFile.replace(LEDGER_PREFIX, "").replace(".md", "");
|
|
44
|
+
|
|
45
|
+
return { sessionName, filePath, content };
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatLedgerInjection(ledger: LedgerInfo): string {
|
|
52
|
+
return `<continuity-ledger session="${ledger.sessionName}">
|
|
53
|
+
${ledger.content}
|
|
54
|
+
</continuity-ledger>
|
|
55
|
+
|
|
56
|
+
You are resuming work from a previous context clear. The ledger above contains your session state.
|
|
57
|
+
Review it and continue from where you left off. The "Now" item is your current focus.`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createLedgerLoaderHook(ctx: PluginInput) {
|
|
61
|
+
return {
|
|
62
|
+
"chat.params": async (
|
|
63
|
+
_input: { sessionID: string },
|
|
64
|
+
output: { options?: Record<string, unknown>; system?: string },
|
|
65
|
+
) => {
|
|
66
|
+
const ledger = await findCurrentLedger(ctx.directory);
|
|
67
|
+
if (!ledger) return;
|
|
68
|
+
|
|
69
|
+
const injection = formatLedgerInjection(ledger);
|
|
70
|
+
|
|
71
|
+
if (output.system) {
|
|
72
|
+
output.system = `${injection}\n\n${output.system}`;
|
|
73
|
+
} else {
|
|
74
|
+
output.system = injection;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|