codeshark-cli 0.1.5 → 0.1.7
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/agent.js +1 -0
- package/dist/index.js +34 -19
- package/dist/loading.js +69 -0
- package/dist/modelAvailability.js +46 -8
- package/dist/repl.js +39 -5
- package/package.json +1 -1
package/dist/agent.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
4
4
|
import { stdin as input, stdout as output } from "node:process";
|
|
5
5
|
import { bold, dim, hex } from "./ansi.js";
|
|
6
6
|
import { printBanner } from "./banner.js";
|
|
7
|
-
import { showLoading,
|
|
7
|
+
import { showLoading, startActivityIndicator, toolPhase } from "./loading.js";
|
|
8
8
|
import { loadConfig, saveConfig } from "./config.js";
|
|
9
9
|
import { readTerms } from "./terms.js";
|
|
10
10
|
import { resolveClients } from "./provider/index.js";
|
|
@@ -77,35 +77,48 @@ async function runOneShot(prompt) {
|
|
|
77
77
|
const cwd = process.cwd();
|
|
78
78
|
const registry = createRegistry();
|
|
79
79
|
const clients = resolveClients(cfg, (m) => console.error(dim(m)));
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// A live indicator shows what the agent is doing (Thinking, Reading
|
|
81
|
+
// files, Running commands, …) until the first token arrives, then the
|
|
82
|
+
// answer streams over it.
|
|
83
|
+
let activityActive = true;
|
|
84
|
+
const stopActivity = () => {
|
|
85
|
+
if (activityActive) {
|
|
86
|
+
activityActive = false;
|
|
87
|
+
activity.stop();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const activity = startActivityIndicator("Thinking");
|
|
83
91
|
const events = {
|
|
84
92
|
onText: (d) => {
|
|
85
|
-
|
|
86
|
-
thinking = false;
|
|
87
|
-
stopThinking();
|
|
88
|
-
}
|
|
93
|
+
stopActivity();
|
|
89
94
|
process.stdout.write(d);
|
|
90
95
|
},
|
|
96
|
+
onToolCall: (call) => {
|
|
97
|
+
activity.setPhase(toolPhase(call.name));
|
|
98
|
+
},
|
|
99
|
+
onToolResult: () => {
|
|
100
|
+
activity.setPhase("Thinking");
|
|
101
|
+
},
|
|
91
102
|
};
|
|
92
103
|
const approvalRl = process.stdin.isTTY ? createInterface({ input, output }) : undefined;
|
|
93
104
|
try {
|
|
94
105
|
const result = await runAgent(prompt, {
|
|
95
106
|
clients,
|
|
96
107
|
registry,
|
|
97
|
-
cwd,
|
|
98
|
-
approveToolCall: approvalRl
|
|
108
|
+
cwd, approveToolCall: approvalRl
|
|
99
109
|
? async (call) => {
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
activity.pause();
|
|
111
|
+
try {
|
|
112
|
+
const answer = await approvalRl.question(`\nApprove ${call.name}? [y/N]: `);
|
|
113
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
activity.resume(toolPhase(call.name));
|
|
117
|
+
}
|
|
102
118
|
}
|
|
103
119
|
: async () => false,
|
|
104
120
|
}, events);
|
|
105
|
-
|
|
106
|
-
thinking = false;
|
|
107
|
-
stopThinking();
|
|
108
|
-
}
|
|
121
|
+
stopActivity();
|
|
109
122
|
if (result.streamedText) {
|
|
110
123
|
if (!result.streamedText.endsWith("\n"))
|
|
111
124
|
process.stdout.write("\n");
|
|
@@ -115,8 +128,7 @@ async function runOneShot(prompt) {
|
|
|
115
128
|
}
|
|
116
129
|
}
|
|
117
130
|
catch (e) {
|
|
118
|
-
|
|
119
|
-
stopThinking();
|
|
131
|
+
stopActivity();
|
|
120
132
|
console.error(hex("#f87171", `✗ ${errorMessage(e)}`));
|
|
121
133
|
process.exitCode = 1;
|
|
122
134
|
}
|
|
@@ -250,7 +262,10 @@ async function main() {
|
|
|
250
262
|
if (status === "available") {
|
|
251
263
|
console.log(` ${hex("#4ade80", "✓ Available")} ${model.label}`);
|
|
252
264
|
}
|
|
253
|
-
else {
|
|
265
|
+
else if (status === "busy") {
|
|
266
|
+
console.log(` ${hex("#fbbf24", "~ Lane busy")} ${model.label}${reason ? dim(` — ${reason}`) : ""}`);
|
|
267
|
+
}
|
|
268
|
+
else if (status === "unavailable") {
|
|
254
269
|
console.log(` ${hex("#f87171", "✗ Unavailable")} ${model.label}${reason ? dim(` — ${reason}`) : ""}`);
|
|
255
270
|
}
|
|
256
271
|
}
|
package/dist/loading.js
CHANGED
|
@@ -61,3 +61,72 @@ export function startThinkingSpinner(label) {
|
|
|
61
61
|
process.stdout.write("\r\u001b[2K");
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
+
/** Human phase label for a tool call — shown by activity indicators. */
|
|
65
|
+
export function toolPhase(name) {
|
|
66
|
+
switch (name) {
|
|
67
|
+
case "run_command":
|
|
68
|
+
return "Running command";
|
|
69
|
+
case "read_file":
|
|
70
|
+
return "Reading files";
|
|
71
|
+
case "write_file":
|
|
72
|
+
return "Writing files";
|
|
73
|
+
case "edit_file":
|
|
74
|
+
return "Editing files";
|
|
75
|
+
case "glob":
|
|
76
|
+
return "Finding files";
|
|
77
|
+
case "list_directory":
|
|
78
|
+
return "Listing directory";
|
|
79
|
+
case "code_search":
|
|
80
|
+
return "Searching code";
|
|
81
|
+
case "finish":
|
|
82
|
+
return "Wrapping up";
|
|
83
|
+
default:
|
|
84
|
+
return `Running ${name}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function startActivityIndicator(initialPhase = "Thinking") {
|
|
88
|
+
if (!canAnimate()) {
|
|
89
|
+
return { setPhase: () => { }, pause: () => { }, resume: () => { }, stop: () => { } };
|
|
90
|
+
}
|
|
91
|
+
let phase = initialPhase;
|
|
92
|
+
let frame = 0;
|
|
93
|
+
let paused = false;
|
|
94
|
+
let pausedLine = "";
|
|
95
|
+
const startedAt = Date.now();
|
|
96
|
+
const render = (pausedText) => {
|
|
97
|
+
const seconds = Math.floor((Date.now() - startedAt) / 1000);
|
|
98
|
+
const suffix = pausedText ?? `${phase} · ${seconds}s`;
|
|
99
|
+
process.stdout.write(`\r\u001b[2K ${hex(ACCENT, FRAMES[frame % FRAMES.length])} ${suffix}`);
|
|
100
|
+
};
|
|
101
|
+
render();
|
|
102
|
+
const timer = setInterval(() => {
|
|
103
|
+
frame++;
|
|
104
|
+
if (!paused)
|
|
105
|
+
render();
|
|
106
|
+
}, 90);
|
|
107
|
+
const clearLine = () => process.stdout.write("\r\u001b[2K");
|
|
108
|
+
return {
|
|
109
|
+
setPhase(next) {
|
|
110
|
+
phase = next;
|
|
111
|
+
if (!paused)
|
|
112
|
+
render();
|
|
113
|
+
},
|
|
114
|
+
pause() {
|
|
115
|
+
if (paused)
|
|
116
|
+
return;
|
|
117
|
+
paused = true;
|
|
118
|
+
const seconds = Math.floor((Date.now() - startedAt) / 1000);
|
|
119
|
+
pausedLine = `${phase} · ${seconds}s — waiting for your approval`;
|
|
120
|
+
render(hex("#fbbf24", `⏸ ${pausedLine}`));
|
|
121
|
+
},
|
|
122
|
+
resume(next) {
|
|
123
|
+
phase = next;
|
|
124
|
+
paused = false;
|
|
125
|
+
render();
|
|
126
|
+
},
|
|
127
|
+
stop() {
|
|
128
|
+
clearInterval(timer);
|
|
129
|
+
clearLine();
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -2,7 +2,9 @@ import { DEFAULT_GATEWAY_URL, envApiKey } from "./config.js";
|
|
|
2
2
|
import { MODELS } from "./models.js";
|
|
3
3
|
const statuses = new Map();
|
|
4
4
|
const reasons = new Map();
|
|
5
|
-
const TIMEOUT_MS =
|
|
5
|
+
const TIMEOUT_MS = 15_000;
|
|
6
|
+
/** Match the gateway's per-IP inflight cap so probes never queue behind themselves. */
|
|
7
|
+
const MAX_CONCURRENT_CHECKS = 2;
|
|
6
8
|
export function modelAvailability(modelId) {
|
|
7
9
|
return statuses.get(modelId) ?? "checking";
|
|
8
10
|
}
|
|
@@ -16,6 +18,25 @@ export function modelAvailabilitySnapshot() {
|
|
|
16
18
|
reason: modelAvailabilityReason(model.id),
|
|
17
19
|
}));
|
|
18
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Classify a failed probe. "busy" covers transient lane problems (rate
|
|
23
|
+
* limits, upstream hiccups, timeouts) where the model is likely fine but
|
|
24
|
+
* the shared free lane is momentarily overloaded — those must NOT read as
|
|
25
|
+
* "model removed". Only explicit client errors mean the model itself is
|
|
26
|
+
* not servable right now.
|
|
27
|
+
*/
|
|
28
|
+
export function classifyProbeFailure(statusOrError) {
|
|
29
|
+
if (statusOrError instanceof Error) {
|
|
30
|
+
if (statusOrError.name === "AbortError")
|
|
31
|
+
return { status: "busy", reason: "lane busy — timed out" };
|
|
32
|
+
return { status: "busy", reason: statusOrError.message || "network error" };
|
|
33
|
+
}
|
|
34
|
+
if (statusOrError === 429)
|
|
35
|
+
return { status: "busy", reason: "rate limited — try again shortly" };
|
|
36
|
+
if (statusOrError >= 500)
|
|
37
|
+
return { status: "busy", reason: `lane busy — HTTP ${statusOrError}` };
|
|
38
|
+
return { status: "unavailable", reason: `HTTP ${statusOrError}` };
|
|
39
|
+
}
|
|
19
40
|
function endpointFor(model, cfg) {
|
|
20
41
|
if (model.provider === "gemini") {
|
|
21
42
|
return {
|
|
@@ -53,8 +74,12 @@ async function checkOne(model, cfg) {
|
|
|
53
74
|
body: JSON.stringify({ contents: [{ role: "user", parts: [{ text: "Reply with OK." }] }] }),
|
|
54
75
|
signal: controller.signal,
|
|
55
76
|
});
|
|
56
|
-
if (!response.ok)
|
|
57
|
-
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const { status, reason } = classifyProbeFailure(response.status);
|
|
79
|
+
statuses.set(model.id, status);
|
|
80
|
+
reasons.set(model.id, reason);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
58
83
|
}
|
|
59
84
|
else {
|
|
60
85
|
const response = await fetch(target.url, {
|
|
@@ -63,15 +88,20 @@ async function checkOne(model, cfg) {
|
|
|
63
88
|
body: JSON.stringify({ model: model.model, messages: [{ role: "user", content: "Reply with OK." }], stream: false, max_tokens: 4 }),
|
|
64
89
|
signal: controller.signal,
|
|
65
90
|
});
|
|
66
|
-
if (!response.ok)
|
|
67
|
-
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const { status, reason } = classifyProbeFailure(response.status);
|
|
93
|
+
statuses.set(model.id, status);
|
|
94
|
+
reasons.set(model.id, reason);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
68
97
|
}
|
|
69
98
|
statuses.set(model.id, "available");
|
|
70
99
|
reasons.delete(model.id);
|
|
71
100
|
}
|
|
72
101
|
catch (error) {
|
|
73
|
-
|
|
74
|
-
|
|
102
|
+
const { status, reason } = classifyProbeFailure(error instanceof Error ? error : new Error(String(error)));
|
|
103
|
+
statuses.set(model.id, status);
|
|
104
|
+
reasons.set(model.id, reason);
|
|
75
105
|
}
|
|
76
106
|
finally {
|
|
77
107
|
clearTimeout(timer);
|
|
@@ -82,5 +112,13 @@ export async function checkModelAvailability(cfg) {
|
|
|
82
112
|
statuses.set(model.id, "checking");
|
|
83
113
|
reasons.delete(model.id);
|
|
84
114
|
}
|
|
85
|
-
|
|
115
|
+
// Throttle to the gateway's per-IP inflight limit: firing all probes at
|
|
116
|
+
// once makes them queue behind each other and time out spuriously.
|
|
117
|
+
const queue = [...MODELS];
|
|
118
|
+
const workers = Array.from({ length: Math.min(MAX_CONCURRENT_CHECKS, queue.length) }, async () => {
|
|
119
|
+
for (let model = queue.shift(); model; model = queue.shift()) {
|
|
120
|
+
await checkOne(model, cfg);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
await Promise.all(workers);
|
|
86
124
|
}
|
package/dist/repl.js
CHANGED
|
@@ -8,6 +8,7 @@ import { loadConfig, modelLabel, saveConfig } from "./config.js";
|
|
|
8
8
|
import { DEFAULT_MODEL_ID, MODELS, findModel } from "./models.js";
|
|
9
9
|
import { launchKeysPage } from "./keysPage.js";
|
|
10
10
|
import { defaultSystemPrompt } from "./system.js";
|
|
11
|
+
import { startActivityIndicator, toolPhase } from "./loading.js";
|
|
11
12
|
import { modelAvailability, modelAvailabilityReason } from "./modelAvailability.js";
|
|
12
13
|
const PROMPT = "> ";
|
|
13
14
|
function replClosed(rl) {
|
|
@@ -48,9 +49,14 @@ export function printModelInfo() {
|
|
|
48
49
|
for (const m of MODELS) {
|
|
49
50
|
const active = m.id === (findModel(cfg.model ?? "")?.id ?? DEFAULT_MODEL_ID);
|
|
50
51
|
const status = modelAvailability(m.id);
|
|
51
|
-
const marker = status === "available" ? hex("#4ade80", "●") : status === "unavailable" ? hex("#f87171", "✗") : dim("○");
|
|
52
|
-
const note = status === "unavailable"
|
|
53
|
-
|
|
52
|
+
const marker = status === "available" ? hex("#4ade80", "●") : status === "unavailable" ? hex("#f87171", "✗") : status === "busy" ? hex("#fbbf24", "◐") : dim("○");
|
|
53
|
+
const note = status === "unavailable"
|
|
54
|
+
? `Unavailable${modelAvailabilityReason(m.id) ? ` — ${modelAvailabilityReason(m.id)}` : ""}`
|
|
55
|
+
: status === "busy"
|
|
56
|
+
? `Lane busy${modelAvailabilityReason(m.id) ? ` — ${modelAvailabilityReason(m.id)}` : ""} — should still work`
|
|
57
|
+
: m.notes;
|
|
58
|
+
const styled = status === "unavailable" ? hex("#f87171", note) : status === "busy" ? hex("#fbbf24", note) : note;
|
|
59
|
+
console.log(` ${marker} ${m.label.padEnd(28)} ${dim(m.context.padEnd(5))} ${styled}`);
|
|
54
60
|
}
|
|
55
61
|
console.log("");
|
|
56
62
|
console.log(dim(" Switch with: /model <name>"));
|
|
@@ -81,6 +87,9 @@ export function switchModel(query) {
|
|
|
81
87
|
console.log(dim(` ${modelAvailabilityReason(entry.id) ?? "The boot health check failed."}`));
|
|
82
88
|
return;
|
|
83
89
|
}
|
|
90
|
+
if (modelAvailability(entry.id) === "busy") {
|
|
91
|
+
console.log(hex("#fbbf24", ` ⚠ ${entry.label}'s shared lane is busy right now — switching anyway. Requests may wait or retry.`));
|
|
92
|
+
}
|
|
84
93
|
cfg.model = entry.id;
|
|
85
94
|
saveConfig(cfg);
|
|
86
95
|
console.log(` ${hex("#4ade80", "✓")} Switched to ${bold(entry.label)} ${dim(`(${entry.context} context)`)}`);
|
|
@@ -204,10 +213,25 @@ export async function startRepl(opts) {
|
|
|
204
213
|
// Keep input active so Ctrl+C can cancel while the model is working.
|
|
205
214
|
rl.resume();
|
|
206
215
|
console.log("");
|
|
216
|
+
const activity = startActivityIndicator("Thinking");
|
|
217
|
+
let activityActive = true;
|
|
218
|
+
const stopActivity = () => {
|
|
219
|
+
if (activityActive) {
|
|
220
|
+
activityActive = false;
|
|
221
|
+
activity.stop();
|
|
222
|
+
}
|
|
223
|
+
};
|
|
207
224
|
const events = {
|
|
208
|
-
onText: (delta) =>
|
|
225
|
+
onText: (delta) => {
|
|
226
|
+
stopActivity();
|
|
227
|
+
process.stdout.write(delta);
|
|
228
|
+
},
|
|
209
229
|
onToolCall: (call) => {
|
|
210
230
|
console.log(dim(` ⚙ ${call.name}(${briefArgs(call.args)})`));
|
|
231
|
+
activity.setPhase(toolPhase(call.name));
|
|
232
|
+
},
|
|
233
|
+
onToolResult: (toolName) => {
|
|
234
|
+
activity.setPhase(toolName === "finish" ? "Wrapping up" : "Thinking");
|
|
211
235
|
},
|
|
212
236
|
onDebug: (msg) => console.log(dim(msg)),
|
|
213
237
|
};
|
|
@@ -220,11 +244,20 @@ export async function startRepl(opts) {
|
|
|
220
244
|
systemPrompt: cfg.systemPrompt ?? defaultSystemPrompt(opts.cwd, mode),
|
|
221
245
|
readOnly: mode === "plan",
|
|
222
246
|
maxIterations: cfg.maxIterations,
|
|
223
|
-
approveToolCall: (call) =>
|
|
247
|
+
approveToolCall: async (call) => {
|
|
248
|
+
activity.pause();
|
|
249
|
+
try {
|
|
250
|
+
return await approveToolCall(rl, call, controller?.signal);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
activity.resume(toolPhase(call.name));
|
|
254
|
+
}
|
|
255
|
+
},
|
|
224
256
|
signal: controller.signal,
|
|
225
257
|
debug: opts.debug,
|
|
226
258
|
initialMessages: history,
|
|
227
259
|
}, events);
|
|
260
|
+
stopActivity();
|
|
228
261
|
history = result.history;
|
|
229
262
|
if (result.streamedText) {
|
|
230
263
|
if (!result.streamedText.endsWith("\n"))
|
|
@@ -235,6 +268,7 @@ export async function startRepl(opts) {
|
|
|
235
268
|
}
|
|
236
269
|
}
|
|
237
270
|
catch (e) {
|
|
271
|
+
stopActivity();
|
|
238
272
|
console.log("");
|
|
239
273
|
if (e instanceof ProviderError) {
|
|
240
274
|
console.log(hex("#f87171", ` ✗ ${e.message}`));
|