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.
- package/AGENTS.md +114 -0
- package/README.md +53 -66
- package/bin/mini-coder.ts +2 -0
- package/demo.gif +0 -0
- package/package.json +17 -20
- package/src/agent.ts +193 -272
- package/src/auth.ts +84 -0
- package/src/cli.ts +99 -0
- package/src/config.ts +179 -0
- package/src/prompt.ts +54 -207
- package/src/session.ts +124 -69
- package/src/tools/bash.ts +86 -0
- package/src/tools/common.ts +32 -0
- package/src/tools/edit.ts +41 -0
- package/src/tools/index.ts +47 -0
- package/src/tools/read.ts +64 -0
- package/src/tui/commands.ts +199 -0
- package/src/tui/complete.ts +85 -0
- package/src/tui/editor.ts +320 -0
- package/src/tui/highlight.ts +189 -0
- package/src/tui/stream.ts +142 -0
- package/src/tui/styles.ts +20 -0
- package/src/tui/term.ts +436 -0
- package/src/tui/theme.ts +120 -0
- package/src/tui/tui.ts +758 -0
- package/src/tui/usage.ts +67 -0
- package/tsconfig.json +8 -8
- package/bin/mc.ts +0 -11
- package/bun.lock +0 -350
- package/nono-mini-coder.json +0 -42
- package/src/args.ts +0 -252
- package/src/error-handling.test.ts +0 -163
- package/src/git.ts +0 -23
- package/src/headless.ts +0 -66
- package/src/index.ts +0 -43
- package/src/models.ts +0 -191
- package/src/oauth.ts +0 -147
- package/src/shared.ts +0 -119
- package/src/themes.ts +0 -234
- package/src/tool-bash.ts +0 -77
- package/src/tool-edit.ts +0 -121
- package/src/tool-read.ts +0 -100
- package/src/tui-components.ts +0 -127
- package/src/tui-conversation.ts +0 -218
- package/src/tui-editor.ts +0 -29
- package/src/tui-overlay.ts +0 -604
- package/src/tui.ts +0 -314
- package/src/types.ts +0 -194
- 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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 =
|
|
32
|
+
entries = readdirSync(root);
|
|
105
33
|
} catch {
|
|
106
34
|
continue;
|
|
107
35
|
}
|
|
108
|
-
|
|
109
36
|
for (const entry of entries) {
|
|
110
|
-
const path =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!
|
|
114
|
-
|
|
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
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
169
|
-
|
|
59
|
+
export function buildSystemPrompt(config: Config): string {
|
|
60
|
+
const sections: string[] = [];
|
|
61
|
+
if (config.systemPrompt.trim()) sections.push(config.systemPrompt.trim());
|
|
170
62
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
|
78
|
+
return sections.join("\n\n");
|
|
232
79
|
}
|