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/models.js
CHANGED
|
@@ -5,31 +5,31 @@ export const MODELS = [
|
|
|
5
5
|
provider: "unorouter",
|
|
6
6
|
model: "gpt-5.6-sol:free",
|
|
7
7
|
context: "400K",
|
|
8
|
-
notes: "
|
|
8
|
+
notes: "Frontier reasoning model when available.",
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
id: "unorouter/deepseek-v4-flash",
|
|
12
12
|
label: "DeepSeek-V4 Flash",
|
|
13
13
|
provider: "unorouter",
|
|
14
|
-
model: "deepseek-v4-flash
|
|
14
|
+
model: "deepseek-v4-flash:free",
|
|
15
15
|
context: "256K",
|
|
16
|
-
notes: "
|
|
16
|
+
notes: "Fast coding model when available.",
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
id: "unorouter/
|
|
20
|
-
label: "
|
|
19
|
+
id: "unorouter/minimax-m3",
|
|
20
|
+
label: "MiniMax M3",
|
|
21
21
|
provider: "unorouter",
|
|
22
|
-
model: "
|
|
23
|
-
context: "
|
|
24
|
-
notes: "
|
|
22
|
+
model: "minimax-m3:free",
|
|
23
|
+
context: "128K",
|
|
24
|
+
notes: "Fast general-purpose coding and reasoning.",
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
|
-
id: "unorouter/
|
|
28
|
-
label: "
|
|
27
|
+
id: "unorouter/glm-5.3-flash-think-search",
|
|
28
|
+
label: "GLM 5.3 Flash Think Search",
|
|
29
29
|
provider: "unorouter",
|
|
30
|
-
model: "
|
|
31
|
-
context: "
|
|
32
|
-
notes: "
|
|
30
|
+
model: "glm-5.3-flash-think-search:free",
|
|
31
|
+
context: "1M",
|
|
32
|
+
notes: "Reasoning model with search-oriented thinking.",
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
id: "unorouter/gemini-3.6-flash",
|
|
@@ -37,16 +37,49 @@ export const MODELS = [
|
|
|
37
37
|
provider: "unorouter",
|
|
38
38
|
model: "gemini-3.6-flash:free",
|
|
39
39
|
context: "1M",
|
|
40
|
-
notes: "
|
|
40
|
+
notes: "Fast long-context model for large codebases.",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "unorouter/sarvam-30b",
|
|
44
|
+
label: "Sarvam 30B",
|
|
45
|
+
provider: "unorouter",
|
|
46
|
+
model: "sarvam-30b:free",
|
|
47
|
+
context: "128K",
|
|
48
|
+
notes: "Open coding model from Sarvam AI.",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "unorouter/gpt-oss-120b",
|
|
52
|
+
label: "GPT-OSS 120B",
|
|
53
|
+
provider: "unorouter",
|
|
54
|
+
model: "gpt-oss-120b:free",
|
|
55
|
+
context: "128K",
|
|
56
|
+
notes: "Large open-weight model for demanding coding tasks.",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "unorouter/nemotron-3-ultra-550b-a55b",
|
|
60
|
+
label: "Nemotron 3 Ultra 550B A55B",
|
|
61
|
+
provider: "unorouter",
|
|
62
|
+
model: "nemotron-3-ultra-550b-a55b:free",
|
|
63
|
+
context: "256K",
|
|
64
|
+
notes: "Large-scale open model for deep reasoning.",
|
|
41
65
|
},
|
|
42
66
|
];
|
|
43
|
-
export const
|
|
67
|
+
export const RETIRED_MODELS = [
|
|
68
|
+
{ id: "gemini/gemini-3.8-flash", label: "Gemini 3.8 Flash", provider: "gemini", model: "gemini-3.8-flash", context: "-", notes: "Removed from the catalog.", available: false },
|
|
69
|
+
{ id: "unorouter/glm-5.3-flash-thinking", label: "GLM 5.3 Flash Thinking", provider: "unorouter", model: "glm-5.3-flash-thinking:free", context: "-", notes: "Removed from UnoRouter.", available: false },
|
|
70
|
+
{ id: "unorouter/kimi-k3", label: "Kimi-K3", provider: "unorouter", model: "kimi-k3:free", context: "-", notes: "Removed from UnoRouter.", available: false },
|
|
71
|
+
];
|
|
72
|
+
export const DEFAULT_MODEL_ID = "unorouter/glm-5.3-flash-think-search";
|
|
44
73
|
export function listModels() {
|
|
45
74
|
return [...MODELS];
|
|
46
75
|
}
|
|
76
|
+
export function isRetiredModel(idOrSlug) {
|
|
77
|
+
const s = idOrSlug.trim();
|
|
78
|
+
return RETIRED_MODELS.some((m) => m.id === s || m.model === s);
|
|
79
|
+
}
|
|
47
80
|
export function findModel(idOrSlug) {
|
|
48
81
|
const s = idOrSlug.trim();
|
|
49
|
-
return MODELS.find((m) => m.id === s || m.model === s);
|
|
82
|
+
return [...MODELS, ...RETIRED_MODELS].find((m) => m.id === s || m.model === s);
|
|
50
83
|
}
|
|
51
84
|
/** Map a catalog id (or raw slug) to the raw slug the provider API expects. */
|
|
52
85
|
export function toApiSlug(idOrSlug) {
|
package/dist/project.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { statSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
1
|
+
import { statSync, lstatSync, realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, dirname, relative, resolve, sep } from "node:path";
|
|
3
3
|
export class ProjectFolderError extends Error {
|
|
4
4
|
constructor(message) {
|
|
5
5
|
super(message);
|
|
@@ -22,10 +22,24 @@ export function requireProjectFolder(folder = process.cwd()) {
|
|
|
22
22
|
export function resolveProjectPath(input, cwd) {
|
|
23
23
|
const root = resolve(cwd);
|
|
24
24
|
const target = isAbsolute(input) ? resolve(input) : resolve(root, input);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
return
|
|
28
|
-
|
|
25
|
+
const inside = (base, candidate) => {
|
|
26
|
+
const path = relative(base, candidate);
|
|
27
|
+
return path === "" || (path !== ".." && !path.startsWith(".." + sep) && !isAbsolute(path));
|
|
28
|
+
};
|
|
29
|
+
if (!inside(root, target))
|
|
30
|
+
throw new ProjectFolderError("Path escapes the project folder: " + input);
|
|
31
|
+
// Validate existing ancestors so links cannot redirect new writes outside the project.
|
|
32
|
+
let ancestor = target;
|
|
33
|
+
while (!lstatSync(ancestor, { throwIfNoEntry: false })) {
|
|
34
|
+
const parent = dirname(ancestor);
|
|
35
|
+
if (parent === ancestor)
|
|
36
|
+
throw new ProjectFolderError("Cannot resolve project path: " + input);
|
|
37
|
+
ancestor = parent;
|
|
38
|
+
}
|
|
39
|
+
if (!inside(realpathSync(root), realpathSync(ancestor))) {
|
|
40
|
+
throw new ProjectFolderError("Path escapes the project folder through a link: " + input);
|
|
41
|
+
}
|
|
42
|
+
return target;
|
|
29
43
|
}
|
|
30
44
|
/** Change into an explicitly selected project folder before starting CodeShark. */
|
|
31
45
|
export function openProjectFolder(folder) {
|
package/dist/provider/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { activeProvider, envApiKey } from "../config.js";
|
|
1
|
+
import { activeModelId, activeProvider, envApiKey } from "../config.js";
|
|
2
2
|
import { createGatewayClient } from "./gateway.js";
|
|
3
3
|
import { createGeminiClient } from "./gemini.js";
|
|
4
4
|
import { createNvidiaClient } from "./nvidia.js";
|
|
5
5
|
import { createOllamaClient } from "./ollama.js";
|
|
6
6
|
import { createOpenRouterClient } from "./openrouter.js";
|
|
7
7
|
import { createUnoRouterClient } from "./unorouter.js";
|
|
8
|
+
import { findModel } from "../models.js";
|
|
8
9
|
/**
|
|
9
10
|
* Build the ordered list of ChatClients for this machine:
|
|
10
11
|
* the active model's provider first, then automatic free fallbacks
|
|
@@ -18,6 +19,7 @@ export function resolveClients(cfg, debug) {
|
|
|
18
19
|
};
|
|
19
20
|
const has = (provider) => clients.some((c) => c.provider === provider);
|
|
20
21
|
const primary = activeProvider(cfg);
|
|
22
|
+
const selectedModel = activeModelId(cfg);
|
|
21
23
|
switch (primary) {
|
|
22
24
|
case "openrouter": {
|
|
23
25
|
const key = cfg.openrouterApiKey ?? envApiKey("openrouter");
|
|
@@ -63,6 +65,10 @@ export function resolveClients(cfg, debug) {
|
|
|
63
65
|
add(createGatewayClient(cfg));
|
|
64
66
|
break;
|
|
65
67
|
}
|
|
68
|
+
// UnoRouter catalog selections must not silently become a different model
|
|
69
|
+
// through another provider's fallback chain.
|
|
70
|
+
if (selectedModel.startsWith("unorouter/") || findModel(selectedModel))
|
|
71
|
+
return clients;
|
|
66
72
|
// Automatic free fallbacks, deduped: any other provider you have a key for.
|
|
67
73
|
if (!has("openrouter")) {
|
|
68
74
|
const key = cfg.openrouterApiKey ?? envApiKey("openrouter");
|
package/dist/provider/nvidia.js
CHANGED
|
@@ -3,7 +3,7 @@ import { effectiveModel } from "../config.js";
|
|
|
3
3
|
import { toApiSlug } from "../models.js";
|
|
4
4
|
export const DEFAULT_NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
|
|
5
5
|
/**
|
|
6
|
-
* NVIDIA NIM hosts open models
|
|
6
|
+
* NVIDIA NIM hosts open models behind a
|
|
7
7
|
* free, OpenAI-compatible API. Get a key at https://build.nvidia.com —
|
|
8
8
|
* no credit card, keys start with `nvapi-`.
|
|
9
9
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
1
2
|
import { ProviderError, classifyStatus, errorMessage, } from "./types.js";
|
|
2
3
|
function toOpenAIMessages(messages) {
|
|
3
4
|
const out = [];
|
|
@@ -37,7 +38,7 @@ function safeParseArgs(raw) {
|
|
|
37
38
|
return {};
|
|
38
39
|
try {
|
|
39
40
|
const parsed = JSON.parse(raw);
|
|
40
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
41
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
41
42
|
}
|
|
42
43
|
catch {
|
|
43
44
|
return {};
|
|
@@ -52,57 +53,78 @@ export function createOpenAICompatClient(opts) {
|
|
|
52
53
|
model: opts.model,
|
|
53
54
|
isFree: opts.isFree ?? true,
|
|
54
55
|
async chat(messages, tools, events, signal) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
56
|
+
signal?.throwIfAborted();
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const abort = () => controller.abort(signal?.reason);
|
|
59
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
60
|
+
const timeout = setTimeout(() => controller.abort(new Error("Provider request timed out.")), opts.timeoutMs ?? 120_000);
|
|
61
|
+
try {
|
|
62
|
+
const body = {
|
|
63
|
+
model: opts.model,
|
|
64
|
+
messages: toOpenAIMessages(messages),
|
|
65
|
+
stream: true,
|
|
66
|
+
temperature: 0.3,
|
|
67
|
+
};
|
|
68
|
+
if (tools.length)
|
|
69
|
+
body.tools = toOpenAITools(tools);
|
|
70
|
+
const headers = {
|
|
71
|
+
"content-type": "application/json",
|
|
72
|
+
...(opts.apiKey ? { authorization: `Bearer ${opts.apiKey}` } : {}),
|
|
73
|
+
...opts.extraHeaders,
|
|
74
|
+
};
|
|
75
|
+
// Wait out rate limits with backoff before surfacing a 429 — the
|
|
76
|
+
// queue lives client-side too, so shared gateway lanes feel smooth.
|
|
77
|
+
const retryDelays = opts.rateLimitRetryDelays ?? [800, 1600];
|
|
78
|
+
let res;
|
|
79
|
+
for (let attempt = 0;; attempt++) {
|
|
80
|
+
let candidate;
|
|
81
|
+
try {
|
|
82
|
+
candidate = await fetchImpl(endpoint, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers,
|
|
85
|
+
body: JSON.stringify(body),
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
throw new ProviderError(`Cannot reach ${opts.provider} at ${base}: ${errorMessage(e)}`, "network");
|
|
91
|
+
}
|
|
92
|
+
if (candidate.status !== 429 || attempt >= retryDelays.length) {
|
|
93
|
+
res = candidate;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
await candidate.body?.cancel();
|
|
97
|
+
const retryAfter = candidate.headers.get("retry-after");
|
|
98
|
+
const seconds = retryAfter === null ? NaN : Number(retryAfter);
|
|
99
|
+
const suggested = Number.isFinite(seconds) ? seconds * 1000 : retryAfter ? Date.parse(retryAfter) - Date.now() : NaN;
|
|
100
|
+
const wait = Number.isFinite(suggested) ? Math.min(30_000, Math.max(0, suggested)) : retryDelays[attempt];
|
|
101
|
+
await delay(wait, undefined, { signal: controller.signal });
|
|
81
102
|
}
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
const raw = await res.text();
|
|
105
|
+
let detail = raw.slice(0, 300);
|
|
106
|
+
try {
|
|
107
|
+
const data = JSON.parse(raw);
|
|
108
|
+
if (typeof data?.error?.message === "string")
|
|
109
|
+
detail = data.error.message.slice(0, 300);
|
|
110
|
+
}
|
|
111
|
+
catch { /* Preserve plain-text errors from proxies. */ }
|
|
112
|
+
throw classifyStatus(res.status, opts.provider, detail);
|
|
84
113
|
}
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
break;
|
|
114
|
+
if (!res.body) {
|
|
115
|
+
throw new ProviderError(`${opts.provider}: empty response body`, "unknown");
|
|
88
116
|
}
|
|
89
|
-
await
|
|
117
|
+
return await parseOpenAIStream(res.body, opts.provider, events);
|
|
90
118
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
detail = j.error?.message ?? JSON.stringify(j).slice(0, 300);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
detail = (await res.text().catch(() => "")).slice(0, 300);
|
|
99
|
-
}
|
|
100
|
-
throw classifyStatus(res.status, opts.provider, detail);
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (controller.signal.aborted)
|
|
121
|
+
throw controller.signal.reason;
|
|
122
|
+
throw error;
|
|
101
123
|
}
|
|
102
|
-
|
|
103
|
-
|
|
124
|
+
finally {
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
signal?.removeEventListener("abort", abort);
|
|
104
127
|
}
|
|
105
|
-
return parseOpenAIStream(res.body, opts.provider, events);
|
|
106
128
|
},
|
|
107
129
|
};
|
|
108
130
|
}
|
|
@@ -111,17 +133,24 @@ async function parseOpenAIStream(body, provider, events) {
|
|
|
111
133
|
const decoder = new TextDecoder();
|
|
112
134
|
let buffer = "";
|
|
113
135
|
let content = "";
|
|
136
|
+
let completed = false;
|
|
137
|
+
let doneMarker = false;
|
|
114
138
|
const pending = new Map();
|
|
115
139
|
const finalized = new Map();
|
|
116
140
|
const handleData = (data) => {
|
|
117
|
-
if (!data
|
|
141
|
+
if (!data)
|
|
118
142
|
return;
|
|
143
|
+
if (data === "[DONE]") {
|
|
144
|
+
doneMarker = true;
|
|
145
|
+
completed = true;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
119
148
|
let json;
|
|
120
149
|
try {
|
|
121
150
|
json = JSON.parse(data);
|
|
122
151
|
}
|
|
123
152
|
catch {
|
|
124
|
-
|
|
153
|
+
throw new ProviderError(provider + ": malformed stream event", "network");
|
|
125
154
|
}
|
|
126
155
|
if (json.error) {
|
|
127
156
|
const err = json.error;
|
|
@@ -151,6 +180,9 @@ async function parseOpenAIStream(body, provider, events) {
|
|
|
151
180
|
}
|
|
152
181
|
}
|
|
153
182
|
if (choice.finish_reason) {
|
|
183
|
+
completed = true;
|
|
184
|
+
if (choice.finish_reason === "length")
|
|
185
|
+
throw new ProviderError(provider + ": response exceeded the token limit; narrow the task.", "model");
|
|
154
186
|
for (const [idx, tc] of pending)
|
|
155
187
|
finalized.set(idx, tc);
|
|
156
188
|
pending.clear();
|
|
@@ -165,12 +197,17 @@ async function parseOpenAIStream(body, provider, events) {
|
|
|
165
197
|
const lines = buffer.split("\n");
|
|
166
198
|
buffer = lines.pop() ?? "";
|
|
167
199
|
for (const line of lines) {
|
|
200
|
+
if (doneMarker)
|
|
201
|
+
break;
|
|
168
202
|
const trimmed = line.trim();
|
|
169
203
|
if (trimmed.startsWith("data:"))
|
|
170
204
|
handleData(trimmed.slice(5).trim());
|
|
171
205
|
}
|
|
206
|
+
if (doneMarker)
|
|
207
|
+
break;
|
|
172
208
|
}
|
|
173
|
-
|
|
209
|
+
buffer += decoder.decode();
|
|
210
|
+
if (!doneMarker && buffer.trim()) {
|
|
174
211
|
const trimmed = buffer.trim();
|
|
175
212
|
if (trimmed.startsWith("data:"))
|
|
176
213
|
handleData(trimmed.slice(5).trim());
|
|
@@ -181,12 +218,20 @@ async function parseOpenAIStream(body, provider, events) {
|
|
|
181
218
|
throw e;
|
|
182
219
|
throw new ProviderError(`${provider}: stream interrupted: ${errorMessage(e)}`, "network");
|
|
183
220
|
}
|
|
221
|
+
finally {
|
|
222
|
+
await reader.cancel().catch(() => { });
|
|
223
|
+
reader.releaseLock();
|
|
224
|
+
}
|
|
225
|
+
if (!completed)
|
|
226
|
+
throw new ProviderError(provider + ": stream ended before completion; please retry.", "network");
|
|
227
|
+
for (const [index, call] of pending)
|
|
228
|
+
finalized.set(index, call);
|
|
184
229
|
const toolCalls = [...finalized.values()]
|
|
185
230
|
.filter((c) => c.name)
|
|
186
231
|
.map((c) => ({
|
|
187
232
|
id: c.id || `call_${Math.random().toString(36).slice(2, 10)}`,
|
|
188
233
|
name: c.name,
|
|
189
|
-
args:
|
|
234
|
+
args: parseToolArgs(c.args, provider),
|
|
190
235
|
}));
|
|
191
236
|
return {
|
|
192
237
|
role: "assistant",
|
|
@@ -194,5 +239,14 @@ async function parseOpenAIStream(body, provider, events) {
|
|
|
194
239
|
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
195
240
|
};
|
|
196
241
|
}
|
|
242
|
+
function parseToolArgs(raw, provider) {
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(raw || "{}");
|
|
245
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
246
|
+
return parsed;
|
|
247
|
+
}
|
|
248
|
+
catch { /* Reject partial tool arguments instead of executing an empty object. */ }
|
|
249
|
+
throw new ProviderError(provider + ": invalid tool arguments; no action was executed.", "model");
|
|
250
|
+
}
|
|
197
251
|
/** Exported for tests. */
|
|
198
252
|
export { toOpenAIMessages, toOpenAITools, safeParseArgs };
|