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 CHANGED
@@ -93,6 +93,7 @@ export async function runAgent(input, opts, events) {
93
93
  continue;
94
94
  }
95
95
  const result = await opts.registry.execute(call.name, call.args, ctx);
96
+ events?.onToolResult?.(call.name);
96
97
  messages.push({
97
98
  role: "tool",
98
99
  content: result.content,
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, startThinkingSpinner } from "./loading.js";
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
- // Show a "thinking" spinner until the first token arrives, then stream.
81
- const stopThinking = startThinkingSpinner("Thinking");
82
- let thinking = true;
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
- if (thinking) {
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
- const answer = await approvalRl.question(`\nApprove ${call.name}? [y/N]: `);
101
- return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
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
- if (thinking) {
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
- if (thinking)
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 = 8_000;
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
- throw new Error(`HTTP ${response.status}`);
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
- throw new Error(`HTTP ${response.status}`);
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
- statuses.set(model.id, "unavailable");
74
- reasons.set(model.id, error instanceof Error && error.name === "AbortError" ? "check timed out" : error instanceof Error ? error.message : String(error));
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
- await Promise.all(MODELS.map((model) => checkOne(model, cfg)));
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" ? `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}`);
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) => process.stdout.write(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) => approveToolCall(rl, call, controller?.signal),
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}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeshark-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "An open-source terminal coding agent. Eight model choices, project tools, and approval before every tool action.",
5
5
  "type": "module",
6
6
  "bin": {