opencodekit 0.14.0 → 0.14.2

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 (54) hide show
  1. package/dist/index.js +53 -18
  2. package/dist/template/.opencode/.background-tasks.json +96 -0
  3. package/dist/template/.opencode/.ralph-state.json +12 -0
  4. package/dist/template/.opencode/AGENTS.md +112 -6
  5. package/dist/template/.opencode/agent/build.md +60 -8
  6. package/dist/template/.opencode/agent/explore.md +1 -0
  7. package/dist/template/.opencode/agent/looker.md +124 -0
  8. package/dist/template/.opencode/agent/planner.md +40 -1
  9. package/dist/template/.opencode/agent/review.md +1 -0
  10. package/dist/template/.opencode/agent/rush.md +53 -6
  11. package/dist/template/.opencode/agent/scout.md +1 -1
  12. package/dist/template/.opencode/agent/vision.md +0 -1
  13. package/dist/template/.opencode/command/brainstorm.md +58 -3
  14. package/dist/template/.opencode/command/finish.md +18 -8
  15. package/dist/template/.opencode/command/fix.md +24 -15
  16. package/dist/template/.opencode/command/implement.md +95 -29
  17. package/dist/template/.opencode/command/import-plan.md +30 -8
  18. package/dist/template/.opencode/command/new-feature.md +105 -14
  19. package/dist/template/.opencode/command/plan.md +78 -11
  20. package/dist/template/.opencode/command/pr.md +25 -15
  21. package/dist/template/.opencode/command/ralph-loop.md +97 -0
  22. package/dist/template/.opencode/command/revert-feature.md +15 -3
  23. package/dist/template/.opencode/command/skill-optimize.md +71 -7
  24. package/dist/template/.opencode/command/start.md +63 -15
  25. package/dist/template/.opencode/dcp.jsonc +11 -7
  26. package/dist/template/.opencode/memory/{project/beads-workflow.md → beads-workflow.md} +53 -0
  27. package/dist/template/.opencode/memory/observations/2026-01-09-pattern-ampcode-mcp-json-includetools-pattern.md +42 -0
  28. package/dist/template/.opencode/memory/project/conventions.md +53 -3
  29. package/dist/template/.opencode/memory/project/gotchas.md +52 -5
  30. package/dist/template/.opencode/memory/vector_db/memories.lance/_transactions/{0-8d00d272-cb80-463b-9774-7120a1c994e7.txn → 0-0d25ba80-ba3b-4209-9046-b45d6093b4da.txn} +0 -0
  31. package/dist/template/.opencode/memory/vector_db/memories.lance/_versions/1.manifest +0 -0
  32. package/dist/template/.opencode/memory/vector_db/memories.lance/data/{001010101000000101110001f998d04b63936ff83f9a34152d.lance → 1111100101010101011010004a9ef34df6b29f36a9a53a2892.lance} +0 -0
  33. package/dist/template/.opencode/opencode.json +529 -587
  34. package/dist/template/.opencode/package.json +2 -1
  35. package/dist/template/.opencode/plugin/lsp.ts +299 -0
  36. package/dist/template/.opencode/plugin/memory.ts +77 -1
  37. package/dist/template/.opencode/plugin/package.json +1 -1
  38. package/dist/template/.opencode/plugin/ralph-wiggum.ts +182 -0
  39. package/dist/template/.opencode/plugin/skill-mcp.ts +155 -36
  40. package/dist/template/.opencode/skill/chrome-devtools/SKILL.md +43 -65
  41. package/dist/template/.opencode/skill/chrome-devtools/mcp.json +19 -0
  42. package/dist/template/.opencode/skill/executing-plans/SKILL.md +32 -2
  43. package/dist/template/.opencode/skill/finishing-a-development-branch/SKILL.md +42 -17
  44. package/dist/template/.opencode/skill/playwright/SKILL.md +58 -133
  45. package/dist/template/.opencode/skill/playwright/mcp.json +16 -0
  46. package/dist/template/.opencode/tool/background.ts +461 -0
  47. package/dist/template/.opencode/tool/memory-search.ts +2 -2
  48. package/dist/template/.opencode/tool/ralph.ts +203 -0
  49. package/package.json +4 -16
  50. package/dist/template/.opencode/memory/vector_db/memories.lance/_transactions/1-a3bea825-dad3-47dd-a6d6-ff41b76ff7b0.txn +0 -0
  51. package/dist/template/.opencode/memory/vector_db/memories.lance/_versions/2.manifest +0 -0
  52. package/dist/template/.opencode/memory/vector_db/memories.lance/data/010000101010000000010010701b3840d38c2b5f275da99978.lance +0 -0
  53. /package/dist/template/.opencode/memory/{project/README.md → README.md} +0 -0
  54. /package/dist/template/.opencode/plugin/{notification.ts → notification.ts.bak} +0 -0
@@ -12,7 +12,8 @@
12
12
  "license": "ISC",
13
13
  "dependencies": {
14
14
  "@lancedb/lancedb": "^0.23.0",
15
- "@opencode-ai/plugin": "1.1.6",
15
+ "@opencode-ai/plugin": "1.1.8",
16
+ "@opencode-ai/sdk": "^1.1.8",
16
17
  "openai": "^6.15.0"
17
18
  },
18
19
  "devDependencies": {
@@ -0,0 +1,299 @@
1
+ /**
2
+ * LSP Plugin - Active LSP Tool Enforcement
3
+ *
4
+ * Forces agents to actively use LSP tools when code files are detected.
5
+ * This is NOT a suggestion - agents MUST execute LSP operations immediately.
6
+ *
7
+ * Mechanism:
8
+ * 1. Hooks into grep/glob/read tool outputs (tool.execute.after)
9
+ * 2. Hooks into user messages mentioning code files (chat.message)
10
+ * 3. Injects MANDATORY LSP execution commands
11
+ * 4. Uses strong language to override agent tendencies to skip
12
+ *
13
+ * Based on oh-my-opencode's LSP forcing pattern.
14
+ */
15
+
16
+ import type { Plugin } from "@opencode-ai/plugin";
17
+
18
+ // File extensions that support LSP
19
+ const LSP_SUPPORTED_EXTENSIONS = new Set([
20
+ ".ts",
21
+ ".tsx",
22
+ ".js",
23
+ ".jsx",
24
+ ".py",
25
+ ".go",
26
+ ".rs",
27
+ ".java",
28
+ ".c",
29
+ ".cpp",
30
+ ".h",
31
+ ".hpp",
32
+ ".cs",
33
+ ".rb",
34
+ ".php",
35
+ ".swift",
36
+ ".kt",
37
+ ".scala",
38
+ ".lua",
39
+ ".zig",
40
+ ".vue",
41
+ ".svelte",
42
+ ]);
43
+
44
+ // Regex to extract file:line patterns from tool output
45
+ const FILE_LINE_PATTERNS = [
46
+ // Standard grep output: path/file.ts:42: content
47
+ /^([^\s:]+\.(ts|tsx|js|jsx|py|go|rs|java|c|cpp|h|hpp|cs|rb|php|swift|kt|scala|lua|zig|vue|svelte)):(\d+):/gm,
48
+ // Just path with line: path/file.ts:42
49
+ /([^\s:]+\.(ts|tsx|js|jsx|py|go|rs|java|c|cpp|h|hpp|cs|rb|php|swift|kt|scala|lua|zig|vue|svelte)):(\d+)/g,
50
+ // Glob/list output with just paths
51
+ /^([^\s:]+\.(ts|tsx|js|jsx|py|go|rs|java|c|cpp|h|hpp|cs|rb|php|swift|kt|scala|lua|zig|vue|svelte))$/gm,
52
+ ];
53
+
54
+ // Patterns that indicate user is asking about code
55
+ const CODE_INTENT_PATTERNS = [
56
+ /\b(edit|modify|change|update|fix|refactor|add|remove|delete)\b.*\.(ts|tsx|js|jsx|py|go|rs)/i,
57
+ /\b(function|class|method|variable|type|interface)\s+\w+/i,
58
+ /\b(implement|create|build)\b.*\b(feature|component|module)/i,
59
+ /@[^\s]+\.(ts|tsx|js|jsx|py|go|rs)/i, // @file.ts mentions
60
+ /\b(src|lib|app|components?)\/[^\s]+\.(ts|tsx|js|jsx)/i, // Path patterns
61
+ ];
62
+
63
+ interface FileMatch {
64
+ filePath: string;
65
+ line?: number;
66
+ character?: number;
67
+ }
68
+
69
+ function extractFileMatches(output: string): FileMatch[] {
70
+ const matches: FileMatch[] = [];
71
+ const seen = new Set<string>();
72
+
73
+ for (const pattern of FILE_LINE_PATTERNS) {
74
+ pattern.lastIndex = 0;
75
+
76
+ let match = pattern.exec(output);
77
+ while (match !== null) {
78
+ const filePath = match[1];
79
+ const line = match[3] ? Number.parseInt(match[3], 10) : undefined;
80
+
81
+ const key = `${filePath}:${line || 0}`;
82
+ if (!seen.has(key)) {
83
+ seen.add(key);
84
+ matches.push({
85
+ filePath,
86
+ line,
87
+ character: 1,
88
+ });
89
+ }
90
+
91
+ match = pattern.exec(output);
92
+ }
93
+ }
94
+
95
+ return matches.slice(0, 5);
96
+ }
97
+
98
+ function extractFilesFromUserMessage(text: string): string[] {
99
+ const files: string[] = [];
100
+ const seen = new Set<string>();
101
+
102
+ // Match @file.ts patterns
103
+ const atMentions = text.match(/@([^\s]+\.(ts|tsx|js|jsx|py|go|rs))/gi) || [];
104
+ for (const mention of atMentions) {
105
+ const file = mention.replace("@", "");
106
+ if (!seen.has(file)) {
107
+ seen.add(file);
108
+ files.push(file);
109
+ }
110
+ }
111
+
112
+ // Match path patterns like src/foo.ts
113
+ const pathPatterns =
114
+ text.match(
115
+ /\b(src|lib|app|components?)\/[^\s]+\.(ts|tsx|js|jsx|py|go|rs)/gi,
116
+ ) || [];
117
+ for (const path of pathPatterns) {
118
+ if (!seen.has(path)) {
119
+ seen.add(path);
120
+ files.push(path);
121
+ }
122
+ }
123
+
124
+ return files.slice(0, 3);
125
+ }
126
+
127
+ function generateLspCommands(matches: FileMatch[]): string {
128
+ if (matches.length === 0) return "";
129
+
130
+ const commands: string[] = [];
131
+
132
+ for (const match of matches) {
133
+ if (match.line) {
134
+ // With line number - get definition and references
135
+ commands.push(
136
+ `lsp({ operation: "goToDefinition", filePath: "${match.filePath}", line: ${match.line}, character: ${match.character || 1} })`,
137
+ );
138
+ commands.push(
139
+ `lsp({ operation: "findReferences", filePath: "${match.filePath}", line: ${match.line}, character: ${match.character || 1} })`,
140
+ );
141
+ commands.push(
142
+ `lsp({ operation: "hover", filePath: "${match.filePath}", line: ${match.line}, character: ${match.character || 1} })`,
143
+ );
144
+ } else {
145
+ // Just file path - get document symbols
146
+ commands.push(
147
+ `lsp({ operation: "documentSymbol", filePath: "${match.filePath}", line: 1, character: 1 })`,
148
+ );
149
+ }
150
+ }
151
+
152
+ const uniqueCommands = [...new Set(commands)].slice(0, 6);
153
+
154
+ // AGGRESSIVE enforcement format - designed to force action
155
+ return `
156
+
157
+ ╔══════════════════════════════════════════════════════════════════════════════╗
158
+ ║ ⚡ LSP EXECUTION REQUIRED - DO NOT SKIP ║
159
+ ╠══════════════════════════════════════════════════════════════════════════════╣
160
+ ║ ║
161
+ ║ Code files detected. You MUST execute these LSP operations NOW. ║
162
+ ║ DO NOT proceed with edits until LSP context is retrieved. ║
163
+ ║ ║
164
+ ║ EXECUTE IMMEDIATELY (copy-paste ready): ║
165
+ ║ ║
166
+ ${uniqueCommands.map((cmd) => `║ → ${cmd}`).join("\n")}
167
+ ║ ║
168
+ ╠══════════════════════════════════════════════════════════════════════════════╣
169
+ ║ WHY THIS IS MANDATORY: ║
170
+ ║ • grep/read show text, LSP shows semantic meaning ║
171
+ ║ • LSP provides accurate type info, definitions, call hierarchies ║
172
+ ║ • Editing without LSP context leads to broken code ║
173
+ ║ • This is a HARD RULE, not a suggestion ║
174
+ ╠══════════════════════════════════════════════════════════════════════════════╣
175
+ ║ WORKFLOW: grep/read → LSP → understand → THEN edit ║
176
+ ╚══════════════════════════════════════════════════════════════════════════════╝
177
+ `;
178
+ }
179
+
180
+ function generateUserMessageNudge(files: string[]): string {
181
+ const lspCommands = files.map(
182
+ (f) =>
183
+ `lsp({ operation: "documentSymbol", filePath: "${f}", line: 1, character: 1 })`,
184
+ );
185
+
186
+ return `
187
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
188
+ ⚡ [LSP CONTEXT REQUIRED]
189
+
190
+ Before making ANY changes to these files, you MUST first retrieve LSP context:
191
+
192
+ ${lspCommands.map((cmd) => `→ ${cmd}`).join("\n")}
193
+
194
+ This is a HARD RULE. Do NOT guess about code structure.
195
+ Workflow: READ file → LSP documentSymbol/hover → UNDERSTAND → THEN edit
196
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
197
+ `;
198
+ }
199
+
200
+ function shouldInjectNudge(toolName: string): boolean {
201
+ return ["grep", "glob", "read", "batch"].includes(toolName);
202
+ }
203
+
204
+ function hasCodeFiles(output: string): boolean {
205
+ for (const ext of LSP_SUPPORTED_EXTENSIONS) {
206
+ if (output.includes(ext)) return true;
207
+ }
208
+ return false;
209
+ }
210
+
211
+ function userMessageMentionsCode(text: string): boolean {
212
+ return CODE_INTENT_PATTERNS.some((pattern) => pattern.test(text));
213
+ }
214
+
215
+ export const Lsp: Plugin = async () => {
216
+ return {
217
+ name: "lsp",
218
+ version: "1.1.0",
219
+
220
+ /**
221
+ * Hook: chat.message
222
+ * Injects LSP reminder when user mentions code files in their message
223
+ * This catches intent BEFORE tools are executed
224
+ */
225
+ "chat.message": async (_input, output) => {
226
+ const { message, parts } = output;
227
+
228
+ // Only process user messages
229
+ if (message.role !== "user") return;
230
+
231
+ // Extract text from all parts
232
+ const fullText = parts
233
+ .filter((p) => p.type === "text")
234
+ .map((p) => ("text" in p ? p.text : ""))
235
+ .join(" ");
236
+
237
+ // Check if user is mentioning code files or asking about code
238
+ if (!userMessageMentionsCode(fullText)) return;
239
+
240
+ // Extract specific files mentioned
241
+ const files = extractFilesFromUserMessage(fullText);
242
+
243
+ // If files found, inject specific LSP commands
244
+ // If no specific files but code intent detected, inject general reminder
245
+ const nudgeText =
246
+ files.length > 0
247
+ ? generateUserMessageNudge(files)
248
+ : `
249
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
250
+ ⚡ [LSP FIRST - HARD RULE]
251
+
252
+ Code modification detected. Before editing:
253
+ 1. Use grep/glob to find relevant files
254
+ 2. Use READ to view the file
255
+ 3. Use LSP (documentSymbol, goToDefinition, findReferences) to understand structure
256
+ 4. ONLY THEN make edits
257
+
258
+ Do NOT skip LSP. Editing without semantic context leads to broken code.
259
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
260
+ `;
261
+
262
+ // Inject synthetic message part - cast to any for synthetic property
263
+ (parts as unknown[]).push({
264
+ type: "text",
265
+ text: nudgeText,
266
+ synthetic: true,
267
+ });
268
+ },
269
+
270
+ /**
271
+ * Hook: tool.execute.after
272
+ * Injects LSP commands after grep/glob/read returns code file results
273
+ * This catches AFTER tools show code files
274
+ */
275
+ "tool.execute.after": async (input, output) => {
276
+ const { tool: toolName } = input;
277
+ const result = output.output;
278
+
279
+ // Skip if not a search/read tool
280
+ if (!shouldInjectNudge(toolName)) return;
281
+
282
+ // Skip if no code files in output
283
+ if (typeof result !== "string" || !hasCodeFiles(result)) return;
284
+
285
+ // Extract file matches
286
+ const matches = extractFileMatches(result);
287
+ if (matches.length === 0) return;
288
+
289
+ // Generate LSP commands (not suggestions - COMMANDS)
290
+ const lspBlock = generateLspCommands(matches);
291
+ if (!lspBlock) return;
292
+
293
+ // Append mandatory LSP block to tool output
294
+ output.output = `${result}${lspBlock}`;
295
+ },
296
+ };
297
+ };
298
+
299
+ export default Lsp;
@@ -29,7 +29,83 @@ import type { Plugin } from "@opencode-ai/plugin";
29
29
  const MEMORY_DIR = ".opencode/memory";
30
30
  const BEADS_DIR = ".beads/artifacts";
31
31
  const SRC_DIR = "src";
32
- const CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"];
32
+ // All extensions supported by OpenCode LSP (https://opencode.ai/docs/lsp/)
33
+ const CODE_EXTENSIONS = [
34
+ // TypeScript/JavaScript
35
+ ".ts",
36
+ ".tsx",
37
+ ".js",
38
+ ".jsx",
39
+ ".mjs",
40
+ ".cjs",
41
+ ".mts",
42
+ ".cts",
43
+ // Web frameworks
44
+ ".vue",
45
+ ".svelte",
46
+ ".astro",
47
+ // Python
48
+ ".py",
49
+ ".pyi",
50
+ // Go
51
+ ".go",
52
+ // Rust
53
+ ".rs",
54
+ // C/C++
55
+ ".c",
56
+ ".cpp",
57
+ ".cc",
58
+ ".cxx",
59
+ ".h",
60
+ ".hpp",
61
+ ".hh",
62
+ ".hxx",
63
+ // Java/Kotlin
64
+ ".java",
65
+ ".kt",
66
+ ".kts",
67
+ // C#/F#
68
+ ".cs",
69
+ ".fs",
70
+ ".fsi",
71
+ ".fsx",
72
+ // Ruby
73
+ ".rb",
74
+ ".rake",
75
+ ".gemspec",
76
+ // PHP
77
+ ".php",
78
+ // Elixir
79
+ ".ex",
80
+ ".exs",
81
+ // Clojure
82
+ ".clj",
83
+ ".cljs",
84
+ ".cljc",
85
+ // Shell
86
+ ".sh",
87
+ ".bash",
88
+ ".zsh",
89
+ // Swift/Objective-C
90
+ ".swift",
91
+ ".m",
92
+ ".mm",
93
+ // Other
94
+ ".lua",
95
+ ".dart",
96
+ ".gleam",
97
+ ".nix",
98
+ ".ml",
99
+ ".mli",
100
+ ".zig",
101
+ ".zon",
102
+ ".prisma",
103
+ ".tf",
104
+ ".tfvars",
105
+ ".typ",
106
+ ".yaml",
107
+ ".yml",
108
+ ];
33
109
  const DEBOUNCE_MS = 30000; // 30 seconds
34
110
  const CODE_CHANGE_DEBOUNCE_MS = 10000; // 10 seconds for toast responsiveness
35
111
 
@@ -2,6 +2,6 @@
2
2
  "name": "opencode-plugins",
3
3
  "type": "module",
4
4
  "dependencies": {
5
- "@opencode-ai/plugin": "^1.1.2"
5
+ "@opencode-ai/plugin": "^1.1.8"
6
6
  }
7
7
  }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Ralph Wiggum Plugin for OpenCode
3
+ *
4
+ * Handles the session.idle event to continue the Ralph loop.
5
+ * Tools are defined separately in .opencode/tool/ralph.ts
6
+ *
7
+ * Based on: https://ghuntley.com/ralph/
8
+ */
9
+
10
+ import fs from "node:fs/promises";
11
+ import type { Plugin } from "@opencode-ai/plugin";
12
+
13
+ const STATE_FILE = ".opencode/.ralph-state.json";
14
+ const IDLE_DEBOUNCE_MS = 2000;
15
+ let lastIdleTime = 0;
16
+
17
+ interface RalphState {
18
+ active: boolean;
19
+ sessionID: string | null;
20
+ iteration: number;
21
+ maxIterations: number;
22
+ completionPromise: string;
23
+ task: string;
24
+ prdFile: string | null;
25
+ progressFile: string;
26
+ startedAt: number | null;
27
+ mode: "hitl" | "afk";
28
+ }
29
+
30
+ async function loadState(): Promise<RalphState | null> {
31
+ try {
32
+ const content = await fs.readFile(STATE_FILE, "utf-8");
33
+ return JSON.parse(content);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ async function saveState(state: RalphState): Promise<void> {
40
+ await fs.writeFile(STATE_FILE, JSON.stringify(state, null, 2));
41
+ }
42
+
43
+ async function resetState(): Promise<void> {
44
+ try {
45
+ await fs.unlink(STATE_FILE);
46
+ } catch {
47
+ // File doesn't exist, that's fine
48
+ }
49
+ }
50
+
51
+ export const RalphWiggum: Plugin = async ({ client }) => {
52
+ const log = async (
53
+ message: string,
54
+ level: "info" | "warn" | "error" = "info",
55
+ ) => {
56
+ await client.app
57
+ .log({
58
+ body: { service: "ralph-wiggum", level, message },
59
+ })
60
+ .catch(() => {});
61
+ };
62
+
63
+ const showToast = async (
64
+ title: string,
65
+ message: string,
66
+ variant: "info" | "success" | "warning" | "error" = "info",
67
+ ) => {
68
+ await client.tui
69
+ .showToast({
70
+ body: {
71
+ title: `Ralph: ${title}`,
72
+ message,
73
+ variant,
74
+ duration: variant === "error" ? 8000 : 5000,
75
+ },
76
+ })
77
+ .catch(() => {});
78
+ };
79
+
80
+ const buildContinuationPrompt = (state: RalphState): string => {
81
+ const prdRef = state.prdFile ? `@${state.prdFile} ` : "";
82
+ const progressRef = `@${state.progressFile}`;
83
+
84
+ return `
85
+ ${prdRef}${progressRef}
86
+
87
+ ## Ralph Wiggum Loop - Iteration ${state.iteration}/${state.maxIterations}
88
+
89
+ You are in an autonomous loop. Continue working on the task.
90
+
91
+ **Task:** ${state.task}
92
+
93
+ **Instructions:**
94
+ 1. Review the PRD/task list and progress file
95
+ 2. Choose the highest-priority INCOMPLETE task
96
+ 3. Implement ONE feature/change only
97
+ 4. Run feedback loops: typecheck, test, lint
98
+ 5. Commit if all pass
99
+ 6. Update ${state.progressFile}
100
+ 7. If ALL tasks complete, output: ${state.completionPromise}
101
+
102
+ **Constraints:** ONE feature per iteration. Quality over speed.
103
+ `.trim();
104
+ };
105
+
106
+ const handleSessionIdle = async (sessionID: string): Promise<void> => {
107
+ const now = Date.now();
108
+ if (now - lastIdleTime < IDLE_DEBOUNCE_MS) return;
109
+ lastIdleTime = now;
110
+
111
+ const state = await loadState();
112
+ if (!state?.active || state.sessionID !== sessionID) return;
113
+
114
+ try {
115
+ const messagesResponse = await client.session.messages({
116
+ path: { id: sessionID },
117
+ });
118
+ const messages = messagesResponse.data || [];
119
+ const lastMessage = messages[messages.length - 1];
120
+
121
+ const lastText =
122
+ lastMessage?.parts
123
+ ?.filter((p) => p.type === "text")
124
+ .map((p) => ("text" in p ? (p.text as string) : ""))
125
+ .join("") || "";
126
+
127
+ if (lastText.includes(state.completionPromise)) {
128
+ const duration = state.startedAt
129
+ ? Math.round((Date.now() - state.startedAt) / 1000 / 60)
130
+ : 0;
131
+ await showToast(
132
+ "Complete!",
133
+ `Finished in ${state.iteration} iterations (${duration} min)`,
134
+ "success",
135
+ );
136
+ await log(`Loop completed in ${state.iteration} iterations`);
137
+ await resetState();
138
+ return;
139
+ }
140
+
141
+ state.iteration++;
142
+ if (state.iteration >= state.maxIterations) {
143
+ await showToast(
144
+ "Stopped",
145
+ `Max iterations (${state.maxIterations}) reached`,
146
+ "warning",
147
+ );
148
+ await log(`Max iterations reached: ${state.maxIterations}`, "warn");
149
+ await resetState();
150
+ return;
151
+ }
152
+
153
+ await saveState(state);
154
+
155
+ await client.session.prompt({
156
+ path: { id: sessionID },
157
+ body: {
158
+ parts: [{ type: "text", text: buildContinuationPrompt(state) }],
159
+ },
160
+ });
161
+
162
+ await log(`Iteration ${state.iteration}/${state.maxIterations}`);
163
+ } catch (error) {
164
+ await log(`Error in Ralph loop: ${error}`, "error");
165
+ await resetState();
166
+ }
167
+ };
168
+
169
+ return {
170
+ event: async ({ event }) => {
171
+ if (event.type === "session.idle") {
172
+ const sessionID = (event as { properties?: { sessionID?: string } })
173
+ .properties?.sessionID;
174
+ if (sessionID) {
175
+ await handleSessionIdle(sessionID);
176
+ }
177
+ }
178
+ },
179
+ };
180
+ };
181
+
182
+ export default RalphWiggum;