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/PRIVACY.md +73 -0
- package/README.md +79 -214
- package/TERMS.md +34 -15
- package/dist/agent.js +33 -3
- package/dist/banner.js +130 -71
- package/dist/config.js +35 -8
- package/dist/index.js +85 -9
- package/dist/keysPage.js +3 -3
- package/dist/modelAvailability.js +86 -0
- package/dist/models.js +49 -16
- package/dist/project.js +20 -6
- package/dist/provider/index.js +7 -1
- package/dist/provider/nvidia.js +1 -1
- package/dist/provider/openaiCompat.js +104 -50
- package/dist/repl.js +119 -51
- package/dist/setup.js +53 -9
- package/dist/system.js +29 -11
- package/dist/tools/files.js +29 -31
- package/dist/tools/registry.js +27 -4
- package/dist/tools/search.js +17 -6
- package/dist/tools/shell.js +40 -8
- package/dist/update.js +43 -0
- package/package.json +8 -8
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
|
-
|
|
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
|
|
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
|
|
42
|
-
|
|
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>
|
|
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("○")}
|
|
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": "
|
|
83
|
-
console.log(dim(" Or set
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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: "
|
|
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 ?? "
|
|
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 —
|
|
130
|
-
console.log(bold("
|
|
131
|
-
console.log(dim(" 1
|
|
132
|
-
console.log(dim(" 2
|
|
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
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
}
|
package/dist/tools/files.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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 +
|
|
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,
|
|
210
|
-
if (depth >
|
|
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;
|
|
224
|
-
if (e.isDirectory())
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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 =
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
.
|
|
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",
|
package/dist/tools/registry.js
CHANGED
|
@@ -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
|
-
|
|
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}
|
|
54
|
+
return { content: bounded(`Tool ${name} failed: ${msg}`), isError: true };
|
|
32
55
|
}
|
|
33
56
|
}
|
|
34
57
|
}
|