ideacode 1.2.5 → 1.3.0
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/dist/repl.js +11 -3
- package/dist/tools/bash.js +156 -4
- package/dist/tools/index.js +18 -3
- package/package.json +1 -1
package/dist/repl.js
CHANGED
|
@@ -39,7 +39,15 @@ const MAX_TOOL_RESULT_CHARS = 3500;
|
|
|
39
39
|
const MAX_AT_SUGGESTIONS = 12;
|
|
40
40
|
const INITIAL_BANNER_LINES = 12;
|
|
41
41
|
const ENABLE_PARALLEL_TOOL_CALLS = process.env.IDEACODE_PARALLEL_TOOL_CALLS !== "0";
|
|
42
|
-
const PARALLEL_SAFE_TOOLS = new Set([
|
|
42
|
+
const PARALLEL_SAFE_TOOLS = new Set([
|
|
43
|
+
"read",
|
|
44
|
+
"glob",
|
|
45
|
+
"grep",
|
|
46
|
+
"web_fetch",
|
|
47
|
+
"web_search",
|
|
48
|
+
"bash_status",
|
|
49
|
+
"bash_logs",
|
|
50
|
+
]);
|
|
43
51
|
const LOADING_TICK_MS = 80;
|
|
44
52
|
const MAX_EMPTY_ASSISTANT_RETRIES = 3;
|
|
45
53
|
const TRUNCATE_NOTE = "\n\n(Output truncated to save context. Use read with offset/limit, grep with a specific pattern, or tail with fewer lines to get more.)";
|
|
@@ -150,7 +158,7 @@ function summarizeBashCommand(cmdRaw) {
|
|
|
150
158
|
return (shown.join(", ") + suffix).slice(0, 140);
|
|
151
159
|
}
|
|
152
160
|
function toolArgPreview(toolName, toolArgs) {
|
|
153
|
-
if (toolName === "bash") {
|
|
161
|
+
if (toolName === "bash" || toolName === "bash_detach") {
|
|
154
162
|
const cmd = String(toolArgs.cmd ?? "").trim();
|
|
155
163
|
return cmd ? summarizeBashCommand(cmd) : "—";
|
|
156
164
|
}
|
|
@@ -643,7 +651,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
643
651
|
appendLog(userPromptBox(userInput));
|
|
644
652
|
appendLog("");
|
|
645
653
|
let state = [...messages, { role: "user", content: userInput }];
|
|
646
|
-
const systemPrompt = `Concise coding assistant. cwd: ${cwd}. PRIORITIZE grep to locate; then read with offset and limit to fetch only relevant sections. Do not read whole files unless the user explicitly asks. Use focused greps (specific patterns, narrow paths) and read in chunks when files are large; avoid one huge grep or read that floods context. When exploring a dependency, set path to that package (e.g. node_modules/<pkg>) and list/read only what you need. Prefer grep or keyword search for the most recent or specific occurrence; avoid tail/read of thousands of lines. If a tool result says it was truncated, call the tool again with offset, limit, or a narrower pattern to get what you need. Use as many parallel read/search/web tool calls as needed in one turn when they are independent (often more than 3 is appropriate for broad research), but keep each call high-signal, non-redundant, and minimal in output size. For bash tool calls, avoid decorative echo headers; run direct commands and keep commands concise.`;
|
|
654
|
+
const systemPrompt = `Concise coding assistant. cwd: ${cwd}. PRIORITIZE grep to locate; then read with offset and limit to fetch only relevant sections. Do not read whole files unless the user explicitly asks. Use focused greps (specific patterns, narrow paths) and read in chunks when files are large; avoid one huge grep or read that floods context. When exploring a dependency, set path to that package (e.g. node_modules/<pkg>) and list/read only what you need. Prefer grep or keyword search for the most recent or specific occurrence; avoid tail/read of thousands of lines. If a tool result says it was truncated, call the tool again with offset, limit, or a narrower pattern to get what you need. Use as many parallel read/search/web tool calls as needed in one turn when they are independent (often more than 3 is appropriate for broad research), but keep each call high-signal, non-redundant, and minimal in output size. For bash tool calls, avoid decorative echo headers; run direct commands and keep commands concise. For long-running commands, prefer bash_detach then poll with bash_status and bash_logs instead of blocking bash.`;
|
|
647
655
|
const modelContext = modelList.find((m) => m.id === currentModel)?.context_length;
|
|
648
656
|
const maxContextTokens = Math.floor((modelContext ?? CONTEXT_WINDOW_K * 1024) * 0.85);
|
|
649
657
|
const stateBeforeCompress = state;
|
package/dist/tools/bash.js
CHANGED
|
@@ -1,4 +1,73 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { closeSync, openSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
const MIN_TIMEOUT_MS = 1_000;
|
|
8
|
+
const MAX_TIMEOUT_MS = 60 * 60 * 1000;
|
|
9
|
+
const TERM_GRACE_MS = 2_000;
|
|
10
|
+
const JOBS_DIR = path.join(tmpdir(), "ideacode-jobs");
|
|
11
|
+
const JOBS_DB_PATH = path.join(JOBS_DIR, "jobs.json");
|
|
12
|
+
function clampTimeoutMs(value) {
|
|
13
|
+
if (!Number.isFinite(value))
|
|
14
|
+
return DEFAULT_TIMEOUT_MS;
|
|
15
|
+
return Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, Math.round(value)));
|
|
16
|
+
}
|
|
17
|
+
function resolveTimeoutMs(args) {
|
|
18
|
+
const fromArgs = Number(args.timeout_ms);
|
|
19
|
+
if (Number.isFinite(fromArgs) && fromArgs > 0)
|
|
20
|
+
return clampTimeoutMs(fromArgs);
|
|
21
|
+
const fromEnv = Number.parseInt(process.env.IDEACODE_BASH_TIMEOUT_MS ?? "", 10);
|
|
22
|
+
if (Number.isFinite(fromEnv) && fromEnv > 0)
|
|
23
|
+
return clampTimeoutMs(fromEnv);
|
|
24
|
+
return DEFAULT_TIMEOUT_MS;
|
|
25
|
+
}
|
|
26
|
+
function shSingleQuote(v) {
|
|
27
|
+
return `'${v.replace(/'/g, `'\\''`)}'`;
|
|
28
|
+
}
|
|
29
|
+
function makeJobId() {
|
|
30
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
31
|
+
return `job_${Date.now().toString(36)}_${rand}`;
|
|
32
|
+
}
|
|
33
|
+
async function ensureJobsDir() {
|
|
34
|
+
await mkdir(JOBS_DIR, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
async function loadJobs() {
|
|
37
|
+
try {
|
|
38
|
+
const raw = await readFile(JOBS_DB_PATH, "utf8");
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function saveJobs(db) {
|
|
47
|
+
await ensureJobsDir();
|
|
48
|
+
await writeFile(JOBS_DB_PATH, JSON.stringify(db, null, 2), "utf8");
|
|
49
|
+
}
|
|
50
|
+
function isPidRunning(pid) {
|
|
51
|
+
try {
|
|
52
|
+
process.kill(pid, 0);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function readExitCode(statusPath) {
|
|
60
|
+
try {
|
|
61
|
+
const raw = await readFile(statusPath, "utf8");
|
|
62
|
+
const m = raw.match(/EXIT_CODE:(-?\d+)/);
|
|
63
|
+
if (!m)
|
|
64
|
+
return null;
|
|
65
|
+
return Number.parseInt(m[1] ?? "", 10);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
2
71
|
export function runBash(args) {
|
|
3
72
|
return new Promise((resolve) => {
|
|
4
73
|
const cmd = args.cmd ?? "";
|
|
@@ -14,14 +83,97 @@ export function runBash(args) {
|
|
|
14
83
|
const onData = (s) => outputChunks.push(s);
|
|
15
84
|
proc.stdout?.on("data", (chunk) => onData(chunk.toString()));
|
|
16
85
|
proc.stderr?.on("data", (chunk) => onData(chunk.toString()));
|
|
86
|
+
const timeoutMs = resolveTimeoutMs(args);
|
|
17
87
|
const t = setTimeout(() => {
|
|
18
|
-
proc.kill("
|
|
19
|
-
outputChunks.push(
|
|
20
|
-
|
|
21
|
-
|
|
88
|
+
proc.kill("SIGTERM");
|
|
89
|
+
outputChunks.push(`\n(timeout ${Math.round(timeoutMs / 1000)}s reached, sending SIGTERM…)`);
|
|
90
|
+
const killTimer = setTimeout(() => {
|
|
91
|
+
proc.kill("SIGKILL");
|
|
92
|
+
outputChunks.push("\n(process did not exit after SIGTERM; sent SIGKILL)");
|
|
93
|
+
}, TERM_GRACE_MS);
|
|
94
|
+
proc.on("close", () => clearTimeout(killTimer));
|
|
95
|
+
}, timeoutMs);
|
|
22
96
|
proc.on("close", () => {
|
|
23
97
|
clearTimeout(t);
|
|
24
98
|
done(outputChunks.join("").trim() || "(empty)");
|
|
25
99
|
});
|
|
26
100
|
});
|
|
27
101
|
}
|
|
102
|
+
export async function runBashDetach(args) {
|
|
103
|
+
const cmd = String(args.cmd ?? "").trim();
|
|
104
|
+
if (!cmd)
|
|
105
|
+
return "error: missing cmd";
|
|
106
|
+
await ensureJobsDir();
|
|
107
|
+
const id = makeJobId();
|
|
108
|
+
const cwd = process.cwd();
|
|
109
|
+
const logPath = path.join(JOBS_DIR, `${id}.log`);
|
|
110
|
+
const statusPath = path.join(JOBS_DIR, `${id}.status`);
|
|
111
|
+
await appendFile(logPath, `# ideacode detached job ${id}\n# cwd: ${cwd}\n# cmd: ${cmd}\n\n`);
|
|
112
|
+
const wrapped = [
|
|
113
|
+
cmd,
|
|
114
|
+
"__ec=$?",
|
|
115
|
+
`printf "EXIT_CODE:%s\\n" "$__ec" > ${shSingleQuote(statusPath)}`,
|
|
116
|
+
"exit $__ec",
|
|
117
|
+
].join("\n");
|
|
118
|
+
const logFd = openSync(logPath, "a");
|
|
119
|
+
const proc = spawn("/bin/sh", ["-lc", wrapped], {
|
|
120
|
+
cwd,
|
|
121
|
+
detached: true,
|
|
122
|
+
stdio: ["ignore", logFd, logFd],
|
|
123
|
+
});
|
|
124
|
+
closeSync(logFd);
|
|
125
|
+
proc.unref();
|
|
126
|
+
const db = await loadJobs();
|
|
127
|
+
db[id] = {
|
|
128
|
+
id,
|
|
129
|
+
cmd,
|
|
130
|
+
pid: proc.pid ?? -1,
|
|
131
|
+
cwd,
|
|
132
|
+
startedAt: new Date().toISOString(),
|
|
133
|
+
logPath,
|
|
134
|
+
statusPath,
|
|
135
|
+
};
|
|
136
|
+
await saveJobs(db);
|
|
137
|
+
return `ok: ${id} pid=${proc.pid ?? -1}\nlog=${logPath}\nuse bash_status(job_id=${id}) or bash_logs(job_id=${id})`;
|
|
138
|
+
}
|
|
139
|
+
export async function runBashStatus(args) {
|
|
140
|
+
const jobId = String(args.job_id ?? "").trim();
|
|
141
|
+
if (!jobId)
|
|
142
|
+
return "error: missing job_id";
|
|
143
|
+
const db = await loadJobs();
|
|
144
|
+
const job = db[jobId];
|
|
145
|
+
if (!job)
|
|
146
|
+
return `error: unknown job_id ${jobId}`;
|
|
147
|
+
const running = job.pid > 0 ? isPidRunning(job.pid) : false;
|
|
148
|
+
const exitCode = await readExitCode(job.statusPath);
|
|
149
|
+
const status = running ? "running" : exitCode == null ? "unknown (not running, no exit code)" : `finished (exit=${exitCode})`;
|
|
150
|
+
return [
|
|
151
|
+
`job_id: ${job.id}`,
|
|
152
|
+
`status: ${status}`,
|
|
153
|
+
`pid: ${job.pid}`,
|
|
154
|
+
`started_at: ${job.startedAt}`,
|
|
155
|
+
`cwd: ${job.cwd}`,
|
|
156
|
+
`log: ${job.logPath}`,
|
|
157
|
+
].join("\n");
|
|
158
|
+
}
|
|
159
|
+
export async function runBashLogs(args) {
|
|
160
|
+
const jobId = String(args.job_id ?? "").trim();
|
|
161
|
+
if (!jobId)
|
|
162
|
+
return "error: missing job_id";
|
|
163
|
+
const tailLinesRaw = Number(args.tail_lines);
|
|
164
|
+
const tailLines = Number.isFinite(tailLinesRaw)
|
|
165
|
+
? Math.max(1, Math.min(500, Math.round(tailLinesRaw)))
|
|
166
|
+
: 80;
|
|
167
|
+
const db = await loadJobs();
|
|
168
|
+
const job = db[jobId];
|
|
169
|
+
if (!job)
|
|
170
|
+
return `error: unknown job_id ${jobId}`;
|
|
171
|
+
try {
|
|
172
|
+
const content = await readFile(job.logPath, "utf8");
|
|
173
|
+
const lines = content.split(/\r?\n/);
|
|
174
|
+
return lines.slice(Math.max(0, lines.length - tailLines)).join("\n").trim() || "(empty)";
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
return `error: failed to read logs for ${jobId}: ${err instanceof Error ? err.message : String(err)}`;
|
|
178
|
+
}
|
|
179
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getBraveSearchApiKey } from "../config.js";
|
|
2
2
|
import { readFile, writeFile, editFile } from "./file.js";
|
|
3
3
|
import { globFiles, grepFiles } from "./search.js";
|
|
4
|
-
import { runBash } from "./bash.js";
|
|
4
|
+
import { runBash, runBashDetach, runBashLogs, runBashStatus } from "./bash.js";
|
|
5
5
|
import { webFetch, webSearch } from "./web.js";
|
|
6
6
|
export const TOOLS = {
|
|
7
7
|
read: [
|
|
@@ -26,10 +26,25 @@ export const TOOLS = {
|
|
|
26
26
|
grepFiles,
|
|
27
27
|
],
|
|
28
28
|
bash: [
|
|
29
|
-
"Run shell command. Use for things
|
|
30
|
-
{ cmd: "string" },
|
|
29
|
+
"Run shell command. Supports optional timeout_ms (default 30000). Use for things other tools don't cover. Prefer targeted commands and avoid huge outputs.",
|
|
30
|
+
{ cmd: "string", timeout_ms: "number?" },
|
|
31
31
|
runBash,
|
|
32
32
|
],
|
|
33
|
+
bash_detach: [
|
|
34
|
+
"Start a long-running shell command in the background and return a job_id for later polling.",
|
|
35
|
+
{ cmd: "string" },
|
|
36
|
+
runBashDetach,
|
|
37
|
+
],
|
|
38
|
+
bash_status: [
|
|
39
|
+
"Check status of a detached bash job.",
|
|
40
|
+
{ job_id: "string" },
|
|
41
|
+
runBashStatus,
|
|
42
|
+
],
|
|
43
|
+
bash_logs: [
|
|
44
|
+
"Read recent log lines from a detached bash job.",
|
|
45
|
+
{ job_id: "string", tail_lines: "number?" },
|
|
46
|
+
runBashLogs,
|
|
47
|
+
],
|
|
33
48
|
web_fetch: [
|
|
34
49
|
"Fetch a URL and return the main text content (handles JS-rendered pages). Use for docs, raw GitHub, any web page.",
|
|
35
50
|
{ url: "string" },
|