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.
Files changed (92) hide show
  1. package/package.json +7 -13
  2. package/src/agents/artifact-searcher.ts +46 -0
  3. package/src/agents/brainstormer.ts +145 -0
  4. package/src/agents/codebase-analyzer.ts +75 -0
  5. package/src/agents/codebase-locator.ts +71 -0
  6. package/src/agents/commander.ts +138 -0
  7. package/src/agents/executor.ts +215 -0
  8. package/src/agents/implementer.ts +99 -0
  9. package/src/agents/index.ts +44 -0
  10. package/src/agents/ledger-creator.ts +113 -0
  11. package/src/agents/pattern-finder.ts +70 -0
  12. package/src/agents/planner.ts +230 -0
  13. package/src/agents/project-initializer.ts +264 -0
  14. package/src/agents/reviewer.ts +102 -0
  15. package/src/config-loader.ts +89 -0
  16. package/src/hooks/artifact-auto-index.ts +111 -0
  17. package/src/hooks/auto-clear-ledger.ts +230 -0
  18. package/src/hooks/auto-compact.ts +241 -0
  19. package/src/hooks/comment-checker.ts +120 -0
  20. package/src/hooks/context-injector.ts +163 -0
  21. package/src/hooks/context-window-monitor.ts +106 -0
  22. package/src/hooks/file-ops-tracker.ts +96 -0
  23. package/src/hooks/ledger-loader.ts +78 -0
  24. package/src/hooks/preemptive-compaction.ts +183 -0
  25. package/src/hooks/session-recovery.ts +258 -0
  26. package/src/hooks/token-aware-truncation.ts +189 -0
  27. package/src/index.ts +258 -0
  28. package/src/tools/artifact-index/index.ts +269 -0
  29. package/src/tools/artifact-index/schema.sql +44 -0
  30. package/src/tools/artifact-search.ts +49 -0
  31. package/src/tools/ast-grep/index.ts +189 -0
  32. package/src/tools/background-task/manager.ts +374 -0
  33. package/src/tools/background-task/tools.ts +145 -0
  34. package/src/tools/background-task/types.ts +68 -0
  35. package/src/tools/btca/index.ts +82 -0
  36. package/src/tools/look-at.ts +210 -0
  37. package/src/tools/pty/buffer.ts +49 -0
  38. package/src/tools/pty/index.ts +34 -0
  39. package/src/tools/pty/manager.ts +159 -0
  40. package/src/tools/pty/tools/kill.ts +68 -0
  41. package/src/tools/pty/tools/list.ts +55 -0
  42. package/src/tools/pty/tools/read.ts +152 -0
  43. package/src/tools/pty/tools/spawn.ts +78 -0
  44. package/src/tools/pty/tools/write.ts +97 -0
  45. package/src/tools/pty/types.ts +62 -0
  46. package/src/utils/model-limits.ts +36 -0
  47. package/dist/agents/artifact-searcher.d.ts +0 -2
  48. package/dist/agents/brainstormer.d.ts +0 -2
  49. package/dist/agents/codebase-analyzer.d.ts +0 -2
  50. package/dist/agents/codebase-locator.d.ts +0 -2
  51. package/dist/agents/commander.d.ts +0 -3
  52. package/dist/agents/executor.d.ts +0 -2
  53. package/dist/agents/implementer.d.ts +0 -2
  54. package/dist/agents/index.d.ts +0 -15
  55. package/dist/agents/ledger-creator.d.ts +0 -2
  56. package/dist/agents/pattern-finder.d.ts +0 -2
  57. package/dist/agents/planner.d.ts +0 -2
  58. package/dist/agents/project-initializer.d.ts +0 -2
  59. package/dist/agents/reviewer.d.ts +0 -2
  60. package/dist/config-loader.d.ts +0 -20
  61. package/dist/hooks/artifact-auto-index.d.ts +0 -19
  62. package/dist/hooks/auto-clear-ledger.d.ts +0 -11
  63. package/dist/hooks/auto-compact.d.ts +0 -9
  64. package/dist/hooks/comment-checker.d.ts +0 -9
  65. package/dist/hooks/context-injector.d.ts +0 -15
  66. package/dist/hooks/context-window-monitor.d.ts +0 -15
  67. package/dist/hooks/file-ops-tracker.d.ts +0 -26
  68. package/dist/hooks/ledger-loader.d.ts +0 -16
  69. package/dist/hooks/preemptive-compaction.d.ts +0 -9
  70. package/dist/hooks/session-recovery.d.ts +0 -9
  71. package/dist/hooks/token-aware-truncation.d.ts +0 -15
  72. package/dist/index.d.ts +0 -3
  73. package/dist/index.js +0 -17089
  74. package/dist/tools/artifact-index/index.d.ts +0 -38
  75. package/dist/tools/artifact-search.d.ts +0 -17
  76. package/dist/tools/ast-grep/index.d.ts +0 -88
  77. package/dist/tools/background-task/manager.d.ts +0 -27
  78. package/dist/tools/background-task/tools.d.ts +0 -41
  79. package/dist/tools/background-task/types.d.ts +0 -53
  80. package/dist/tools/btca/index.d.ts +0 -19
  81. package/dist/tools/look-at.d.ts +0 -11
  82. package/dist/tools/pty/buffer.d.ts +0 -11
  83. package/dist/tools/pty/index.d.ts +0 -74
  84. package/dist/tools/pty/manager.d.ts +0 -14
  85. package/dist/tools/pty/tools/kill.d.ts +0 -12
  86. package/dist/tools/pty/tools/list.d.ts +0 -6
  87. package/dist/tools/pty/tools/read.d.ts +0 -18
  88. package/dist/tools/pty/tools/spawn.d.ts +0 -20
  89. package/dist/tools/pty/tools/write.d.ts +0 -12
  90. package/dist/tools/pty/types.d.ts +0 -54
  91. package/dist/utils/model-limits.d.ts +0 -7
  92. /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
+ }