min-agent 0.1.4 → 0.1.6
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/README.md +95 -194
- package/bin/min-agent.js +0 -0
- package/dist/agent.js +431 -28
- package/dist/cli.js +177 -27
- package/dist/clipboard.js +106 -0
- package/dist/code-mode.js +166 -0
- package/dist/compaction.js +243 -48
- package/dist/config.js +34 -7
- package/dist/context-window.js +185 -0
- package/dist/doom-loop.js +36 -0
- package/dist/instructions.js +42 -0
- package/dist/mcp.js +82 -28
- package/dist/output.js +15 -2
- package/dist/paste-handler.js +41 -0
- package/dist/serve.js +351 -3
- package/dist/sessions.js +13 -4
- package/dist/skills.js +15 -7
- package/dist/structured-output.js +29 -0
- package/dist/title-gen.js +48 -0
- package/dist/tools/bash.js +85 -74
- package/dist/tools/code_search.js +91 -0
- package/dist/tools/explore.js +104 -0
- package/dist/tools/index.js +10 -1
- package/dist/tools/question.js +53 -0
- package/dist/tools/read.js +14 -3
- package/dist/tools/task.js +98 -0
- package/dist/tools/todo.js +88 -0
- package/docs/API.md +219 -123
- package/package.json +1 -1
package/dist/skills.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
|
3
3
|
import os from "os";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { globSync } from "glob";
|
|
6
|
+
import { loadConfig } from "./config.js";
|
|
6
7
|
/**
|
|
7
8
|
* Skill scan order: later entries win on duplicate `name` in frontmatter.
|
|
8
9
|
* Global user skills first, then project-local dirs so repo skills override ~/.agents.
|
|
@@ -15,7 +16,7 @@ const SKILL_DIRS = [
|
|
|
15
16
|
path.join(process.cwd(), ".claude", "skills"),
|
|
16
17
|
];
|
|
17
18
|
let loadedSkills = {};
|
|
18
|
-
export function discoverSkills() {
|
|
19
|
+
export function discoverSkills(opts) {
|
|
19
20
|
loadedSkills = {};
|
|
20
21
|
for (const dir of SKILL_DIRS) {
|
|
21
22
|
if (!existsSync(dir))
|
|
@@ -29,8 +30,10 @@ export function discoverSkills() {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
const count = Object.keys(loadedSkills).length;
|
|
32
|
-
if (count > 0) {
|
|
33
|
-
|
|
33
|
+
if (count > 0 && !opts?.silent) {
|
|
34
|
+
const disabled = new Set(loadConfig().disabledSkills ?? []);
|
|
35
|
+
const enabledCount = Object.keys(loadedSkills).filter((n) => !disabled.has(n)).length;
|
|
36
|
+
console.log(`\x1b[90m Skills: ${enabledCount} enabled${count > enabledCount ? `, ${count - enabledCount} disabled` : ""}\x1b[0m`);
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
function parseSkillFile(filePath) {
|
|
@@ -58,7 +61,8 @@ function parseSkillFile(filePath) {
|
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
export function getSkills() {
|
|
61
|
-
|
|
64
|
+
const disabled = new Set(loadConfig().disabledSkills ?? []);
|
|
65
|
+
return Object.values(loadedSkills).filter((s) => !disabled.has(s.name));
|
|
62
66
|
}
|
|
63
67
|
export function getSkill(name) {
|
|
64
68
|
return loadedSkills[name];
|
|
@@ -74,9 +78,13 @@ export function getSkillsTool() {
|
|
|
74
78
|
required: ["name"],
|
|
75
79
|
}),
|
|
76
80
|
execute: async ({ name }) => {
|
|
81
|
+
const disabled = new Set(loadConfig().disabledSkills ?? []);
|
|
82
|
+
if (disabled.has(name)) {
|
|
83
|
+
return `Skill "${name}" is disabled. Available skills: ${getSkills().map((s) => s.name).join(", ") || "none"}`;
|
|
84
|
+
}
|
|
77
85
|
const skill = loadedSkills[name];
|
|
78
86
|
if (!skill) {
|
|
79
|
-
const available =
|
|
87
|
+
const available = getSkills().map((s) => s.name);
|
|
80
88
|
return `Skill "${name}" not found. Available skills: ${available.length ? available.join(", ") : "none"}`;
|
|
81
89
|
}
|
|
82
90
|
const dir = path.dirname(skill.location);
|
|
@@ -104,7 +112,7 @@ export function getSkillsTool() {
|
|
|
104
112
|
});
|
|
105
113
|
}
|
|
106
114
|
export function getSkillsSystemPrompt() {
|
|
107
|
-
const skills =
|
|
115
|
+
const skills = getSkills();
|
|
108
116
|
if (skills.length === 0)
|
|
109
117
|
return "";
|
|
110
118
|
return [
|
|
@@ -115,7 +123,7 @@ export function getSkillsSystemPrompt() {
|
|
|
115
123
|
].join("\n");
|
|
116
124
|
}
|
|
117
125
|
function buildSkillDescription() {
|
|
118
|
-
const skills =
|
|
126
|
+
const skills = getSkills();
|
|
119
127
|
if (skills.length === 0)
|
|
120
128
|
return "Load a specialized skill. No skills are currently available.";
|
|
121
129
|
return [
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { tool, jsonSchema } from "ai";
|
|
2
|
+
/**
|
|
3
|
+
* Structured output support.
|
|
4
|
+
*
|
|
5
|
+
* When the user requests JSON output matching a schema, a StructuredOutput tool
|
|
6
|
+
* is injected. The model MUST call this tool to return its final answer in the
|
|
7
|
+
* requested format.
|
|
8
|
+
*
|
|
9
|
+
* Based on opencode's structured output implementation in prompt.ts.
|
|
10
|
+
*/
|
|
11
|
+
const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
|
|
12
|
+
|
|
13
|
+
IMPORTANT:
|
|
14
|
+
- You MUST call this tool exactly once at the end of your response
|
|
15
|
+
- The input must be valid JSON matching the required schema
|
|
16
|
+
- Complete all necessary research and tool calls BEFORE calling this tool
|
|
17
|
+
- This tool provides your final answer - no further actions are taken after calling it`;
|
|
18
|
+
export const STRUCTURED_OUTPUT_SYSTEM = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.`;
|
|
19
|
+
export function createStructuredOutputTool(request, onSuccess) {
|
|
20
|
+
const { $schema: _, ...toolSchema } = request.schema;
|
|
21
|
+
return tool({
|
|
22
|
+
description: STRUCTURED_OUTPUT_DESCRIPTION,
|
|
23
|
+
inputSchema: jsonSchema(toolSchema),
|
|
24
|
+
execute: async (args) => {
|
|
25
|
+
onSuccess(args);
|
|
26
|
+
return JSON.stringify(args, null, 2);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { generateText } from "ai";
|
|
2
|
+
/**
|
|
3
|
+
* Auto-generate a short title for a conversation session.
|
|
4
|
+
*
|
|
5
|
+
* Based on opencode's title agent — uses the LLM to produce a concise
|
|
6
|
+
* title from the first user message and assistant response.
|
|
7
|
+
*/
|
|
8
|
+
const TITLE_PROMPT = `Generate a very short title (max 50 chars) for this conversation.
|
|
9
|
+
Output ONLY the title text, nothing else. No quotes, no prefix.
|
|
10
|
+
The title should capture the main topic or intent.`;
|
|
11
|
+
export async function generateTitle(model, messages) {
|
|
12
|
+
// Need at least one user message
|
|
13
|
+
const userMsg = messages.find((m) => m.role === "user");
|
|
14
|
+
if (!userMsg)
|
|
15
|
+
return null;
|
|
16
|
+
const userContent = typeof userMsg.content === "string"
|
|
17
|
+
? userMsg.content
|
|
18
|
+
: Array.isArray(userMsg.content)
|
|
19
|
+
? userMsg.content.filter((p) => "text" in p).map((p) => p.text).join(" ")
|
|
20
|
+
: "";
|
|
21
|
+
if (!userContent.trim())
|
|
22
|
+
return null;
|
|
23
|
+
// Include first assistant response if available for better context
|
|
24
|
+
const assistantMsg = messages.find((m) => m.role === "assistant");
|
|
25
|
+
const assistantContent = assistantMsg
|
|
26
|
+
? typeof assistantMsg.content === "string"
|
|
27
|
+
? assistantMsg.content.slice(0, 200)
|
|
28
|
+
: ""
|
|
29
|
+
: "";
|
|
30
|
+
const context = assistantContent
|
|
31
|
+
? `User: ${userContent.slice(0, 300)}\nAssistant: ${assistantContent}`
|
|
32
|
+
: `User: ${userContent.slice(0, 300)}`;
|
|
33
|
+
try {
|
|
34
|
+
const result = await generateText({
|
|
35
|
+
model,
|
|
36
|
+
messages: [
|
|
37
|
+
{ role: "system", content: TITLE_PROMPT },
|
|
38
|
+
{ role: "user", content: context },
|
|
39
|
+
],
|
|
40
|
+
temperature: 0.5,
|
|
41
|
+
});
|
|
42
|
+
const title = result.text.trim().replace(/^["']|["']$/g, "").slice(0, 60);
|
|
43
|
+
return title || null;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/tools/bash.js
CHANGED
|
@@ -1,72 +1,13 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
2
1
|
import { tool, jsonSchema } from "ai";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
3
|
import { confirm, isDangerousCommand, isAutoApprove } from "../confirm.js";
|
|
4
|
-
import { truncateToolOutput } from "../tool-output.js";
|
|
5
|
-
/** Hard cap for in-memory collection before killing the process (avoid OOM on huge stdout). */
|
|
6
|
-
const COLLECT_HARD_CAP_BYTES = 16 * 1024 * 1024;
|
|
7
|
-
function runCommand(command, cwd, timeoutMs) {
|
|
8
|
-
return new Promise((resolve, reject) => {
|
|
9
|
-
const child = spawn(command, {
|
|
10
|
-
shell: true,
|
|
11
|
-
cwd,
|
|
12
|
-
env: process.env,
|
|
13
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
14
|
-
});
|
|
15
|
-
const outChunks = [];
|
|
16
|
-
const errChunks = [];
|
|
17
|
-
let total = 0;
|
|
18
|
-
let killedCap = false;
|
|
19
|
-
let killedTimeout = false;
|
|
20
|
-
const timer = setTimeout(() => {
|
|
21
|
-
killedTimeout = true;
|
|
22
|
-
child.kill("SIGTERM");
|
|
23
|
-
setTimeout(() => child.kill("SIGKILL"), 2000).unref();
|
|
24
|
-
}, timeoutMs);
|
|
25
|
-
const push = (buf, arr) => {
|
|
26
|
-
total += buf.length;
|
|
27
|
-
if (total > COLLECT_HARD_CAP_BYTES && !killedCap) {
|
|
28
|
-
killedCap = true;
|
|
29
|
-
child.kill("SIGKILL");
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
arr.push(buf);
|
|
33
|
-
};
|
|
34
|
-
child.stdout?.on("data", (b) => push(b, outChunks));
|
|
35
|
-
child.stderr?.on("data", (b) => push(b, errChunks));
|
|
36
|
-
child.on("error", (err) => {
|
|
37
|
-
clearTimeout(timer);
|
|
38
|
-
reject(err);
|
|
39
|
-
});
|
|
40
|
-
child.on("close", (code) => {
|
|
41
|
-
clearTimeout(timer);
|
|
42
|
-
const stdout = Buffer.concat(outChunks).toString("utf-8");
|
|
43
|
-
const stderr = Buffer.concat(errChunks).toString("utf-8");
|
|
44
|
-
let combined = stdout.replace(/\s+$/, "");
|
|
45
|
-
if (stderr)
|
|
46
|
-
combined += (combined ? "\n" : "") + stderr.replace(/\s+$/, "");
|
|
47
|
-
if (killedCap) {
|
|
48
|
-
combined +=
|
|
49
|
-
`\n\n[bash] Output collection stopped: exceeded ${COLLECT_HARD_CAP_BYTES} bytes in-memory cap (process was killed). Prefer redirecting to a file (e.g. > out.txt) then read with startLine/endLine.`;
|
|
50
|
-
}
|
|
51
|
-
else if (killedTimeout) {
|
|
52
|
-
combined += `\n\n[bash] Command exceeded timeout ${timeoutMs} ms (process terminated).`;
|
|
53
|
-
}
|
|
54
|
-
resolve({
|
|
55
|
-
code,
|
|
56
|
-
output: combined || "(no output)",
|
|
57
|
-
killedByTimeout: killedTimeout,
|
|
58
|
-
killedByCap: killedCap,
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
4
|
export const bashTool = tool({
|
|
64
|
-
description: "Run a shell command. Use this for system operations, running builds, tests, git commands, etc. The command runs in the current working directory.
|
|
5
|
+
description: "Run a shell command. Use this for system operations, running builds, tests, git commands, etc. The command runs in the current working directory. You SHOULD set a timeout based on how long you expect the command to take. If no timeout is set, the command runs until it finishes or the user manually interrupts (Ctrl+C).",
|
|
65
6
|
inputSchema: jsonSchema({
|
|
66
7
|
type: "object",
|
|
67
8
|
properties: {
|
|
68
9
|
command: { type: "string", description: "The shell command to execute" },
|
|
69
|
-
timeout: { type: "number", description: "Timeout in milliseconds (
|
|
10
|
+
timeout: { type: "number", description: "Timeout in milliseconds. Set based on expected duration (e.g. 5000 for quick commands, 60000 for builds). Omit only for commands with unpredictable duration." },
|
|
70
11
|
},
|
|
71
12
|
required: ["command"],
|
|
72
13
|
}),
|
|
@@ -76,18 +17,88 @@ export const bashTool = tool({
|
|
|
76
17
|
if (!approved)
|
|
77
18
|
return "Command rejected by user.";
|
|
78
19
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
let killed = false;
|
|
23
|
+
let timer;
|
|
24
|
+
// On Windows, force UTF-8 codepage to avoid Chinese garbled text
|
|
25
|
+
const isWin = process.platform === "win32";
|
|
26
|
+
const actualCommand = isWin ? `chcp 65001 >nul && ${command}` : command;
|
|
27
|
+
const proc = spawn(actualCommand, [], {
|
|
28
|
+
shell: true,
|
|
29
|
+
cwd: process.cwd(),
|
|
30
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
31
|
+
detached: process.platform !== "win32",
|
|
32
|
+
env: { ...process.env, ...(isWin ? { PYTHONIOENCODING: "utf-8" } : {}) },
|
|
33
|
+
});
|
|
34
|
+
proc.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
35
|
+
proc.stderr?.on("data", (chunk) => chunks.push(chunk));
|
|
36
|
+
// Timeout kill (only if timeout is specified)
|
|
37
|
+
if (timeout && timeout > 0) {
|
|
38
|
+
timer = setTimeout(() => {
|
|
39
|
+
killed = true;
|
|
40
|
+
killProcess(proc.pid);
|
|
41
|
+
resolve(getOutput(chunks) +
|
|
42
|
+
`\n\n[Command timed out after ${timeout}ms and was killed. Retry with a larger timeout if needed.]`);
|
|
43
|
+
}, timeout);
|
|
85
44
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
45
|
+
// Allow user to interrupt with Ctrl+C (SIGINT)
|
|
46
|
+
const sigintHandler = () => {
|
|
47
|
+
killed = true;
|
|
48
|
+
if (timer)
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
killProcess(proc.pid);
|
|
51
|
+
resolve(getOutput(chunks) + "\n\n[Command interrupted by user.]");
|
|
52
|
+
};
|
|
53
|
+
process.on("SIGINT", sigintHandler);
|
|
54
|
+
proc.on("close", (code) => {
|
|
55
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
56
|
+
if (timer)
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
if (killed)
|
|
59
|
+
return;
|
|
60
|
+
const output = getOutput(chunks);
|
|
61
|
+
if (code === 0) {
|
|
62
|
+
resolve(output || "(no output)");
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
resolve(`Exit code ${code}\n${output || "(no output)"}`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
proc.on("error", (err) => {
|
|
69
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
70
|
+
if (timer)
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
if (killed)
|
|
73
|
+
return;
|
|
74
|
+
resolve(`Error: ${err.message}`);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
92
77
|
},
|
|
93
78
|
});
|
|
79
|
+
function getOutput(chunks) {
|
|
80
|
+
const output = Buffer.concat(chunks).toString("utf-8").trim();
|
|
81
|
+
if (output.length > 100_000) {
|
|
82
|
+
return output.slice(0, 50_000) + `\n\n...(truncated, ${output.length} bytes total)...\n\n` + output.slice(-10_000);
|
|
83
|
+
}
|
|
84
|
+
return output;
|
|
85
|
+
}
|
|
86
|
+
function killProcess(pid) {
|
|
87
|
+
if (!pid)
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
if (process.platform !== "win32") {
|
|
91
|
+
process.kill(-pid, "SIGTERM");
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(-pid, "SIGKILL");
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
}, 3000);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
process.kill(pid, "SIGTERM");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { tool, jsonSchema } from "ai";
|
|
2
|
+
import { truncateToolOutput } from "../tool-output.js";
|
|
3
|
+
const EXA_MCP_URL = process.env.EXA_API_KEY
|
|
4
|
+
? `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(process.env.EXA_API_KEY)}`
|
|
5
|
+
: "https://mcp.exa.ai/mcp";
|
|
6
|
+
export const codeSearchTool = tool({
|
|
7
|
+
description: `Search and get relevant context for any programming task using Exa Code API.
|
|
8
|
+
Provides high-quality, fresh context for libraries, SDKs, and APIs.
|
|
9
|
+
Use this for ANY question or task related to programming — finding code examples, documentation, API references, and patterns.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
- Adjustable token count (1000-50000) for focused or comprehensive results
|
|
13
|
+
- Default 5000 tokens for balanced context
|
|
14
|
+
- Examples: 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Rust async trait implementation'`,
|
|
15
|
+
inputSchema: jsonSchema({
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
query: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Search query for APIs, libraries, SDKs. E.g. 'React useState hook examples', 'Express.js middleware'",
|
|
21
|
+
},
|
|
22
|
+
tokensNum: {
|
|
23
|
+
type: "number",
|
|
24
|
+
description: "Number of tokens to return (1000-50000). Default 5000. Use lower for focused queries, higher for comprehensive docs.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ["query"],
|
|
28
|
+
}),
|
|
29
|
+
execute: async ({ query, tokensNum }) => {
|
|
30
|
+
const tokens = Math.max(1000, Math.min(50000, tokensNum ?? 5000));
|
|
31
|
+
try {
|
|
32
|
+
const result = await callExaCode(query, tokens);
|
|
33
|
+
if (!result) {
|
|
34
|
+
return "No code snippets or documentation found. Try a different query, be more specific about the library or concept, or check spelling.";
|
|
35
|
+
}
|
|
36
|
+
return truncateToolOutput(result, { direction: "head" }).content;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return `Code search error: ${err.message}`;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Call Exa's MCP endpoint for code context.
|
|
45
|
+
* Uses JSON-RPC over HTTP with SSE response format.
|
|
46
|
+
*/
|
|
47
|
+
async function callExaCode(query, tokensNum) {
|
|
48
|
+
const body = JSON.stringify({
|
|
49
|
+
jsonrpc: "2.0",
|
|
50
|
+
id: 1,
|
|
51
|
+
method: "tools/call",
|
|
52
|
+
params: {
|
|
53
|
+
name: "get_code_context_exa",
|
|
54
|
+
arguments: { query, tokensNum },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const response = await fetch(EXA_MCP_URL, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
Accept: "application/json, text/event-stream",
|
|
62
|
+
},
|
|
63
|
+
body,
|
|
64
|
+
signal: AbortSignal.timeout(30000),
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Exa API returned ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
const text = await response.text();
|
|
70
|
+
// Parse SSE response: look for "data: {...}" lines
|
|
71
|
+
for (const line of text.split("\n")) {
|
|
72
|
+
if (!line.startsWith("data: "))
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
75
|
+
const data = JSON.parse(line.slice(6));
|
|
76
|
+
const content = data?.result?.content?.[0]?.text;
|
|
77
|
+
if (content)
|
|
78
|
+
return content;
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
}
|
|
82
|
+
// Try parsing as direct JSON response
|
|
83
|
+
try {
|
|
84
|
+
const data = JSON.parse(text);
|
|
85
|
+
const content = data?.result?.content?.[0]?.text;
|
|
86
|
+
if (content)
|
|
87
|
+
return content;
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { tool, jsonSchema, streamText, stepCountIs } from "ai";
|
|
2
|
+
import { resolveModel } from "../provider.js";
|
|
3
|
+
import { bashTool } from "./bash.js";
|
|
4
|
+
import { readTool } from "./read.js";
|
|
5
|
+
import { globTool } from "./glob.js";
|
|
6
|
+
import { grepTool } from "./grep.js";
|
|
7
|
+
import { stripThinkingFromAssistantText } from "../assistant-stream.js";
|
|
8
|
+
import { truncateToolOutput } from "../tool-output.js";
|
|
9
|
+
const EXPLORE_SYSTEM = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
|
10
|
+
|
|
11
|
+
Your strengths:
|
|
12
|
+
- Rapidly finding files using glob patterns
|
|
13
|
+
- Searching code and text with powerful regex patterns
|
|
14
|
+
- Reading and analyzing file contents
|
|
15
|
+
|
|
16
|
+
Guidelines:
|
|
17
|
+
- Use glob for broad file pattern matching
|
|
18
|
+
- Use grep for searching file contents with regex
|
|
19
|
+
- Use read when you know the specific file path
|
|
20
|
+
- Use bash ONLY for read-only operations (ls, find, cat, wc, head, tail)
|
|
21
|
+
- Adapt your search approach based on the thoroughness level specified
|
|
22
|
+
- Return file paths as absolute paths in your final response
|
|
23
|
+
- Do NOT create, modify, or delete any files
|
|
24
|
+
- Do NOT run commands that change system state
|
|
25
|
+
|
|
26
|
+
Complete the search request efficiently and report findings clearly.
|
|
27
|
+
|
|
28
|
+
Working directory: ${process.cwd()}
|
|
29
|
+
Platform: ${process.platform}`;
|
|
30
|
+
const EXPLORE_MAX_STEPS = 20;
|
|
31
|
+
export function createExploreTool(modelId) {
|
|
32
|
+
return tool({
|
|
33
|
+
description: `Deep codebase exploration agent. Use this to understand project structure, find files by patterns, search code for keywords, trace module relationships, or answer questions about the codebase.
|
|
34
|
+
|
|
35
|
+
Specify thoroughness:
|
|
36
|
+
- "quick": basic search, 1-3 tool calls
|
|
37
|
+
- "medium": moderate exploration, follow references
|
|
38
|
+
- "thorough": comprehensive analysis across multiple locations and naming conventions
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
- "Find all API route handlers" (medium)
|
|
42
|
+
- "How does the auth system work?" (thorough)
|
|
43
|
+
- "Where is the database config?" (quick)`,
|
|
44
|
+
inputSchema: jsonSchema({
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
query: { type: "string", description: "What to explore or find in the codebase" },
|
|
48
|
+
thoroughness: { type: "string", description: "Search depth: quick, medium, or thorough (default: medium)" },
|
|
49
|
+
},
|
|
50
|
+
required: ["query"],
|
|
51
|
+
}),
|
|
52
|
+
execute: async ({ query, thoroughness }) => {
|
|
53
|
+
const level = thoroughness ?? "medium";
|
|
54
|
+
console.log(`\x1b[90m ┌─ Explore (${level}): ${query.slice(0, 60)}\x1b[0m`);
|
|
55
|
+
try {
|
|
56
|
+
const result = await runExploreAgent(query, level, modelId);
|
|
57
|
+
console.log(`\x1b[90m └─ ✓ Done\x1b[0m`);
|
|
58
|
+
return truncateToolOutput(result, { direction: "head" }).content;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
console.log(`\x1b[90m └─ ✗ Failed: ${err.message}\x1b[0m`);
|
|
62
|
+
return `Explore error: ${err.message}`;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function runExploreAgent(query, thoroughness, modelId) {
|
|
68
|
+
const model = resolveModel(modelId);
|
|
69
|
+
// Read-only tools only
|
|
70
|
+
const tools = {
|
|
71
|
+
glob: globTool,
|
|
72
|
+
grep: grepTool,
|
|
73
|
+
read: readTool,
|
|
74
|
+
bash: bashTool, // bash is available but prompt restricts to read-only
|
|
75
|
+
};
|
|
76
|
+
const prompt = `Thoroughness level: ${thoroughness}
|
|
77
|
+
${thoroughness === "quick" ? "Do a quick search (1-3 tool calls max)." : ""}
|
|
78
|
+
${thoroughness === "medium" ? "Do a moderate exploration, follow references if needed." : ""}
|
|
79
|
+
${thoroughness === "thorough" ? "Do a comprehensive analysis. Check multiple locations, naming conventions, and cross-references." : ""}
|
|
80
|
+
|
|
81
|
+
Task: ${query}`;
|
|
82
|
+
const messages = [{ role: "user", content: prompt }];
|
|
83
|
+
const result = streamText({
|
|
84
|
+
model,
|
|
85
|
+
system: EXPLORE_SYSTEM,
|
|
86
|
+
messages,
|
|
87
|
+
tools,
|
|
88
|
+
stopWhen: stepCountIs(EXPLORE_MAX_STEPS),
|
|
89
|
+
maxRetries: 2,
|
|
90
|
+
onError() { },
|
|
91
|
+
});
|
|
92
|
+
let assistantText = "";
|
|
93
|
+
for await (const event of result.fullStream) {
|
|
94
|
+
switch (event.type) {
|
|
95
|
+
case "text-delta":
|
|
96
|
+
assistantText += event.text;
|
|
97
|
+
break;
|
|
98
|
+
case "tool-call":
|
|
99
|
+
console.log(`\x1b[90m │ ⚡ ${event.toolName}\x1b[0m`);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return stripThinkingFromAssistantText(assistantText) || "(explore agent produced no output)";
|
|
104
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -6,8 +6,11 @@ import { globTool } from "./glob.js";
|
|
|
6
6
|
import { grepTool } from "./grep.js";
|
|
7
7
|
import { webSearchTool } from "./web_search.js";
|
|
8
8
|
import { webFetchTool } from "./web_fetch.js";
|
|
9
|
+
import { todoTool } from "./todo.js";
|
|
10
|
+
import { questionTool } from "./question.js";
|
|
11
|
+
import { codeSearchTool } from "./code_search.js";
|
|
9
12
|
export function createTools() {
|
|
10
|
-
|
|
13
|
+
const tools = {
|
|
11
14
|
bash: bashTool,
|
|
12
15
|
read: readTool,
|
|
13
16
|
write: writeTool,
|
|
@@ -16,5 +19,11 @@ export function createTools() {
|
|
|
16
19
|
grep: grepTool,
|
|
17
20
|
web_search: webSearchTool,
|
|
18
21
|
web_fetch: webFetchTool,
|
|
22
|
+
todo: todoTool,
|
|
23
|
+
question: questionTool,
|
|
19
24
|
};
|
|
25
|
+
if (process.env.EXA_API_KEY) {
|
|
26
|
+
tools.codesearch = codeSearchTool;
|
|
27
|
+
}
|
|
28
|
+
return tools;
|
|
20
29
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { tool, jsonSchema } from "ai";
|
|
2
|
+
import readline from "readline";
|
|
3
|
+
export const questionTool = tool({
|
|
4
|
+
description: `Ask the user a question to get clarification before proceeding. Use this when:
|
|
5
|
+
- The task is ambiguous and you need more information
|
|
6
|
+
- There are multiple valid approaches and you want the user to choose
|
|
7
|
+
- You need confirmation before a potentially destructive action
|
|
8
|
+
- You're unsure about a requirement or preference
|
|
9
|
+
|
|
10
|
+
Provide clear, specific questions. Optionally include numbered options for the user to choose from.`,
|
|
11
|
+
inputSchema: jsonSchema({
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
question: { type: "string", description: "The question to ask the user" },
|
|
15
|
+
options: {
|
|
16
|
+
type: "array",
|
|
17
|
+
items: { type: "string" },
|
|
18
|
+
description: "Optional list of choices for the user to pick from",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ["question"],
|
|
22
|
+
}),
|
|
23
|
+
execute: async ({ question, options }) => {
|
|
24
|
+
console.log();
|
|
25
|
+
console.log(`\x1b[33m❓ ${question}\x1b[0m`);
|
|
26
|
+
if (options && options.length > 0) {
|
|
27
|
+
for (let i = 0; i < options.length; i++) {
|
|
28
|
+
console.log(`\x1b[90m ${i + 1}. ${options[i]}\x1b[0m`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const answer = await askUser();
|
|
32
|
+
// If user picked a number and options exist, resolve it
|
|
33
|
+
if (options && options.length > 0) {
|
|
34
|
+
const idx = parseInt(answer) - 1;
|
|
35
|
+
if (idx >= 0 && idx < options.length) {
|
|
36
|
+
return `User chose: ${options[idx]}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return `User answered: ${answer}`;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
function askUser() {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const rl = readline.createInterface({
|
|
45
|
+
input: process.stdin,
|
|
46
|
+
output: process.stdout,
|
|
47
|
+
});
|
|
48
|
+
rl.question("\x1b[36m → \x1b[0m", (answer) => {
|
|
49
|
+
rl.close();
|
|
50
|
+
resolve(answer.trim() || "(no answer)");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
package/dist/tools/read.js
CHANGED
|
@@ -2,6 +2,9 @@ import { tool, jsonSchema } from "ai";
|
|
|
2
2
|
import { readFileSync, statSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { truncateToolOutput } from "../tool-output.js";
|
|
5
|
+
import { InstructionTracker } from "../instructions.js";
|
|
6
|
+
// Shared tracker instance for context-aware instruction discovery
|
|
7
|
+
const instructionTracker = new InstructionTracker();
|
|
5
8
|
export const readTool = tool({
|
|
6
9
|
description: "Read the contents of a file. Returns the file content as text. Use this to understand code, check configurations, etc.",
|
|
7
10
|
inputSchema: jsonSchema({
|
|
@@ -20,14 +23,22 @@ export const readTool = tool({
|
|
|
20
23
|
if (stat.isDirectory())
|
|
21
24
|
return `Error: ${filePath} is a directory, not a file`;
|
|
22
25
|
const content = readFileSync(resolved, "utf-8");
|
|
26
|
+
let result;
|
|
23
27
|
if (startLine || endLine) {
|
|
24
28
|
const lines = content.split("\n");
|
|
25
29
|
const start = (startLine ?? 1) - 1;
|
|
26
30
|
const end = endLine ?? lines.length;
|
|
27
|
-
|
|
28
|
-
return truncateToolOutput(slice, { direction: "head" }).content;
|
|
31
|
+
result = lines.slice(start, end).join("\n");
|
|
29
32
|
}
|
|
30
|
-
|
|
33
|
+
else {
|
|
34
|
+
result = content;
|
|
35
|
+
}
|
|
36
|
+
// Context-aware: discover nearby instruction files
|
|
37
|
+
const nearbyInstructions = instructionTracker.resolveForFile(resolved);
|
|
38
|
+
if (nearbyInstructions.length > 0) {
|
|
39
|
+
result += "\n\n<system-reminder>\n" + nearbyInstructions.join("\n\n") + "\n</system-reminder>";
|
|
40
|
+
}
|
|
41
|
+
return truncateToolOutput(result, { direction: "head" }).content;
|
|
31
42
|
}
|
|
32
43
|
catch (err) {
|
|
33
44
|
return `Error reading file: ${err.message}`;
|