mini-coder 0.7.4 → 0.8.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 (49) hide show
  1. package/AGENTS.md +114 -0
  2. package/README.md +53 -66
  3. package/bin/mini-coder.ts +2 -0
  4. package/demo.gif +0 -0
  5. package/package.json +17 -20
  6. package/src/agent.ts +193 -272
  7. package/src/auth.ts +84 -0
  8. package/src/cli.ts +99 -0
  9. package/src/config.ts +179 -0
  10. package/src/prompt.ts +54 -207
  11. package/src/session.ts +124 -69
  12. package/src/tools/bash.ts +86 -0
  13. package/src/tools/common.ts +32 -0
  14. package/src/tools/edit.ts +41 -0
  15. package/src/tools/index.ts +47 -0
  16. package/src/tools/read.ts +64 -0
  17. package/src/tui/commands.ts +199 -0
  18. package/src/tui/complete.ts +85 -0
  19. package/src/tui/editor.ts +320 -0
  20. package/src/tui/highlight.ts +189 -0
  21. package/src/tui/stream.ts +142 -0
  22. package/src/tui/styles.ts +20 -0
  23. package/src/tui/term.ts +436 -0
  24. package/src/tui/theme.ts +120 -0
  25. package/src/tui/tui.ts +758 -0
  26. package/src/tui/usage.ts +67 -0
  27. package/tsconfig.json +8 -8
  28. package/bin/mc.ts +0 -11
  29. package/bun.lock +0 -350
  30. package/nono-mini-coder.json +0 -42
  31. package/src/args.ts +0 -252
  32. package/src/error-handling.test.ts +0 -163
  33. package/src/git.ts +0 -23
  34. package/src/headless.ts +0 -66
  35. package/src/index.ts +0 -43
  36. package/src/models.ts +0 -191
  37. package/src/oauth.ts +0 -147
  38. package/src/shared.ts +0 -119
  39. package/src/themes.ts +0 -234
  40. package/src/tool-bash.ts +0 -77
  41. package/src/tool-edit.ts +0 -121
  42. package/src/tool-read.ts +0 -100
  43. package/src/tui-components.ts +0 -127
  44. package/src/tui-conversation.ts +0 -218
  45. package/src/tui-editor.ts +0 -29
  46. package/src/tui-overlay.ts +0 -604
  47. package/src/tui.ts +0 -314
  48. package/src/types.ts +0 -194
  49. package/src/update.ts +0 -171
package/src/cli.ts ADDED
@@ -0,0 +1,99 @@
1
+ import process from "node:process";
2
+ import { clampThinkingLevel, type AssistantMessage, type Message, type UserMessage } from "@earendil-works/pi-ai";
3
+ import { loadConfig, resolveModel } from "./config.ts";
4
+ import { buildSystemPrompt } from "./prompt.ts";
5
+ import { acceptsImages, toolSchemas } from "./tools/index.ts";
6
+ import { Session } from "./session.ts";
7
+ import { NO_INTERACTION, assistantText, runAgentTurn, type AgentOptions } from "./agent.ts";
8
+ import { runTui } from "./tui/tui.ts";
9
+
10
+ function parseArgs(argv: string[]): { print: string | null } {
11
+ let print: string | null = null;
12
+ for (let i = 0; i < argv.length; i++) {
13
+ const match = /^(?:-p|--print)(?:=(.*))?$/.exec(argv[i]);
14
+ if (match === null) throw new Error(`unknown argument: ${argv[i]}`);
15
+ const value = match[1] ?? argv[++i];
16
+ if (value === undefined) throw new Error(`${argv[i - 1]} requires a prompt`);
17
+ print = value;
18
+ }
19
+ return { print };
20
+ }
21
+
22
+ async function runPrint(prompt: string, ctx: AgentOptions): Promise<number> {
23
+ const messages: Message[] = [];
24
+ const user: UserMessage = { role: "user", content: prompt, timestamp: Date.now() };
25
+ messages.push(user);
26
+ ctx.session.appendMessage(user);
27
+
28
+ const controller = new AbortController();
29
+ const onSignal = () => controller.abort();
30
+ process.on("SIGINT", onSignal);
31
+ process.on("SIGTERM", onSignal);
32
+
33
+ let failed = false;
34
+ let cancelled = false;
35
+ try {
36
+ await runAgentTurn({
37
+ ...ctx,
38
+ messages,
39
+ signal: controller.signal,
40
+ interaction: NO_INTERACTION,
41
+ onEvent: (event) => {
42
+ if (event.type === "toolCall") process.stderr.write(`[tool] ${event.name}\n`);
43
+ else if (event.type === "toolOutput") process.stderr.write(event.chunk);
44
+ else if (event.type === "error") {
45
+ failed = true;
46
+ process.stderr.write(`[error] ${event.message}\n`);
47
+ } else if (event.type === "cancelled") {
48
+ cancelled = true;
49
+ process.stderr.write("[cancelled]\n");
50
+ }
51
+ },
52
+ });
53
+ } catch (error) {
54
+ failed = true;
55
+ process.stderr.write(`[error] ${(error as Error).message}\n`);
56
+ } finally {
57
+ process.off("SIGINT", onSignal);
58
+ process.off("SIGTERM", onSignal);
59
+ ctx.session.close();
60
+ }
61
+
62
+ const last = messages.filter((message): message is AssistantMessage => message.role === "assistant").at(-1);
63
+ if (!failed && !cancelled && last !== undefined) {
64
+ const text = assistantText(last);
65
+ if (text !== "") process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
66
+ }
67
+ return failed || cancelled ? 1 : 0;
68
+ }
69
+
70
+ async function main(): Promise<void> {
71
+ const args = parseArgs(process.argv.slice(2));
72
+ const config = loadConfig();
73
+ const { models, model } = resolveModel(config);
74
+ const session = new Session(config.sessionsDir, process.cwd());
75
+ const ctx: AgentOptions = {
76
+ models,
77
+ model,
78
+ systemPrompt: buildSystemPrompt(config),
79
+ tools: toolSchemas(config.tools, acceptsImages(model)),
80
+ toolNames: config.tools,
81
+ thinkingEffort: clampThinkingLevel(model, config.thinkingEffort),
82
+ session,
83
+ };
84
+
85
+ if (args.print !== null) {
86
+ process.exitCode = await runPrint(args.print, ctx);
87
+ return;
88
+ }
89
+
90
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
91
+ throw new Error("interactive mode requires a TTY; use -p for non-interactive mode");
92
+ }
93
+ await runTui(ctx);
94
+ }
95
+
96
+ main().catch((error) => {
97
+ process.stderr.write(`${(error as Error).message}\n`);
98
+ process.exit(1);
99
+ });
package/src/config.ts ADDED
@@ -0,0 +1,179 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import {
5
+ createProvider,
6
+ envApiKeyAuth,
7
+ Type,
8
+ type Api,
9
+ type Model,
10
+ type ModelThinkingLevel,
11
+ type MutableModels,
12
+ type Static,
13
+ } from "@earendil-works/pi-ai";
14
+ import { Value } from "typebox/value";
15
+ import { builtinModels } from "@earendil-works/pi-ai/providers/all";
16
+ import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
17
+ import { googleGenerativeAIApi } from "@earendil-works/pi-ai/api/google-generative-ai.lazy";
18
+ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
19
+ import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
20
+ import { createCredentialStore } from "./auth.ts";
21
+
22
+ const ToolNameSchema = Type.Union([Type.Literal("edit"), Type.Literal("read"), Type.Literal("bash")]);
23
+ export type ToolName = Static<typeof ToolNameSchema>;
24
+
25
+ const CustomApiSchema = Type.Union([
26
+ Type.Literal("openai-completions"),
27
+ Type.Literal("openai-responses"),
28
+ Type.Literal("anthropic-messages"),
29
+ Type.Literal("google-generative-ai"),
30
+ ]);
31
+ type CustomApi = Static<typeof CustomApiSchema>;
32
+
33
+ const CustomProviderSchema = Type.Object(
34
+ {
35
+ id: Type.String({ minLength: 1 }),
36
+ name: Type.Optional(Type.String()),
37
+ baseUrl: Type.String({ minLength: 1 }),
38
+ api: CustomApiSchema,
39
+ models: Type.Array(Type.String(), { minItems: 1 }),
40
+ envKeys: Type.Optional(Type.Array(Type.String())),
41
+ headers: Type.Optional(Type.Record(Type.String(), Type.String())),
42
+ },
43
+ { additionalProperties: false },
44
+ );
45
+
46
+ const ConfigSchema = Type.Object(
47
+ {
48
+ sessionsDir: Type.String({ default: join(process.cwd(), "sessions") }),
49
+ authFile: Type.String({ default: join(configDir(), "auth.json") }),
50
+ systemPrompt: Type.String({ default: "" }),
51
+ discoverAgentFiles: Type.Boolean({ default: true }),
52
+ skillsDirs: Type.Array(Type.String(), { default: [] }),
53
+ tools: Type.Array(ToolNameSchema, { default: ["edit", "read", "bash"] }),
54
+ provider: Type.String(),
55
+ model: Type.String(),
56
+ thinkingEffort: Type.Union(
57
+ [
58
+ Type.Literal("off"),
59
+ Type.Literal("minimal"),
60
+ Type.Literal("low"),
61
+ Type.Literal("medium"),
62
+ Type.Literal("high"),
63
+ Type.Literal("xhigh"),
64
+ Type.Literal("max"),
65
+ ],
66
+ { default: "medium" },
67
+ ),
68
+ customProviders: Type.Array(CustomProviderSchema, { default: [] }),
69
+ },
70
+ { additionalProperties: false },
71
+ );
72
+ export type Config = Static<typeof ConfigSchema>;
73
+
74
+ function configDir(): string {
75
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
76
+ return join(base, "mini-coder");
77
+ }
78
+
79
+ function configPath(): string {
80
+ return join(configDir(), "config.json");
81
+ }
82
+
83
+ function readJson(path: string): unknown {
84
+ let text: string;
85
+ try {
86
+ text = readFileSync(path, "utf8");
87
+ } catch (error) {
88
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
89
+ throw new Error(`config ${path}: ${(error as Error).message}`);
90
+ }
91
+ try {
92
+ return JSON.parse(text);
93
+ } catch (error) {
94
+ throw new Error(`config ${path}: ${(error as Error).message}`);
95
+ }
96
+ }
97
+
98
+ export function loadConfig(): Config {
99
+ const path = configPath();
100
+ const filled = Value.Default(ConfigSchema, readJson(path));
101
+ try {
102
+ return Value.Parse(ConfigSchema, filled);
103
+ } catch {
104
+ const first = [...Value.Errors(ConfigSchema, filled)][0];
105
+ throw new Error(`config ${path}: ${first ? `${first.instancePath || "/"} ${first.message}` : "invalid"}`);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Persist a patch to the global config, leaving every other key untouched.
111
+ * Written atomically: a temp file, then a rename over the target.
112
+ */
113
+ export function saveConfig(patch: {
114
+ provider?: string;
115
+ model?: string;
116
+ thinkingEffort?: ModelThinkingLevel;
117
+ }): void {
118
+ const path = configPath();
119
+ const existing = readJson(path);
120
+ if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
121
+ throw new Error(`config ${path}: not an object`);
122
+ }
123
+ const next = { ...existing, ...patch };
124
+ mkdirSync(dirname(path), { recursive: true });
125
+ const temp = join(dirname(path), `.${process.pid}.${Date.now()}.tmp`);
126
+ writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`);
127
+ renameSync(temp, path);
128
+ }
129
+
130
+ const API_FACTORY: Record<CustomApi, () => ReturnType<typeof openAICompletionsApi>> = {
131
+ "openai-completions": openAICompletionsApi,
132
+ "openai-responses": openAIResponsesApi,
133
+ "anthropic-messages": anthropicMessagesApi,
134
+ "google-generative-ai": googleGenerativeAIApi,
135
+ };
136
+
137
+ export function resolveModel(config: Config): { models: MutableModels; model: Model<Api> } {
138
+ const models = builtinModels({ credentials: createCredentialStore(config.authFile) });
139
+ for (const provider of config.customProviders) {
140
+ const name = provider.name ?? provider.id;
141
+ models.setProvider(
142
+ createProvider({
143
+ id: provider.id,
144
+ name,
145
+ baseUrl: provider.baseUrl,
146
+ headers: provider.headers,
147
+ auth: {
148
+ apiKey: provider.envKeys?.length
149
+ ? envApiKeyAuth(name, provider.envKeys)
150
+ : { name, resolve: async () => ({ auth: { apiKey: "unused" } }) },
151
+ },
152
+ api: API_FACTORY[provider.api](),
153
+ models: provider.models.map((id) => ({
154
+ id,
155
+ name: id,
156
+ api: provider.api,
157
+ provider: provider.id,
158
+ baseUrl: provider.baseUrl,
159
+ reasoning: false,
160
+ input: ["text"] satisfies ("text" | "image")[],
161
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
162
+ contextWindow: 200_000,
163
+ maxTokens: 32_768,
164
+ })),
165
+ }),
166
+ );
167
+ }
168
+
169
+ const model = models.getModel(config.provider, config.model);
170
+ if (!model) {
171
+ const known = models
172
+ .getModels(config.provider)
173
+ .map((m) => m.id)
174
+ .slice(0, 12);
175
+ const hint = known.length ? ` (available: ${known.join(", ")})` : "";
176
+ throw new Error(`config model: unknown model "${config.model}" for provider "${config.provider}"${hint}`);
177
+ }
178
+ return { models, model };
179
+ }
package/src/prompt.ts CHANGED
@@ -1,232 +1,79 @@
1
- import { promises } from "node:fs";
2
- import { readdir } from "node:fs/promises";
3
- import { homedir, platform } from "node:os";
4
- import { join } from "node:path";
5
- import type { Message, ToolResultMessage } from "@earendil-works/pi-ai";
6
- import { getGitStatus } from "./git";
7
- import { parseSkillFrontmatter } from "./shared";
8
-
9
- export const MAIN_PROMPT = `You are a coding agent interacting with users via the mini-coder harness. You help users by reading files, executing commands, and editting code.
10
-
11
- - Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without unnecessary superlatives, praise, or emotional validation.
12
- - User messages and Tool results may include <system-reminder> tags. These contain system-generated reminders and bear no direct relation to the specific tool result in which they appear.
13
- - You have access to bash, read and edit tools. Prefer using read and edit for file operations, use bash for finding read candidates or to run development commands.
14
- - Use recent online information, the current environment, and your training data combined for a complete answer.
15
- - Ensure that you fulfill the user's expectation, requirements and contract **exactly**.
16
- - Do not overstate what changed or what was verified. Summaries must match the diff.
17
- - Use temp directory for temp files, scripts, plan files, or anything that doesn't match the requested output.
18
- - Be concise. Use a professional colleague tone: direct, never condescending, and never rude.
19
- `;
20
-
21
- async function getDir() {
22
- const ignoreFile = Bun.file(".gitignore");
23
- let ignoreContent = "";
24
- if (await ignoreFile.exists()) {
25
- ignoreContent = await ignoreFile.text();
26
- }
27
- const ignored = ignoreContent.split("\n");
28
- const dir = [];
29
- const glob = promises.glob(["*", "*/*"], { exclude: ignored });
30
- for await (const file of glob) {
31
- dir.push(file);
32
- }
33
- return dir;
34
- }
35
-
36
- async function getEnvPrompt() {
37
- // TODO: What else do the agents always check before answering every time?
38
- const gitStatus = await getGitStatus();
39
- const envKeys = ["PATH", "USER", "LANG", "HOME", "SHELL", "BUN_INSTALL"];
40
- const env: Record<string, string> = {};
41
- for (const key of envKeys) {
42
- const v = Bun.env[key];
43
-
44
- if (v !== undefined) {
45
- env[key] = v;
46
- }
47
- }
48
-
49
- const envStatus = JSON.stringify(
50
- {
51
- os: platform(),
52
- env,
53
- cwd: process.cwd(),
54
- dir: await getDir(),
55
- git: gitStatus,
56
- },
57
- null,
58
- 4,
59
- );
60
-
61
- const text = `### Environment status and information
62
-
63
- \`\`\`json
64
- ${envStatus}
65
- \`\`\`
66
- `;
67
-
68
- return text;
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import type { Config } from "./config.ts";
5
+
6
+ interface Skill {
7
+ name: string;
8
+ description: string;
9
+ path: string;
69
10
  }
70
11
 
71
- // `AGENTS.md` support: find it in current folder (./AGENTS.md) and a global one. (`.agents/AGENTS.md`)
72
- export async function getAGENTSFiles() {
73
- const content: string[] = [];
74
-
75
- const globalPath = join(homedir(), ".agents/AGENTS.md");
76
- const globalFile = Bun.file(globalPath);
77
-
78
- if (await globalFile.exists()) {
79
- content.push(await globalFile.text());
80
- }
81
-
82
- const localPath = join(process.cwd(), "AGENTS.md");
83
- const localFile = Bun.file(localPath);
84
-
85
- if (await localFile.exists()) {
86
- content.push(await localFile.text());
12
+ function frontmatter(text: string): { name?: string; description?: string } {
13
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
14
+ if (!match) return {};
15
+ const out: { name?: string; description?: string } = {};
16
+ for (const line of match[1].split(/\r?\n/)) {
17
+ const separator = line.indexOf(":");
18
+ if (separator < 0) continue;
19
+ const key = line.slice(0, separator).trim();
20
+ const value = line.slice(separator + 1).trim().replace(/^["']|["']$/g, "");
21
+ if (key === "name") out.name = value;
22
+ if (key === "description") out.description = value;
87
23
  }
88
-
89
- return content.join("\n\n").trim();
24
+ return out;
90
25
  }
91
26
 
92
- // `SKILLS.md` discovery from [~|.]/agents/skills/*/SKILL.md
93
- export async function getSkills(): Promise<string> {
94
- let skillsBlock: string = "";
95
- const skillRoots = [
96
- join(homedir(), ".agents", "skills"),
97
- join(process.cwd(), ".agents", "skills"),
98
- ];
99
-
100
- for (const root of skillRoots) {
27
+ function discoverSkills(dirs: string[]): Skill[] {
28
+ const skills: Skill[] = [];
29
+ for (const root of dirs) {
101
30
  let entries: string[];
102
-
103
31
  try {
104
- entries = await readdir(root);
32
+ entries = readdirSync(root);
105
33
  } catch {
106
34
  continue;
107
35
  }
108
-
109
36
  for (const entry of entries) {
110
- const path = join(root, entry, "SKILL.md");
111
- const file = Bun.file(path);
112
-
113
- if (!(await file.exists())) {
114
- continue;
115
- }
116
-
117
- const parsed = parseSkillFrontmatter(await file.text());
118
-
119
- if (!parsed) {
120
- continue;
121
- }
122
-
123
- skillsBlock += `## ${parsed.name}
124
-
125
- > Absolute file path to read: ${path}
126
-
127
- ${parsed.description}
128
-
129
- `;
37
+ const path = resolve(root, entry, "SKILL.md");
38
+ if (!existsSync(path)) continue;
39
+ const meta = frontmatter(readFileSync(path, "utf8"));
40
+ if (!meta.name) continue;
41
+ skills.push({ name: meta.name, description: meta.description ?? "", path });
130
42
  }
131
43
  }
132
-
133
- if (!skillsBlock.length) return "";
134
-
135
- const skills = `# Skills
136
-
137
- - The following skills provide specialized instructions for specific tasks.
138
- - Use the bash tool to read a skill's file when the task matches its description.
139
- - Use the skill provided absolute file path instead of guessing or constructing one.
140
- - Skills can be global (in ~/.agents/skills) or local to the directory (./agents/skills)
141
-
142
- ${skillsBlock}`;
143
-
144
- return skills.trim();
44
+ return skills;
145
45
  }
146
46
 
147
- export async function buildSystemPrompt(systemPrompt: string) {
148
- const agentsContent = await getAGENTSFiles();
149
- const skillsContent = await getSkills();
150
- let complete = systemPrompt;
151
-
152
- if (skillsContent) {
153
- complete += `\n${skillsContent}`;
154
- }
155
-
156
- if (agentsContent) {
157
- complete += `\n${agentsContent}`;
47
+ function agentFiles(): { path: string; text: string }[] {
48
+ const roots = [join(homedir(), ".agents"), process.cwd()];
49
+ const found: { path: string; text: string }[] = [];
50
+ for (const root of roots) {
51
+ for (const name of ["AGENTS.md", "CLAUDE.md"]) {
52
+ const path = join(root, name);
53
+ if (existsSync(path)) found.push({ path, text: readFileSync(path, "utf8").trim() });
54
+ }
158
55
  }
159
-
160
- return complete;
161
- }
162
-
163
- export async function injectEnvReminder(): Promise<string> {
164
- const envStatus = await getEnvPrompt();
165
- return `<system-reminder>\n${envStatus}\n</system-reminder>`;
56
+ return found;
166
57
  }
167
58
 
168
- const doomLoopReminder = `<system-reminder>
169
- You may be entering a tool-call doom loop: recent tool usage is repetitive or not clearly progressing.
59
+ export function buildSystemPrompt(config: Config): string {
60
+ const sections: string[] = [];
61
+ if (config.systemPrompt.trim()) sections.push(config.systemPrompt.trim());
170
62
 
171
- - Stop repeating the same or similar tool call unless new evidence requires it.
172
- - Re-read the user's request and summarize what is known, what failed, and what is still needed.
173
- - Change strategy before calling more tools: narrow the next check, use a different source of evidence, or ask the user if blocked.
174
- - If the request is complete, stop calling tools and provide the final answer.
175
- </system-reminder>`;
176
-
177
- // Checks recent tool usage for simple repeated-call patterns and inserts an
178
- // anti-doom-loop reminder when the agent appears stuck.
179
- export function insertToolUsageReminder(
180
- messages: Message[],
181
- toolMessage: ToolResultMessage,
182
- ): ToolResultMessage {
183
- const lastReminderIdx = messages.findLastIndex((m) => {
184
- return (
185
- m.role === "toolResult" &&
186
- m.content.find(
187
- (b) => b.type === "text" && b.text.includes(doomLoopReminder),
188
- )
189
- );
190
- });
191
- const lastUserMessageIdx = messages.findLastIndex((m) => {
192
- return m.role === "user";
193
- });
194
- const messagesSinceLast = messages.slice(
195
- Math.max(lastReminderIdx, lastUserMessageIdx) + 1,
196
- );
197
- const minMessagesForReminder = 4;
198
-
199
- if (messagesSinceLast.length < minMessagesForReminder) return toolMessage;
200
-
201
- let errorCount = 0;
202
- let sameToolCount = 0;
203
- const seenArgs = new Set<string>();
204
-
205
- for (const msg of messagesSinceLast) {
206
- if (msg.role === "toolResult" && msg.isError) errorCount++;
207
-
208
- if (msg.role === "assistant") {
209
- const calls = msg.content.filter((b) => b.type === "toolCall");
210
- for (const c of calls) {
211
- const args = JSON.stringify(c.arguments);
212
- if (seenArgs.has(args)) sameToolCount++;
213
- seenArgs.add(args);
214
- }
63
+ if (config.skillsDirs.length > 0) {
64
+ const skills = discoverSkills(config.skillsDirs);
65
+ if (skills.length > 0) {
66
+ sections.push(
67
+ "## Skills\n\n" + skills.map((s) => `- ${s.name}: ${s.description} (${s.path})`).join("\n"),
68
+ );
215
69
  }
216
70
  }
217
71
 
218
- if (
219
- errorCount >= minMessagesForReminder ||
220
- sameToolCount >= minMessagesForReminder
221
- ) {
222
- return {
223
- ...toolMessage,
224
- content: [
225
- ...toolMessage.content,
226
- { type: "text", text: doomLoopReminder },
227
- ],
228
- };
72
+ if (config.discoverAgentFiles) {
73
+ for (const file of agentFiles()) {
74
+ sections.push(`## ${file.path}\n\n${file.text}`);
75
+ }
229
76
  }
230
77
 
231
- return toolMessage;
78
+ return sections.join("\n\n");
232
79
  }