codeshark-cli 0.1.4 → 0.1.6

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/config.js CHANGED
@@ -36,6 +36,9 @@ export function loadConfig() {
36
36
  delete cfg[key];
37
37
  if (cfg.provider && !["gateway", "openrouter", "nvidia", "gemini", "ollama", "unorouter"].includes(cfg.provider))
38
38
  delete cfg.provider;
39
+ // Self-heal: drop a saved model that a provider retired so the current default applies.
40
+ if (cfg.model && isRetiredModel(cfg.model))
41
+ delete cfg.model;
39
42
  return cfg;
40
43
  }
41
44
  catch {
package/dist/index.js CHANGED
@@ -250,7 +250,10 @@ async function main() {
250
250
  if (status === "available") {
251
251
  console.log(` ${hex("#4ade80", "✓ Available")} ${model.label}`);
252
252
  }
253
- else {
253
+ else if (status === "busy") {
254
+ console.log(` ${hex("#fbbf24", "~ Lane busy")} ${model.label}${reason ? dim(` — ${reason}`) : ""}`);
255
+ }
256
+ else if (status === "unavailable") {
254
257
  console.log(` ${hex("#f87171", "✗ Unavailable")} ${model.label}${reason ? dim(` — ${reason}`) : ""}`);
255
258
  }
256
259
  }
@@ -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
@@ -5,7 +5,7 @@ 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, RETIRED_MODELS, findModel, isRetiredModel } from "./models.js";
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
11
  import { modelAvailability, modelAvailabilityReason } from "./modelAvailability.js";
@@ -48,16 +48,14 @@ export function printModelInfo() {
48
48
  for (const m of MODELS) {
49
49
  const active = m.id === (findModel(cfg.model ?? "")?.id ?? DEFAULT_MODEL_ID);
50
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}.`));
51
+ const marker = status === "available" ? hex("#4ade80", "●") : status === "unavailable" ? hex("#f87171", "✗") : status === "busy" ? hex("#fbbf24", "◐") : dim("○");
52
+ const note = status === "unavailable"
53
+ ? `Unavailable${modelAvailabilityReason(m.id) ? ` — ${modelAvailabilityReason(m.id)}` : ""}`
54
+ : status === "busy"
55
+ ? `Lane busy${modelAvailabilityReason(m.id) ? ` — ${modelAvailabilityReason(m.id)}` : ""} — should still work`
56
+ : m.notes;
57
+ const styled = status === "unavailable" ? hex("#f87171", note) : status === "busy" ? hex("#fbbf24", note) : note;
58
+ console.log(` ${marker} ${m.label.padEnd(28)} ${dim(m.context.padEnd(5))} ${styled}`);
61
59
  }
62
60
  console.log("");
63
61
  console.log(dim(" Switch with: /model <name>"));
@@ -88,6 +86,9 @@ export function switchModel(query) {
88
86
  console.log(dim(` ${modelAvailabilityReason(entry.id) ?? "The boot health check failed."}`));
89
87
  return;
90
88
  }
89
+ if (modelAvailability(entry.id) === "busy") {
90
+ console.log(hex("#fbbf24", ` ⚠ ${entry.label}'s shared lane is busy right now — switching anyway. Requests may wait or retry.`));
91
+ }
91
92
  cfg.model = entry.id;
92
93
  saveConfig(cfg);
93
94
  console.log(` ${hex("#4ade80", "✓")} Switched to ${bold(entry.label)} ${dim(`(${entry.context} context)`)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeshark-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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": {