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/banner.js
CHANGED
|
@@ -1,65 +1,133 @@
|
|
|
1
1
|
import { hex, bold, dim, stripAnsi } from "./ansi.js";
|
|
2
2
|
import { modelLabel, loadConfig, activeModelId } from "./config.js";
|
|
3
3
|
import { findModel } from "./models.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
4
|
+
const WORDMARK = "CODESHARK";
|
|
5
|
+
const WORDMARK_GLYPHS = {
|
|
6
|
+
"C": [
|
|
7
|
+
"01111",
|
|
8
|
+
"10000",
|
|
9
|
+
"10000",
|
|
10
|
+
"10000",
|
|
11
|
+
"10000",
|
|
12
|
+
"10000",
|
|
13
|
+
"01111"
|
|
14
|
+
],
|
|
15
|
+
"O": [
|
|
16
|
+
"01110",
|
|
17
|
+
"10001",
|
|
18
|
+
"10001",
|
|
19
|
+
"10001",
|
|
20
|
+
"10001",
|
|
21
|
+
"10001",
|
|
22
|
+
"01110"
|
|
23
|
+
],
|
|
24
|
+
"D": [
|
|
25
|
+
"11110",
|
|
26
|
+
"10001",
|
|
27
|
+
"10001",
|
|
28
|
+
"10001",
|
|
29
|
+
"10001",
|
|
30
|
+
"10001",
|
|
31
|
+
"11110"
|
|
32
|
+
],
|
|
33
|
+
"E": [
|
|
34
|
+
"11111",
|
|
35
|
+
"10000",
|
|
36
|
+
"10000",
|
|
37
|
+
"11110",
|
|
38
|
+
"10000",
|
|
39
|
+
"10000",
|
|
40
|
+
"11111"
|
|
41
|
+
],
|
|
42
|
+
"S": [
|
|
43
|
+
"01111",
|
|
44
|
+
"10000",
|
|
45
|
+
"10000",
|
|
46
|
+
"01110",
|
|
47
|
+
"00001",
|
|
48
|
+
"00001",
|
|
49
|
+
"11110"
|
|
50
|
+
],
|
|
51
|
+
"H": [
|
|
52
|
+
"10001",
|
|
53
|
+
"10001",
|
|
54
|
+
"10001",
|
|
55
|
+
"11111",
|
|
56
|
+
"10001",
|
|
57
|
+
"10001",
|
|
58
|
+
"10001"
|
|
59
|
+
],
|
|
60
|
+
"A": [
|
|
61
|
+
"01110",
|
|
62
|
+
"10001",
|
|
63
|
+
"10001",
|
|
64
|
+
"11111",
|
|
65
|
+
"10001",
|
|
66
|
+
"10001",
|
|
67
|
+
"10001"
|
|
68
|
+
],
|
|
69
|
+
"R": [
|
|
70
|
+
"11110",
|
|
71
|
+
"10001",
|
|
72
|
+
"10001",
|
|
73
|
+
"11110",
|
|
74
|
+
"10100",
|
|
75
|
+
"10010",
|
|
76
|
+
"10001"
|
|
77
|
+
],
|
|
78
|
+
"K": [
|
|
79
|
+
"10001",
|
|
80
|
+
"10010",
|
|
81
|
+
"10100",
|
|
82
|
+
"11000",
|
|
83
|
+
"10100",
|
|
84
|
+
"10010",
|
|
85
|
+
"10001"
|
|
86
|
+
]
|
|
49
87
|
};
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
function interpolate(start, end, amount) {
|
|
89
|
+
return Math.round(start + (end - start) * amount);
|
|
90
|
+
}
|
|
91
|
+
function gradientColor(index, length) {
|
|
92
|
+
const stops = [
|
|
93
|
+
["#0b2a5b", 11, 42, 91],
|
|
94
|
+
["#1677c8", 22, 119, 200],
|
|
95
|
+
["#9bdcff", 155, 220, 255],
|
|
96
|
+
];
|
|
97
|
+
const amount = length <= 1 ? 0 : index / (length - 1);
|
|
98
|
+
const start = amount <= 0.5 ? stops[0] : stops[1];
|
|
99
|
+
const end = amount <= 0.5 ? stops[1] : stops[2];
|
|
100
|
+
const segmentAmount = amount <= 0.5 ? amount * 2 : (amount - 0.5) * 2;
|
|
101
|
+
const [, startR, startG, startB] = start;
|
|
102
|
+
const [, endR, endG, endB] = end;
|
|
103
|
+
return `#${[interpolate(startR, endR, segmentAmount), interpolate(startG, endG, segmentAmount), interpolate(startB, endB, segmentAmount)]
|
|
104
|
+
.map((value) => value.toString(16).padStart(2, "0"))
|
|
105
|
+
.join("")}`;
|
|
106
|
+
}
|
|
107
|
+
function renderWordmark(plain, columns) {
|
|
108
|
+
// Leave a spare column to avoid terminal auto-wrap.
|
|
109
|
+
if (columns < 56)
|
|
110
|
+
return plain ? WORDMARK : bold(hex("#9bdcff", WORDMARK));
|
|
111
|
+
const pixelWidth = columns >= 101 ? 2 : 1;
|
|
112
|
+
const letters = [...WORDMARK.toUpperCase()];
|
|
113
|
+
const sharkStart = 4;
|
|
114
|
+
const sharkLength = letters.length - sharkStart;
|
|
115
|
+
return Array.from({ length: 7 }, (_, row) => {
|
|
116
|
+
const parts = [];
|
|
117
|
+
letters.forEach((letter, letterIndex) => {
|
|
118
|
+
const glyph = WORDMARK_GLYPHS[letter];
|
|
119
|
+
const pixels = [...glyph[row]].map((pixel) => {
|
|
120
|
+
if (pixel === "0")
|
|
121
|
+
return " ".repeat(pixelWidth);
|
|
122
|
+
const block = "\u2588".repeat(pixelWidth);
|
|
123
|
+
if (plain)
|
|
124
|
+
return block;
|
|
125
|
+
return letterIndex < sharkStart ? bold(hex("#f8fafc", block)) : hex(gradientColor(letterIndex - sharkStart, sharkLength), block);
|
|
126
|
+
});
|
|
127
|
+
parts.push(pixels.join(""));
|
|
128
|
+
});
|
|
129
|
+
return parts.join(" ");
|
|
130
|
+
}).join("\n");
|
|
63
131
|
}
|
|
64
132
|
export function renderBanner(opts = {}) {
|
|
65
133
|
const plain = opts.plain ?? false;
|
|
@@ -68,20 +136,10 @@ export function renderBanner(opts = {}) {
|
|
|
68
136
|
const baseLabel = modelLabel(cfg);
|
|
69
137
|
const modelLine = opts.modelLine ?? (entry ? `${baseLabel} · ${entry.context} context` : baseLabel);
|
|
70
138
|
const cwd = opts.cwd ?? process.cwd();
|
|
71
|
-
const title = plain
|
|
139
|
+
const title = renderWordmark(plain, opts.columns ?? process.stdout.columns ?? 80);
|
|
72
140
|
const sub = plain ? modelLine : hex("#8ba3ba", modelLine);
|
|
73
141
|
const dir = plain ? cwd : dim(cwd);
|
|
74
|
-
const lines = [];
|
|
75
|
-
const textLines = [title, sub, dir];
|
|
76
|
-
for (let i = 0; i < SPRITE.length; i++) {
|
|
77
|
-
let line = " ";
|
|
78
|
-
for (const ch of SPRITE[i])
|
|
79
|
-
line += renderSpriteCell(ch, plain);
|
|
80
|
-
const text = textLines[i];
|
|
81
|
-
if (text !== undefined)
|
|
82
|
-
line += " " + text;
|
|
83
|
-
lines.push(line);
|
|
84
|
-
}
|
|
142
|
+
const lines = [...title.split("\n").map((line) => " " + line), "", " " + sub, " " + dir];
|
|
85
143
|
if (opts.footer)
|
|
86
144
|
lines.push("", opts.footer);
|
|
87
145
|
return lines.join("\n") + "\n";
|
|
@@ -90,5 +148,6 @@ export function printBanner(opts = {}) {
|
|
|
90
148
|
process.stdout.write(renderBanner(opts));
|
|
91
149
|
}
|
|
92
150
|
export function bannerHasTitle(text) {
|
|
93
|
-
|
|
151
|
+
const clean = stripAnsi(text);
|
|
152
|
+
return clean.includes(WORDMARK) || [56, 101].some((columns) => clean.includes(renderWordmark(true, columns).split("\n").map((line) => " " + line).join("\n")));
|
|
94
153
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, renameSync, unlinkSync } from "node:fs";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { DEFAULT_MODEL_ID, findModel, toApiSlug } from "./models.js";
|
|
5
|
+
import { DEFAULT_MODEL_ID, findModel, isRetiredModel, toApiSlug } from "./models.js";
|
|
5
6
|
export const DEFAULT_GATEWAY_URL = "https://codeshark-gateway.ajrgp.workers.dev";
|
|
6
7
|
/** Kept for backwards compatibility; the catalog in src/models.ts is canonical. */
|
|
7
|
-
export const DEFAULT_MODEL = "glm-5.3-flash-
|
|
8
|
+
export const DEFAULT_MODEL = "glm-5.3-flash-think-search:free";
|
|
8
9
|
/** Fallback slug for OpenRouter when the active model lives on another provider. */
|
|
9
10
|
export const DEFAULT_OPENROUTER_MODEL = "z-ai/glm-5.2:free";
|
|
10
11
|
export const DEFAULT_GEMINI_MODEL = "gemini-2.5-pro";
|
|
@@ -20,7 +21,22 @@ export function loadConfig() {
|
|
|
20
21
|
if (!existsSync(p))
|
|
21
22
|
return {};
|
|
22
23
|
const raw = readFileSync(p, "utf8");
|
|
23
|
-
|
|
24
|
+
const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
|
|
25
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
26
|
+
return {};
|
|
27
|
+
const cfg = parsed;
|
|
28
|
+
if (cfg.maxIterations !== undefined && (!Number.isSafeInteger(cfg.maxIterations) || cfg.maxIterations < 1))
|
|
29
|
+
delete cfg.maxIterations;
|
|
30
|
+
const strings = ["model", "systemPrompt", "openrouterApiKey", "openrouterBaseUrl", "unorouterApiKey", "unorouterBaseUrl", "nvidiaApiKey", "nvidiaBaseUrl", "geminiApiKey", "geminiBaseUrl", "gatewayUrl", "gatewayKey", "ollamaBaseUrl", "ollamaModel"];
|
|
31
|
+
for (const key of strings)
|
|
32
|
+
if (cfg[key] !== undefined && typeof cfg[key] !== "string")
|
|
33
|
+
delete cfg[key];
|
|
34
|
+
for (const key of ["termsAccepted", "setupCompleted"])
|
|
35
|
+
if (typeof cfg[key] !== "boolean")
|
|
36
|
+
delete cfg[key];
|
|
37
|
+
if (cfg.provider && !["gateway", "openrouter", "nvidia", "gemini", "ollama", "unorouter"].includes(cfg.provider))
|
|
38
|
+
delete cfg.provider;
|
|
39
|
+
return cfg;
|
|
24
40
|
}
|
|
25
41
|
catch {
|
|
26
42
|
return {};
|
|
@@ -30,7 +46,17 @@ export function loadConfig() {
|
|
|
30
46
|
export function saveConfig(cfg) {
|
|
31
47
|
const p = configPath();
|
|
32
48
|
mkdirSync(dirname(p), { recursive: true });
|
|
33
|
-
|
|
49
|
+
const temporary = p + "." + randomUUID() + ".tmp";
|
|
50
|
+
try {
|
|
51
|
+
writeFileSync(temporary, JSON.stringify(cfg, null, 2) + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
52
|
+
renameSync(temporary, p);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
try {
|
|
56
|
+
unlinkSync(temporary);
|
|
57
|
+
}
|
|
58
|
+
catch { /* Already renamed, or never created. */ }
|
|
59
|
+
}
|
|
34
60
|
try {
|
|
35
61
|
chmodSync(p, 0o600);
|
|
36
62
|
}
|
|
@@ -43,9 +69,9 @@ export function saveConfig(cfg) {
|
|
|
43
69
|
* Falls back sensibly when config/env only name a provider.
|
|
44
70
|
*/
|
|
45
71
|
export function activeModelId(cfg) {
|
|
46
|
-
if (process.env.CODESHARK_MODEL)
|
|
72
|
+
if (process.env.CODESHARK_MODEL && !isRetiredModel(process.env.CODESHARK_MODEL))
|
|
47
73
|
return process.env.CODESHARK_MODEL;
|
|
48
|
-
if (cfg.model)
|
|
74
|
+
if (cfg.model && !isRetiredModel(cfg.model))
|
|
49
75
|
return cfg.model;
|
|
50
76
|
return DEFAULT_MODEL_ID;
|
|
51
77
|
}
|
|
@@ -74,7 +100,8 @@ export function activeProvider(cfg) {
|
|
|
74
100
|
/** Effective model for a provider, honoring env overrides and config. */
|
|
75
101
|
export function effectiveModel(cfg, provider) {
|
|
76
102
|
if (provider === "gemini") {
|
|
77
|
-
const
|
|
103
|
+
const selected = findModel(activeModelId(cfg));
|
|
104
|
+
const configured = selected?.provider === "gemini" ? selected.model : cfg.model && !selected ? cfg.model : undefined;
|
|
78
105
|
return process.env.GEMINI_MODEL ?? configured ?? DEFAULT_GEMINI_MODEL;
|
|
79
106
|
}
|
|
80
107
|
if (provider === "ollama")
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,8 @@ import { runAgent } from "./agent.js";
|
|
|
15
15
|
import { errorMessage } from "./provider/types.js";
|
|
16
16
|
import { launchKeysPage } from "./keysPage.js";
|
|
17
17
|
import { extractFolderArg, openProjectFolder } from "./project.js";
|
|
18
|
+
import { installLatestVersion, isNewerVersion, latestPublishedVersion } from "./update.js";
|
|
19
|
+
import { checkModelAvailability, modelAvailabilitySnapshot } from "./modelAvailability.js";
|
|
18
20
|
const require = createRequire(import.meta.url);
|
|
19
21
|
const VERSION = require("../package.json").version;
|
|
20
22
|
function printHelp() {
|
|
@@ -27,10 +29,11 @@ function printHelp() {
|
|
|
27
29
|
" codeshark <prompt…> one-shot prompt for the current project folder",
|
|
28
30
|
" codeshark --folder <path> open a specific project folder first",
|
|
29
31
|
" codeshark --folder <path> \"prompt…\" run a prompt in that folder",
|
|
30
|
-
" codeshark banner [--plain] print just the
|
|
32
|
+
" codeshark banner [--plain] print just the CodeShark wordmark",
|
|
31
33
|
" codeshark setup guided setup for providers / API keys",
|
|
32
34
|
" codeshark keys open the password-protected local key page",
|
|
33
35
|
" codeshark model list and switch the model catalog",
|
|
36
|
+
" codeshark update check for and install the latest published version",
|
|
34
37
|
" codeshark terms read the Terms of Service",
|
|
35
38
|
" codeshark --version print the version",
|
|
36
39
|
"",
|
|
@@ -39,11 +42,38 @@ function printHelp() {
|
|
|
39
42
|
"",
|
|
40
43
|
].join("\n"));
|
|
41
44
|
}
|
|
45
|
+
async function updatePackage() {
|
|
46
|
+
console.log(dim("Checking npm for a newer CodeShark version…"));
|
|
47
|
+
const latest = await latestPublishedVersion();
|
|
48
|
+
if (!latest) {
|
|
49
|
+
console.log(dim("Could not check npm right now. You can retry with `codeshark update`."));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!isNewerVersion(VERSION, latest)) {
|
|
53
|
+
console.log(dim(`CodeShark ${VERSION} is up to date.`));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const rl = createInterface({ input, output });
|
|
57
|
+
const answer = await rl.question(`CodeShark ${latest} is available (you have ${VERSION}). Install it now? [y/N]: `);
|
|
58
|
+
rl.close();
|
|
59
|
+
if (answer.trim().toLowerCase() !== "y" && answer.trim().toLowerCase() !== "yes") {
|
|
60
|
+
console.log(dim("Update skipped."));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await installLatestVersion();
|
|
65
|
+
console.log(hex("#4ade80", `Updated to CodeShark ${latest}. Restart CodeShark to use it.`));
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
console.log(hex("#f87171", `Update failed: ${errorMessage(e)}`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
42
71
|
function isTTY() {
|
|
43
72
|
return Boolean(process.stdout.isTTY) && !process.env.CODESHARK_NO_BANNER;
|
|
44
73
|
}
|
|
45
74
|
async function runOneShot(prompt) {
|
|
46
75
|
const cfg = loadConfig();
|
|
76
|
+
await checkModelAvailability(cfg);
|
|
47
77
|
const cwd = process.cwd();
|
|
48
78
|
const registry = createRegistry();
|
|
49
79
|
const clients = resolveClients(cfg, (m) => console.error(dim(m)));
|
|
@@ -59,8 +89,19 @@ async function runOneShot(prompt) {
|
|
|
59
89
|
process.stdout.write(d);
|
|
60
90
|
},
|
|
61
91
|
};
|
|
92
|
+
const approvalRl = process.stdin.isTTY ? createInterface({ input, output }) : undefined;
|
|
62
93
|
try {
|
|
63
|
-
const result = await runAgent(prompt, {
|
|
94
|
+
const result = await runAgent(prompt, {
|
|
95
|
+
clients,
|
|
96
|
+
registry,
|
|
97
|
+
cwd,
|
|
98
|
+
approveToolCall: approvalRl
|
|
99
|
+
? async (call) => {
|
|
100
|
+
const answer = await approvalRl.question(`\nApprove ${call.name}? [y/N]: `);
|
|
101
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
102
|
+
}
|
|
103
|
+
: async () => false,
|
|
104
|
+
}, events);
|
|
64
105
|
if (thinking) {
|
|
65
106
|
thinking = false;
|
|
66
107
|
stopThinking();
|
|
@@ -79,6 +120,9 @@ async function runOneShot(prompt) {
|
|
|
79
120
|
console.error(hex("#f87171", `✗ ${errorMessage(e)}`));
|
|
80
121
|
process.exitCode = 1;
|
|
81
122
|
}
|
|
123
|
+
finally {
|
|
124
|
+
approvalRl?.close();
|
|
125
|
+
}
|
|
82
126
|
}
|
|
83
127
|
async function main() {
|
|
84
128
|
const parsed = extractFolderArg(process.argv.slice(2));
|
|
@@ -101,12 +145,13 @@ async function main() {
|
|
|
101
145
|
console.log(readTerms());
|
|
102
146
|
return;
|
|
103
147
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
throw new Error("CodeShark needs a project folder. Open your project in a terminal, then run `codeshark --folder .` (or pass its full path).");
|
|
148
|
+
if (command === "update") {
|
|
149
|
+
await updatePackage();
|
|
150
|
+
return;
|
|
108
151
|
}
|
|
109
|
-
|
|
152
|
+
// Plain `codeshark` uses the current directory; all agent work remains
|
|
153
|
+
// scoped to that directory, which must be a real folder.
|
|
154
|
+
const projectFolder = openProjectFolder(parsed.folder ?? process.cwd());
|
|
110
155
|
switch (command) {
|
|
111
156
|
case "setup":
|
|
112
157
|
await runSetup();
|
|
@@ -120,6 +165,7 @@ async function main() {
|
|
|
120
165
|
}
|
|
121
166
|
case "model": {
|
|
122
167
|
// `codeshark model` lists the catalog; `codeshark model <name>` switches.
|
|
168
|
+
await checkModelAvailability(loadConfig());
|
|
123
169
|
if (rest.length) {
|
|
124
170
|
switchModel(rest.join(" "));
|
|
125
171
|
return;
|
|
@@ -163,12 +209,33 @@ async function main() {
|
|
|
163
209
|
process.env.OPENROUTER_API_KEY ||
|
|
164
210
|
process.env.NVIDIA_API_KEY ||
|
|
165
211
|
process.env.GEMINI_API_KEY);
|
|
166
|
-
|
|
212
|
+
const setupCompleted = cfg.setupCompleted ?? hasProviderSetup;
|
|
213
|
+
if (!setupCompleted) {
|
|
167
214
|
const rl = createInterface({ input, output });
|
|
168
|
-
await runSetupFlow(cfg, rl, { title: "
|
|
215
|
+
await runSetupFlow(cfg, rl, { title: "Welcome to CodeShark" });
|
|
169
216
|
rl.close();
|
|
217
|
+
cfg.setupCompleted = true;
|
|
218
|
+
saveConfig(cfg);
|
|
170
219
|
console.log("");
|
|
171
220
|
}
|
|
221
|
+
if (process.env.CODESHARK_NO_UPDATE === undefined) {
|
|
222
|
+
const latest = await latestPublishedVersion();
|
|
223
|
+
if (latest && isNewerVersion(VERSION, latest)) {
|
|
224
|
+
const rl = createInterface({ input, output });
|
|
225
|
+
const answer = await rl.question(` CodeShark ${latest} is available. Install it now? [y/N]: `);
|
|
226
|
+
rl.close();
|
|
227
|
+
if (answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes") {
|
|
228
|
+
try {
|
|
229
|
+
await installLatestVersion();
|
|
230
|
+
console.log(hex("#4ade80", ` Updated to CodeShark ${latest}. Restart to use the new version.`));
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
console.log(hex("#f87171", ` Update failed: ${errorMessage(e)}`));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
await checkModelAvailability(loadConfig());
|
|
172
239
|
// The REPL prints the input instructions once after the banner. Keeping
|
|
173
240
|
// them out of the banner avoids the duplicated startup line. The loading
|
|
174
241
|
// screen types itself out with a spinner, then hands off to the REPL.
|
|
@@ -178,6 +245,15 @@ async function main() {
|
|
|
178
245
|
"Loading model catalog",
|
|
179
246
|
"Starting the agent",
|
|
180
247
|
]);
|
|
248
|
+
console.log(dim(" Model health"));
|
|
249
|
+
for (const { model, status, reason } of modelAvailabilitySnapshot()) {
|
|
250
|
+
if (status === "available") {
|
|
251
|
+
console.log(` ${hex("#4ade80", "✓ Available")} ${model.label}`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
console.log(` ${hex("#f87171", "✗ Unavailable")} ${model.label}${reason ? dim(` — ${reason}`) : ""}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
181
257
|
console.log("");
|
|
182
258
|
}
|
|
183
259
|
const registry = createRegistry();
|
package/dist/keysPage.js
CHANGED
|
@@ -107,14 +107,14 @@ body{max-width:760px;margin:0 auto;padding:42px 22px}h1{font-size:25px;margin:0
|
|
|
107
107
|
function loginPage(message = "") {
|
|
108
108
|
const auth = readAuth();
|
|
109
109
|
if (!auth) {
|
|
110
|
-
return page("Create local password", `<h1
|
|
110
|
+
return page("Create local password", `<h1>CodeShark key vault</h1><p>Set a local password before viewing API keys. This password is stored as a one-way hash and never leaves this computer.</p>${message ? `<p class="warning">${htmlEscape(message)}</p>` : ""}<div class="card"><form method="post" action="/setup"><label for="password">Create password</label><input id="password" name="password" type="password" minlength="8" required autofocus><label for="confirm">Confirm password</label><input id="confirm" name="confirm" type="password" minlength="8" required><button>Protect my keys</button></form></div><p class="small">The page only listens on 127.0.0.1. Do not expose this port publicly.</p>`);
|
|
111
111
|
}
|
|
112
|
-
return page("Unlock key vault", `<h1
|
|
112
|
+
return page("Unlock key vault", `<h1>CodeShark key vault</h1><p>Enter your local password to view or edit saved provider keys.</p>${message ? `<p class="warning">${htmlEscape(message)}</p>` : ""}<div class="card"><form method="post" action="/login"><input type="hidden" name="csrf" value="${htmlEscape(randomBytes(16).toString("hex"))}"><label for="password">Password</label><input id="password" name="password" type="password" required autofocus><button>Unlock</button></form></div><p class="small">Forgot the password? Delete ${htmlEscape(authFilePath ?? authPath())} to reset local protection.</p>`);
|
|
113
113
|
}
|
|
114
114
|
function vaultPage(session) {
|
|
115
115
|
const cfg = loadConfig();
|
|
116
116
|
const secretInput = (id, label, placeholder) => `<label for="${id}">${label}</label><input id="${id}" name="${id}" type="password" value="" placeholder="${placeholder}" autocomplete="new-password" spellcheck="false"><label class="small"><input type="checkbox" name="clear_${id}" value="1" style="width:auto;margin-right:8px"> Clear this saved key</label>`;
|
|
117
|
-
return page("API keys", `<h1
|
|
117
|
+
return page("API keys", `<h1>CodeShark key vault</h1><p>Unlocked locally. Values are saved to <code>${htmlEscape(configPath())}</code>. Keep this page on your own computer.</p><div class="card"><form method="post" action="/save"><input type="hidden" name="csrf" value="${htmlEscape(session.csrf)}"><h2>Provider keys</h2>${secretInput("unorouterApiKey", "Primary model API key", "paste a replacement (shown once)…")}${secretInput("openrouterApiKey", "OpenRouter API key", "paste a replacement: sk-or-v1-…")}${secretInput("nvidiaApiKey", "NVIDIA NIM API key", "paste a replacement: nvapi-…")}${secretInput("geminiApiKey", "Gemini API key", "paste a replacement: AIza…")}<p class="small">Saved keys are never placed in this page's HTML. Leave a field blank to keep its current value, or check Clear to remove it.</p><button>Save keys</button></form></div><div class="card"><h2>Current status</h2><p class="key">Primary model API: ${htmlEscape(mask(cfg.unorouterApiKey) || "not set")}</p><p class="key">OpenRouter: ${htmlEscape(mask(cfg.openrouterApiKey) || "not set")}</p><p class="key">NVIDIA: ${htmlEscape(mask(cfg.nvidiaApiKey) || "not set")}</p><p class="key">Gemini: ${htmlEscape(mask(cfg.geminiApiKey) || "not set")}</p><p class="hint">Your keys are never printed to the terminal by CodeShark.</p><form method="post" action="/logout"><input type="hidden" name="csrf" value="${htmlEscape(session.csrf)}"><button class="danger">Lock vault</button></form></div>`);
|
|
118
118
|
}
|
|
119
119
|
function parseBody(req) {
|
|
120
120
|
return new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { DEFAULT_GATEWAY_URL, envApiKey } from "./config.js";
|
|
2
|
+
import { MODELS } from "./models.js";
|
|
3
|
+
const statuses = new Map();
|
|
4
|
+
const reasons = new Map();
|
|
5
|
+
const TIMEOUT_MS = 8_000;
|
|
6
|
+
export function modelAvailability(modelId) {
|
|
7
|
+
return statuses.get(modelId) ?? "checking";
|
|
8
|
+
}
|
|
9
|
+
export function modelAvailabilityReason(modelId) {
|
|
10
|
+
return reasons.get(modelId);
|
|
11
|
+
}
|
|
12
|
+
export function modelAvailabilitySnapshot() {
|
|
13
|
+
return MODELS.map((model) => ({
|
|
14
|
+
model,
|
|
15
|
+
status: modelAvailability(model.id),
|
|
16
|
+
reason: modelAvailabilityReason(model.id),
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
function endpointFor(model, cfg) {
|
|
20
|
+
if (model.provider === "gemini") {
|
|
21
|
+
return {
|
|
22
|
+
url: `${(cfg.geminiBaseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(/\/+$/, "")}/models/${model.model}:generateContent`,
|
|
23
|
+
key: cfg.geminiApiKey ?? envApiKey("gemini"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const key = cfg.unorouterApiKey ?? envApiKey("unorouter");
|
|
27
|
+
if (key) {
|
|
28
|
+
return { url: `${(cfg.unorouterBaseUrl ?? "https://api.unorouter.com/v1").replace(/\/+$/, "")}/chat/completions`, key };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
url: `${(cfg.gatewayUrl ?? process.env.CODESHARK_GATEWAY_URL ?? DEFAULT_GATEWAY_URL).replace(/\/+$/, "")}/v1/chat/completions`,
|
|
32
|
+
key: cfg.gatewayKey ?? process.env.CODESHARK_GATEWAY_KEY,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function checkOne(model, cfg) {
|
|
36
|
+
const target = endpointFor(model, cfg);
|
|
37
|
+
if (!target.key && model.provider === "gemini") {
|
|
38
|
+
statuses.set(model.id, "unavailable");
|
|
39
|
+
reasons.set(model.id, "GEMINI_API_KEY is not configured");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
44
|
+
try {
|
|
45
|
+
const headers = { "content-type": "application/json" };
|
|
46
|
+
if (target.key)
|
|
47
|
+
headers.authorization = `Bearer ${target.key}`;
|
|
48
|
+
if (model.provider === "gemini") {
|
|
49
|
+
const url = `${target.url}?key=${encodeURIComponent(target.key ?? "")}`;
|
|
50
|
+
const response = await fetch(url, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "content-type": "application/json" },
|
|
53
|
+
body: JSON.stringify({ contents: [{ role: "user", parts: [{ text: "Reply with OK." }] }] }),
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok)
|
|
57
|
+
throw new Error(`HTTP ${response.status}`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const response = await fetch(target.url, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers,
|
|
63
|
+
body: JSON.stringify({ model: model.model, messages: [{ role: "user", content: "Reply with OK." }], stream: false, max_tokens: 4 }),
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok)
|
|
67
|
+
throw new Error(`HTTP ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
statuses.set(model.id, "available");
|
|
70
|
+
reasons.delete(model.id);
|
|
71
|
+
}
|
|
72
|
+
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));
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function checkModelAvailability(cfg) {
|
|
81
|
+
for (const model of MODELS) {
|
|
82
|
+
statuses.set(model.id, "checking");
|
|
83
|
+
reasons.delete(model.id);
|
|
84
|
+
}
|
|
85
|
+
await Promise.all(MODELS.map((model) => checkOne(model, cfg)));
|
|
86
|
+
}
|