codeshark-cli 0.1.1 → 0.1.4

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 CHANGED
@@ -5,9 +5,11 @@ import { runAgent } from "./agent.js";
5
5
  import { ProviderError, errorMessage } from "./provider/types.js";
6
6
  import { runSetup } from "./setup.js";
7
7
  import { loadConfig, modelLabel, saveConfig } from "./config.js";
8
- import { DEFAULT_MODEL_ID, MODELS, findModel } from "./models.js";
8
+ import { DEFAULT_MODEL_ID, MODELS, RETIRED_MODELS, findModel, isRetiredModel } from "./models.js";
9
9
  import { launchKeysPage } from "./keysPage.js";
10
- const PROMPT = "🦈 ";
10
+ import { defaultSystemPrompt } from "./system.js";
11
+ import { modelAvailability, modelAvailabilityReason } from "./modelAvailability.js";
12
+ const PROMPT = "> ";
11
13
  function replClosed(rl) {
12
14
  return Boolean(rl.closed);
13
15
  }
@@ -15,16 +17,23 @@ function briefArgs(args) {
15
17
  const s = JSON.stringify(args);
16
18
  return s.length > 90 ? s.slice(0, 90) + "…" : s;
17
19
  }
20
+ async function approveToolCall(rl, call, signal) {
21
+ rl.resume();
22
+ const answer = await rl.question(`\n Approve ${call.name}(${briefArgs(call.args)})? [y/N]: `, { signal });
23
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
24
+ }
18
25
  function printHelp() {
19
26
  console.log([
20
27
  "",
21
28
  bold(" CodeShark commands"),
22
29
  dim(" /help show this help"),
23
30
  dim(" /model show the active model and the full catalog"),
24
- dim(" /model <name> switch model, e.g. /model glm or /model kimi-k3"),
31
+ dim(" /model <name> switch model, e.g. /model glm or /model deepseek"),
25
32
  dim(" /keys open the password-protected local API key page"),
26
33
  dim(" /key-status show masked key status in the terminal"),
27
34
  dim(" /setup guided setup wizard (providers / keys)"),
35
+ dim(" /plan switch to read-only planning mode"),
36
+ dim(" /build switch to implementation mode"),
28
37
  dim(" /clear clear the screen"),
29
38
  dim(" /quit exit (Ctrl+C also works)"),
30
39
  "",
@@ -38,11 +47,21 @@ export function printModelInfo() {
38
47
  console.log(bold(" Model catalog"));
39
48
  for (const m of MODELS) {
40
49
  const active = m.id === (findModel(cfg.model ?? "")?.id ?? DEFAULT_MODEL_ID);
41
- const marker = active ? hex("#4ade80", "●") : dim("○");
42
- console.log(` ${marker} ${m.label.padEnd(28)} ${dim(m.context.padEnd(5))} ${m.notes}`);
50
+ const status = modelAvailability(m.id);
51
+ const marker = status === "available" ? hex("#4ade80", "●") : status === "unavailable" ? hex("#f87171", "✗") : dim("○");
52
+ const note = status === "unavailable" ? `Unavailable${modelAvailabilityReason(m.id) ? ` — ${modelAvailabilityReason(m.id)}` : ""}` : m.notes;
53
+ console.log(` ${marker} ${m.label.padEnd(28)} ${dim(m.context.padEnd(5))} ${status === "unavailable" ? hex("#f87171", note) : note}`);
54
+ }
55
+ for (const m of RETIRED_MODELS) {
56
+ console.log(` ${hex("#f87171", "✗")} ${m.label.padEnd(28)} ${dim(m.context.padEnd(5))} ${hex("#f87171", "Unavailable — removed by provider")}`);
57
+ }
58
+ const cfgModel = loadConfig().model;
59
+ if (cfgModel && isRetiredModel(cfgModel)) {
60
+ console.log(hex("#f87171", ` ✗ Saved model "${cfgModel}" is unavailable. Using ${findModel(DEFAULT_MODEL_ID)?.label ?? DEFAULT_MODEL_ID}.`));
43
61
  }
44
62
  console.log("");
45
- console.log(dim(" Switch with: /model <name> e.g. /model glm, /model kimi, /model gpt"));
63
+ console.log(dim(" Switch with: /model <name>"));
64
+ console.log(dim(" Names: gpt, deepseek, minimax, glm, gemini-3.6, sarvam, gpt-oss, nemotron"));
46
65
  console.log("");
47
66
  }
48
67
  export function switchModel(query) {
@@ -59,6 +78,16 @@ export function switchModel(query) {
59
78
  console.log(dim(" Available: " + MODELS.map((m) => m.label).join(", ")));
60
79
  return;
61
80
  }
81
+ if (entry.available === false) {
82
+ console.log(hex("#f87171", ` ✗ ${entry.label} is unavailable — it was removed by the provider.`));
83
+ console.log(dim(" Choose one of the currently available models listed by /model."));
84
+ return;
85
+ }
86
+ if (modelAvailability(entry.id) === "unavailable") {
87
+ console.log(hex("#f87171", ` ✗ ${entry.label} is currently unavailable.`));
88
+ console.log(dim(` ${modelAvailabilityReason(entry.id) ?? "The boot health check failed."}`));
89
+ return;
90
+ }
62
91
  cfg.model = entry.id;
63
92
  saveConfig(cfg);
64
93
  console.log(` ${hex("#4ade80", "✓")} Switched to ${bold(entry.label)} ${dim(`(${entry.context} context)`)}`);
@@ -73,28 +102,37 @@ function printKeys() {
73
102
  const or = Boolean(cfg.openrouterApiKey ?? process.env.OPENROUTER_API_KEY);
74
103
  const nv = Boolean(cfg.nvidiaApiKey ?? process.env.NVIDIA_API_KEY);
75
104
  const gm = Boolean(cfg.geminiApiKey ?? process.env.GEMINI_API_KEY);
76
- console.log(` ${ur ? hex("#4ade80", "✓") : dim("○")} UnoRouter ${ur ? dim("(configured)") : dim("(not set)")} → https://unorouter.com/en/tokens ${dim("key: shown once")}`);
105
+ console.log(` ${ur ? hex("#4ade80", "✓") : dim("○")} Model API ${ur ? dim("(configured)") : dim("(not set)")} ${dim("key: shown once")}`);
77
106
  console.log(` ${or ? hex("#4ade80", "✓") : dim("○")} OpenRouter ${or ? dim("(configured)") : dim("(not set)")} → https://openrouter.ai/keys ${dim("key: sk-or-v1-…")}`);
78
107
  console.log(` ${nv ? hex("#4ade80", "✓") : dim("○")} NVIDIA NIM ${nv ? dim("(configured)") : dim("(not set)")} → https://build.nvidia.com ${dim("key: nvapi-…")}`);
79
108
  console.log(` ${gm ? hex("#4ade80", "✓") : dim("○")} Google AI Studio ${gm ? dim("(configured)") : dim("(not set)")} → https://aistudio.google.com/apikey ${dim("key: AIza…")}`);
80
109
  console.log("");
81
110
  console.log(dim(" Add one: run /setup, or paste it into ~/.codeshark.json like:"));
82
- console.log(dim(' { "unorouterApiKey": "ur-…", "openrouterApiKey": "sk-or-v1-…" }'));
83
- console.log(dim(" Or set env vars: UNOROUTER_API_KEY, OPENROUTER_API_KEY, NVIDIA_API_KEY, GEMINI_API_KEY"));
111
+ console.log(dim(' { "unorouterApiKey": "your-key", "openrouterApiKey": "sk-or-v1-…" }'));
112
+ console.log(dim(" Or set the provider environment variables configured for your deployment."));
84
113
  console.log("");
85
114
  }
86
115
  export async function startRepl(opts) {
87
116
  const rl = createInterface({ input, output });
88
117
  rl.setPrompt(PROMPT);
89
118
  let history = [];
90
- console.log(dim(" Type a message and press Enter. /help for commands · Ctrl+C to quit."));
119
+ let mode = "build";
120
+ let busy = false;
121
+ let controller;
122
+ console.log(dim(" Type a message and press Enter. /help for commands · Ctrl+C cancels a request; press again to quit."));
91
123
  rl.prompt();
92
124
  rl.on("SIGINT", () => {
125
+ if (controller && !controller.signal.aborted) {
126
+ controller.abort(new Error("Request cancelled."));
127
+ return;
128
+ }
93
129
  console.log("");
94
- console.log(dim("Bye! 🦈"));
130
+ console.log(dim("Bye!"));
95
131
  process.exit(0);
96
132
  });
97
133
  rl.on("line", async (raw) => {
134
+ if (busy)
135
+ return;
98
136
  const line = raw.trim();
99
137
  if (!line) {
100
138
  if (!replClosed(rl))
@@ -103,53 +141,75 @@ export async function startRepl(opts) {
103
141
  }
104
142
  // Commands
105
143
  if (line.startsWith("/")) {
106
- const [cmd, ...rest] = line.slice(1).split(/\s+/);
107
- switch (cmd) {
108
- case "help":
109
- case "h":
110
- printHelp();
111
- break;
112
- case "model":
113
- case "m":
114
- if (rest.length)
115
- switchModel(rest.join(" "));
116
- else
117
- printModelInfo();
118
- break;
119
- case "keys":
120
- case "key":
121
- console.log(dim(` Opening local key vault… ${await launchKeysPage()}`));
122
- console.log(dim(" The browser page asks for a password before showing keys."));
123
- break;
124
- case "key-status":
125
- case "keystatus":
126
- printKeys();
127
- break;
128
- case "clear":
129
- history = [];
130
- process.stdout.write("\u001b[2J\u001b[H");
131
- break;
132
- case "setup":
133
- rl.pause();
134
- await runSetup();
144
+ busy = true;
145
+ try {
146
+ const [cmd, ...rest] = line.slice(1).split(/\s+/);
147
+ switch (cmd) {
148
+ case "help":
149
+ case "h":
150
+ printHelp();
151
+ break;
152
+ case "model":
153
+ case "m":
154
+ if (rest.length)
155
+ switchModel(rest.join(" "));
156
+ else
157
+ printModelInfo();
158
+ break;
159
+ case "keys":
160
+ case "key":
161
+ console.log(dim(` Opening local key vault… ${await launchKeysPage()}`));
162
+ console.log(dim(" The browser page asks for a password before showing keys."));
163
+ break;
164
+ case "key-status":
165
+ case "keystatus":
166
+ printKeys();
167
+ break;
168
+ case "clear":
169
+ history = [];
170
+ process.stdout.write("\u001b[2J\u001b[H");
171
+ break;
172
+ case "setup":
173
+ rl.pause();
174
+ await runSetup();
175
+ rl.resume();
176
+ console.log(dim(" Provider chain reloaded."));
177
+ break;
178
+ case "plan":
179
+ mode = "plan";
180
+ console.log(dim(" Plan mode enabled. The agent will inspect and explain without changing files."));
181
+ break;
182
+ case "build":
183
+ mode = "build";
184
+ console.log(dim(" Build mode enabled. The agent can implement changes and verify them."));
185
+ break;
186
+ case "quit":
187
+ case "exit":
188
+ case "q":
189
+ console.log(dim("Bye!"));
190
+ process.exit(0);
191
+ break;
192
+ default:
193
+ console.log(dim(` Unknown command "${cmd}". Try /help.`));
194
+ }
195
+ }
196
+ catch (error) {
197
+ console.log(hex("#f87171", " " + errorMessage(error)));
198
+ }
199
+ finally {
200
+ busy = false;
201
+ if (!replClosed(rl))
135
202
  rl.resume();
136
- console.log(dim(" Provider chain reloaded."));
137
- break;
138
- case "quit":
139
- case "exit":
140
- case "q":
141
- console.log(dim("Bye! 🦈"));
142
- process.exit(0);
143
- break;
144
- default:
145
- console.log(dim(` Unknown command "${cmd}". Try /help.`));
146
203
  }
147
204
  if (!replClosed(rl))
148
205
  rl.prompt();
149
206
  return;
150
207
  }
151
208
  // Agent run
152
- rl.pause();
209
+ busy = true;
210
+ controller = new AbortController();
211
+ // Keep input active so Ctrl+C can cancel while the model is working.
212
+ rl.resume();
153
213
  console.log("");
154
214
  const events = {
155
215
  onText: (delta) => process.stdout.write(delta),
@@ -159,10 +219,16 @@ export async function startRepl(opts) {
159
219
  onDebug: (msg) => console.log(dim(msg)),
160
220
  };
161
221
  try {
222
+ const cfg = loadConfig();
162
223
  const result = await runAgent(line, {
163
224
  clients: opts.getClients(),
164
225
  registry: opts.registry,
165
226
  cwd: opts.cwd,
227
+ systemPrompt: cfg.systemPrompt ?? defaultSystemPrompt(opts.cwd, mode),
228
+ readOnly: mode === "plan",
229
+ maxIterations: cfg.maxIterations,
230
+ approveToolCall: (call) => approveToolCall(rl, call, controller?.signal),
231
+ signal: controller.signal,
166
232
  debug: opts.debug,
167
233
  initialMessages: history,
168
234
  }, events);
@@ -190,6 +256,8 @@ export async function startRepl(opts) {
190
256
  console.log(hex("#f87171", ` ✗ ${errorMessage(e)}`));
191
257
  }
192
258
  }
259
+ busy = false;
260
+ controller = undefined;
193
261
  console.log("");
194
262
  if (!replClosed(rl)) {
195
263
  rl.resume();
package/dist/setup.js CHANGED
@@ -2,9 +2,11 @@ import { createInterface } from "node:readline/promises";
2
2
  import { stdin as input, stdout as output } from "node:process";
3
3
  import { exec } from "node:child_process";
4
4
  import { bold, dim, hex } from "./ansi.js";
5
- import { configPath, hasApiKey, loadConfig, saveConfig } from "./config.js";
5
+ import { DEFAULT_GEMINI_MODEL, configPath, hasApiKey, loadConfig, saveConfig } from "./config.js";
6
6
  import { createUnoRouterClient } from "./provider/unorouter.js";
7
7
  import { createGeminiClient } from "./provider/gemini.js";
8
+ import { createNvidiaClient } from "./provider/nvidia.js";
9
+ import { createOpenRouterClient } from "./provider/openrouter.js";
8
10
  import { ollamaAvailable } from "./provider/ollama.js";
9
11
  import { MODELS } from "./models.js";
10
12
  import { errorMessage } from "./provider/types.js";
@@ -79,7 +81,7 @@ function saveDone(cfg) {
79
81
  export async function runSetup() {
80
82
  const rl = createInterface({ input, output });
81
83
  try {
82
- await runSetupFlow(loadConfig(), rl, { title: "🦈 CodeShark setup" });
84
+ await runSetupFlow(loadConfig(), rl, { title: "CodeShark setup" });
83
85
  }
84
86
  finally {
85
87
  rl.close();
@@ -92,7 +94,7 @@ export async function runSetup() {
92
94
  export async function runSetupFlow(cfg, rl, opts = {}) {
93
95
  const askSecret = secretQuestion(rl);
94
96
  console.log("");
95
- console.log(bold(hex("#d96b43", opts.title ?? "🦈 CodeShark setup")));
97
+ console.log(bold(hex("#d96b43", opts.title ?? "CodeShark setup")));
96
98
  console.log("");
97
99
  console.log(bold(" How do you want to talk to the models?"));
98
100
  console.log(dim(" [1] Shared CodeShark gateway — zero setup. No key on this computer;"));
@@ -126,17 +128,57 @@ export async function runSetupFlow(cfg, rl, opts = {}) {
126
128
  console.log("");
127
129
  return true;
128
130
  }
129
- // Choice 2 — the user's own key (typed into the terminal, never echoed).
130
- console.log(bold(" Step 1 UnoRouter key (unlocks all 5 models)"));
131
- console.log(dim(" 1. Open https://unorouter.com/en/tokens (sign up with Discord/GitHub, no card)"));
132
- console.log(dim(" 2. Create an API key on the Tokens page — it is shown exactly once — and copy it"));
131
+ // Choice 2 — choose which provider key to configure.
132
+ console.log(bold(" Choose an API provider"));
133
+ console.log(dim(" [1] Primary model API eight free catalog models"));
134
+ console.log(dim(" [2] Gemini API"));
135
+ console.log(dim(" [3] NVIDIA NIM"));
136
+ console.log(dim(" [4] OpenRouter"));
137
+ console.log(dim(" [5] Cancel"));
138
+ const keyChoice = (await rl.question(" Choose [1-5]: ")).trim();
139
+ if (keyChoice === "5")
140
+ return false;
141
+ if (keyChoice === "2") {
142
+ const gk = await askSecret(" Paste your Gemini API key (Enter to skip): ");
143
+ if (!gk)
144
+ return false;
145
+ cfg.geminiApiKey = gk;
146
+ cfg.provider = "gemini";
147
+ cfg.model = DEFAULT_GEMINI_MODEL;
148
+ await testClient("Gemini", createGeminiClient(cfg, gk));
149
+ saveDone(cfg);
150
+ return true;
151
+ }
152
+ if (keyChoice === "3") {
153
+ const nk = await askSecret(" Paste your NVIDIA API key (Enter to skip): ");
154
+ if (!nk)
155
+ return false;
156
+ cfg.nvidiaApiKey = nk;
157
+ cfg.provider = "nvidia";
158
+ await testClient("NVIDIA", createNvidiaClient(cfg, nk));
159
+ saveDone(cfg);
160
+ return true;
161
+ }
162
+ if (keyChoice === "4") {
163
+ const ok = await askSecret(" Paste your OpenRouter API key (Enter to skip): ");
164
+ if (!ok)
165
+ return false;
166
+ cfg.openrouterApiKey = ok;
167
+ cfg.provider = "openrouter";
168
+ await testClient("OpenRouter", createOpenRouterClient(cfg, ok));
169
+ saveDone(cfg);
170
+ return true;
171
+ }
172
+ console.log(bold(" Step 1 — Model API key (unlocks all 8 free models)"));
173
+ console.log(dim(" 1. Open the provider token page shown by your administrator"));
174
+ console.log(dim(" 2. Create an API key — it is shown exactly once — and copy it"));
133
175
  console.log(dim(" Your key is typed hidden: it will not appear on screen."));
134
176
  openBrowser("https://unorouter.com/en/tokens");
135
- const urKey = await askSecret(" Paste your UnoRouter key (Enter to skip): ");
177
+ const urKey = await askSecret(" Paste your model API key (Enter to skip): ");
136
178
  if (urKey) {
137
179
  cfg.unorouterApiKey = urKey;
138
180
  cfg.provider = "unorouter";
139
- await testClient("UnoRouter (GLM 5.3 Flash Thinking)", createUnoRouterClient(cfg, urKey));
181
+ await testClient("Model provider (GLM 5.3 Flash Think Search)", createUnoRouterClient(cfg, urKey));
140
182
  await pickModel(rl, cfg);
141
183
  saveDone(cfg);
142
184
  return true;
@@ -147,7 +189,9 @@ export async function runSetupFlow(cfg, rl, opts = {}) {
147
189
  if (gk) {
148
190
  cfg.geminiApiKey = gk;
149
191
  cfg.provider = "gemini";
192
+ cfg.model = DEFAULT_GEMINI_MODEL;
150
193
  await testClient("Gemini", createGeminiClient(cfg, gk));
194
+ await pickModel(rl, cfg);
151
195
  saveDone(cfg);
152
196
  return true;
153
197
  }
package/dist/system.js CHANGED
@@ -1,17 +1,35 @@
1
- export function defaultSystemPrompt(cwd) {
2
- return `You are CodeShark, a friendly, capable coding agent running inside the user's terminal. Your mascot is a pixel-art shark.
1
+ export function defaultSystemPrompt(cwd, mode = "build") {
2
+ const modeInstructions = mode === "plan"
3
+ ? `
4
+ Current mode: PLAN
5
+ - Inspect the relevant files and use read-only tools as needed.
6
+ - Do not edit, create, delete, install, commit, or otherwise mutate anything.
7
+ - Return a practical implementation plan with the controlling files, assumptions, risks, exact change sequence, and verification commands.`
8
+ : `
9
+ Current mode: BUILD
10
+ - Implement the user's request completely with focused edits.
11
+ - Verify the work with the narrowest useful tests or typechecks, then review the result for regressions before reporting completion.`;
12
+ return `You are CodeShark, a senior software engineer working directly in the user's terminal. Your job is to produce correct, maintainable, verified results, not merely plausible suggestions.
3
13
 
4
14
  Working directory: ${cwd}
15
+ ${modeInstructions}
5
16
 
6
- Rules:
7
- - Use your tools to inspect the project before making changes. Prefer reading the relevant files over guessing.
8
- - For existing files, make surgical edits with edit_file (oldString must match exactly once). Use write_file only to create new files or fully replace tiny ones.
9
- - Use run_command for anything a shell can do: tests, builds, git status, package installs. Prefer read-only commands (git status, git diff, npm test) over destructive ones.
10
- - After editing code, verify it: run the typecheck/tests if the project has them, and report the result honestly.
11
- - Be concise. Answer in the user's language unless asked otherwise.
12
- - Work step by step. If a step fails, diagnose with tools and retry with a different approach.
13
- - Never claim you ran a command or edited a file unless you actually did through your tools.
14
- - When the task is complete, call the "finish" tool with a one or two sentence summary instead of typing it as chat text.
17
+ Engineering protocol:
18
+ 1. Translate the request into concrete acceptance criteria. Resolve ambiguity from nearby code, tests, and configuration before guessing.
19
+ 2. Inspect first. Read the owning implementation, its call sites, and the nearest relevant tests. Keep exploration focused on the requested behavior.
20
+ 3. State a short working hypothesis internally, then make the smallest change that tests it. Preserve existing APIs, conventions, and unrelated user changes.
21
+ 4. Use the tools deliberately: read/search to gather evidence, edit_file for precise existing-file changes, write_file for new files, and run_command for validation. Stay inside the working directory.
22
+ 5. Treat tool results as evidence. Check paths, types, return values, error cases, platform behavior, and security boundaries. Never invent output.
23
+ 6. Validate after changes. Prefer a focused test first, then the project's typecheck/build/test command when relevant. If validation fails, diagnose the failure, repair the same slice, and rerun it.
24
+ 7. Before finishing, review the changed behavior for regressions, missing edge cases, stale documentation, and unnecessary scope. Report remaining risk or unavailable checks honestly.
25
+
26
+ Safety and quality rules:
27
+ - Never edit, delete, install, commit, push, or run a mutating command without the user's approval prompt being accepted.
28
+ - Never weaken folder boundaries, secret handling, approval checks, or dangerous-command protections to make a task pass.
29
+ - Do not expose secrets, fabricate citations, or claim work you did not perform.
30
+ - Do not make speculative broad refactors. Prefer a small complete fix over many clever abstractions.
31
+ - Keep responses concise and in the user's language. Explain decisions and tradeoffs when they affect behavior.
32
+ - When the task is complete, call the "finish" tool with a one- or two-sentence factual summary instead of repeating the whole transcript.
15
33
 
16
34
  If the user's request is a simple question that needs no tools, just answer it directly.`;
17
35
  }
@@ -19,13 +19,14 @@ function looksBinary(text) {
19
19
  }
20
20
  export const readFileTool = {
21
21
  name: "read_file",
22
+ readOnly: true,
22
23
  description: "Read a text file. Use offset (1-based line number) and limit (number of lines) to read large files in windows. Output is line-numbered.",
23
24
  inputSchema: {
24
25
  type: "object",
25
26
  properties: {
26
27
  path: { type: "string", description: "Path to the file, absolute or relative to the working directory." },
27
28
  offset: { type: "integer", description: "1-based starting line. Default 1." },
28
- limit: { type: "integer", description: "Max lines to return. Default: whole file." },
29
+ limit: { type: "integer", description: "Max lines to return. Default 400." },
29
30
  },
30
31
  required: ["path"],
31
32
  },
@@ -37,14 +38,14 @@ export const readFileTool = {
37
38
  if (st.isDirectory())
38
39
  throw new Error(`${prettyPath(p, ctx.cwd)} is a directory — use list_directory instead.`);
39
40
  if (st.size > MAX_READ_BYTES) {
40
- throw new Error(`File is ${Math.round(st.size / 1024 / 1024)} MB — too large to read whole. Use offset/limit to page through it.`);
41
+ throw new Error(`File is ${Math.round(st.size / 1024 / 1024)} MB — too large for the text reader. Use a targeted shell command to inspect it.`);
41
42
  }
42
43
  const text = readFileSync(p, "utf8");
43
44
  if (looksBinary(text))
44
45
  throw new Error(`File appears to be binary — refusing to read as text.`);
45
46
  const lines = text.split("\n");
46
47
  const start = args.offset ? Math.max(1, Number(args.offset)) : 1;
47
- const end = args.limit ? Math.min(lines.length, start + Number(args.limit) - 1) : lines.length;
48
+ const end = Math.min(lines.length, start + Number(args.limit ?? 400) - 1);
48
49
  const body = lines
49
50
  .slice(start - 1, end)
50
51
  .map((line, i) => `${String(start + i).padStart(5)} | ${line}`)
@@ -106,7 +107,7 @@ export const editFileTool = {
106
107
  count++;
107
108
  if (idx === -1)
108
109
  idx = hit;
109
- from = hit + oldString.length;
110
+ from = hit + 1;
110
111
  }
111
112
  if (count === 0) {
112
113
  throw new Error(`oldString not found in ${prettyPath(p, ctx.cwd)}. It must match the file exactly — check whitespace/indentation.`);
@@ -122,6 +123,7 @@ export const editFileTool = {
122
123
  };
123
124
  export const listDirectoryTool = {
124
125
  name: "list_directory",
126
+ readOnly: true,
125
127
  description: "List the files and subdirectories in a directory. Directories are suffixed with '/'.",
126
128
  inputSchema: {
127
129
  type: "object",
@@ -206,33 +208,28 @@ export function globToRegExp(glob) {
206
208
  return new RegExp(re + "$");
207
209
  }
208
210
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", ".next", "target", "vendor", "__pycache__"]);
209
- function walkFiles(dir, out, depth) {
210
- if (depth > 12)
211
+ function* walkFiles(dir, depth) {
212
+ if (depth > 30)
211
213
  return;
212
214
  let entries;
213
215
  try {
214
- entries = readdirSync(dir, { withFileTypes: true });
216
+ entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
215
217
  }
216
218
  catch {
217
219
  return;
218
220
  }
219
221
  for (const e of entries) {
220
- if (out.length >= MAX_GLOB_MATCHES)
221
- return;
222
222
  if (e.name.startsWith("."))
223
- continue; // skip hidden
224
- if (e.isDirectory()) {
225
- if (SKIP_DIRS.has(e.name))
226
- continue;
227
- walkFiles(resolve(dir, e.name), out, depth + 1);
228
- }
229
- else if (e.isFile()) {
230
- out.push(resolve(dir, e.name));
231
- }
223
+ continue;
224
+ if (e.isDirectory() && !SKIP_DIRS.has(e.name))
225
+ yield* walkFiles(resolve(dir, e.name), depth + 1);
226
+ else if (e.isFile())
227
+ yield resolve(dir, e.name);
232
228
  }
233
229
  }
234
230
  export const globTool = {
235
231
  name: "glob",
232
+ readOnly: true,
236
233
  description: "Find files matching a glob pattern, e.g. \"src/**/*.ts\" or \"*.json\". Skips node_modules, .git, and hidden files by default.",
237
234
  inputSchema: {
238
235
  type: "object",
@@ -247,26 +244,27 @@ export const globTool = {
247
244
  if (!pattern)
248
245
  return "pattern is required.";
249
246
  const cwd = args.cwd ? resolvePath(args.cwd, ctx.cwd) : ctx.cwd;
250
- const staticPrefix = pattern.slice(0, pattern.indexOf("*"));
251
- const prefix = staticPrefix.split(/[\\/]/).slice(0, -1).join("/") || ".";
252
- const searchRoot = resolveProjectPath(prefix, cwd);
253
- if (!statSync(searchRoot, { throwIfNoEntry: false })?.isDirectory()) {
254
- return `No matches for ${pattern}`;
255
- }
256
- const all = [];
257
- walkFiles(searchRoot, all, 0);
258
247
  const rx = globToRegExp(pattern.replace(/\\/g, "/"));
259
- const matches = all
260
- .map((f) => relative(cwd, f).replace(/\\/g, "/"))
261
- .filter((f) => rx.test(f))
262
- .slice(0, MAX_GLOB_MATCHES);
248
+ const matches = [];
249
+ let truncated = false;
250
+ for (const file of walkFiles(cwd, 0)) {
251
+ const name = relative(cwd, file).replace(/\\/g, "/");
252
+ if (!rx.test(name))
253
+ continue;
254
+ if (matches.length === MAX_GLOB_MATCHES) {
255
+ truncated = true;
256
+ break;
257
+ }
258
+ matches.push(name);
259
+ }
263
260
  if (!matches.length)
264
261
  return `No matches for ${pattern}`;
265
- return `${matches.length} match${matches.length === 1 ? "" : "es"} for ${pattern}:\n${matches.join("\n")}`;
262
+ return `${matches.length} match${matches.length === 1 ? "" : "es"} for ${pattern}:\n${matches.join("\n")}${truncated ? "\n... More matches exist; narrow the pattern." : ""}`;
266
263
  },
267
264
  };
268
265
  export const finishTool = {
269
266
  name: "finish",
267
+ readOnly: true,
270
268
  description: "Signal that the task is complete. Call this with a short summary of what was done instead of answering in chat text.",
271
269
  inputSchema: {
272
270
  type: "object",
@@ -1,3 +1,6 @@
1
+ function bounded(text) {
2
+ return text.length > 30_000 ? text.slice(0, 30_000) + "\n[Output truncated; narrow the request or read a smaller window.]" : text;
3
+ }
1
4
  export class ToolRegistry {
2
5
  tools = new Map();
3
6
  add(tool) {
@@ -7,8 +10,8 @@ export class ToolRegistry {
7
10
  names() {
8
11
  return [...this.tools.keys()];
9
12
  }
10
- schemas() {
11
- return [...this.tools.values()].map((t) => ({
13
+ schemas(readOnly = false) {
14
+ return [...this.tools.values()].filter((t) => !readOnly || t.readOnly).map((t) => ({
12
15
  name: t.name,
13
16
  description: t.description,
14
17
  inputSchema: t.inputSchema,
@@ -23,12 +26,32 @@ export class ToolRegistry {
23
26
  };
24
27
  }
25
28
  try {
29
+ ctx.signal?.throwIfAborted();
30
+ if (ctx.readOnly && !tool.readOnly)
31
+ throw new Error("Plan mode is read-only. Switch to /build before changing files or running commands.");
32
+ if (!args || typeof args !== "object" || Array.isArray(args))
33
+ throw new Error("Arguments must be a JSON object.");
34
+ const required = tool.inputSchema.required;
35
+ for (const key of required ?? []) {
36
+ if (args[key] === undefined)
37
+ throw new Error(`Missing required argument: ${key}`);
38
+ }
39
+ const properties = tool.inputSchema.properties;
40
+ for (const [key, value] of Object.entries(args)) {
41
+ const type = properties?.[key]?.type;
42
+ if (type === "string" && typeof value !== "string")
43
+ throw new Error(`${key} must be a string.`);
44
+ if (type === "integer" && (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)) {
45
+ throw new Error(`${key} must be a positive integer.`);
46
+ }
47
+ }
26
48
  const content = await tool.run(args, ctx);
27
- return { content: String(content), isError: false };
49
+ const text = String(content);
50
+ return { content: bounded(text), isError: false };
28
51
  }
29
52
  catch (e) {
30
53
  const msg = e instanceof Error ? e.message : String(e);
31
- return { content: `Tool ${name} failed: ${msg}`, isError: true };
54
+ return { content: bounded(`Tool ${name} failed: ${msg}`), isError: true };
32
55
  }
33
56
  }
34
57
  }