codeshark-cli 0.1.1

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 ADDED
@@ -0,0 +1,126 @@
1
+ import { homedir } from "node:os";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { DEFAULT_MODEL_ID, findModel, toApiSlug } from "./models.js";
5
+ export const DEFAULT_GATEWAY_URL = "https://codeshark-gateway.ajrgp.workers.dev";
6
+ /** Kept for backwards compatibility; the catalog in src/models.ts is canonical. */
7
+ export const DEFAULT_MODEL = "glm-5.3-flash-thinking:free";
8
+ /** Fallback slug for OpenRouter when the active model lives on another provider. */
9
+ export const DEFAULT_OPENROUTER_MODEL = "z-ai/glm-5.2:free";
10
+ export const DEFAULT_GEMINI_MODEL = "gemini-2.5-pro";
11
+ export const GEMINI_FLASH_MODEL = "gemini-2.5-flash";
12
+ export const DEFAULT_OLLAMA_MODEL = "qwen3-coder:30b";
13
+ export const DEFAULT_MAX_ITERATIONS = 25;
14
+ export function configPath() {
15
+ return process.env.CODESHARK_CONFIG ?? join(homedir(), ".codeshark.json");
16
+ }
17
+ export function loadConfig() {
18
+ const p = configPath();
19
+ try {
20
+ if (!existsSync(p))
21
+ return {};
22
+ const raw = readFileSync(p, "utf8");
23
+ return JSON.parse(raw);
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }
29
+ /** Persist config, creating the directory and locking file permissions (POSIX only). */
30
+ export function saveConfig(cfg) {
31
+ const p = configPath();
32
+ mkdirSync(dirname(p), { recursive: true });
33
+ writeFileSync(p, JSON.stringify(cfg, null, 2) + "\n", "utf8");
34
+ try {
35
+ chmodSync(p, 0o600);
36
+ }
37
+ catch {
38
+ // Windows: chmod is a no-op / may throw — ignore.
39
+ }
40
+ }
41
+ /**
42
+ * The effective model *catalog id* (e.g. "unorouter/glm-5.3-flash-thinking").
43
+ * Falls back sensibly when config/env only name a provider.
44
+ */
45
+ export function activeModelId(cfg) {
46
+ if (process.env.CODESHARK_MODEL)
47
+ return process.env.CODESHARK_MODEL;
48
+ if (cfg.model)
49
+ return cfg.model;
50
+ return DEFAULT_MODEL_ID;
51
+ }
52
+ /** Raw provider-API slug for the active model. */
53
+ export function activeApiModel(cfg) {
54
+ return toApiSlug(activeModelId(cfg));
55
+ }
56
+ /** The provider implied by the active model selection (or explicit config). */
57
+ export function activeProvider(cfg) {
58
+ const m = activeModelId(cfg);
59
+ const entry = findModel(m);
60
+ if (entry)
61
+ return entry.provider;
62
+ const explicit = cfg.provider;
63
+ if (explicit)
64
+ return explicit;
65
+ // Slug heuristics for raw slugs not in the catalog.
66
+ if (m.startsWith("unorouter/"))
67
+ return "unorouter";
68
+ if (m.endsWith(":free") || m.startsWith("openrouter/"))
69
+ return "openrouter";
70
+ if (m.startsWith("moonshotai/") || m.startsWith("deepseek-ai/"))
71
+ return "nvidia";
72
+ return "gateway";
73
+ }
74
+ /** Effective model for a provider, honoring env overrides and config. */
75
+ export function effectiveModel(cfg, provider) {
76
+ if (provider === "gemini") {
77
+ const configured = cfg.model && !findModel(cfg.model) ? cfg.model : undefined;
78
+ return process.env.GEMINI_MODEL ?? configured ?? DEFAULT_GEMINI_MODEL;
79
+ }
80
+ if (provider === "ollama")
81
+ return process.env.CODESHARK_OLLAMA_MODEL ?? cfg.ollamaModel ?? DEFAULT_OLLAMA_MODEL;
82
+ const selected = findModel(activeModelId(cfg));
83
+ if (selected?.provider === provider)
84
+ return selected.model;
85
+ // The community gateway proxies UnoRouter/OpenRouter models, so preserve
86
+ // the selected model instead of silently falling back to GLM.
87
+ if (provider === "gateway" && (selected?.provider === "openrouter" || selected?.provider === "unorouter"))
88
+ return selected.model;
89
+ if (provider === "unorouter")
90
+ return process.env.UNOROUTER_MODEL ?? DEFAULT_MODEL;
91
+ if (provider === "openrouter")
92
+ return DEFAULT_OPENROUTER_MODEL;
93
+ if (provider === "nvidia") {
94
+ const raw = activeModelId(cfg);
95
+ if (raw.startsWith("moonshotai/") || raw.startsWith("deepseek-ai/"))
96
+ return raw;
97
+ return "moonshotai/kimi-k3";
98
+ }
99
+ return DEFAULT_MODEL;
100
+ }
101
+ export function envApiKey(provider) {
102
+ switch (provider) {
103
+ case "openrouter":
104
+ return process.env.OPENROUTER_API_KEY;
105
+ case "unorouter":
106
+ return process.env.UNOROUTER_API_KEY;
107
+ case "nvidia":
108
+ return process.env.NVIDIA_API_KEY ?? process.env.NVIDIA_NIM_API_KEY;
109
+ case "gemini":
110
+ return process.env.GEMINI_API_KEY;
111
+ default:
112
+ return undefined;
113
+ }
114
+ }
115
+ export function hasApiKey(cfg, provider) {
116
+ if (provider === "gateway")
117
+ return true;
118
+ if (provider === "ollama")
119
+ return true;
120
+ return Boolean(cfg[`${provider}ApiKey`]) || Boolean(envApiKey(provider));
121
+ }
122
+ /** Short human label for the banner's model line — just the model, no provider branding. */
123
+ export function modelLabel(cfg) {
124
+ const id = activeModelId(cfg);
125
+ return findModel(id)?.label ?? id;
126
+ }
package/dist/index.js ADDED
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { stdin as input, stdout as output } from "node:process";
5
+ import { bold, dim, hex } from "./ansi.js";
6
+ import { printBanner } from "./banner.js";
7
+ import { showLoading, startThinkingSpinner } from "./loading.js";
8
+ import { loadConfig, saveConfig } from "./config.js";
9
+ import { readTerms } from "./terms.js";
10
+ import { resolveClients } from "./provider/index.js";
11
+ import { createRegistry } from "./tools/index.js";
12
+ import { printModelInfo, startRepl, switchModel } from "./repl.js";
13
+ import { runSetup, runSetupFlow } from "./setup.js";
14
+ import { runAgent } from "./agent.js";
15
+ import { errorMessage } from "./provider/types.js";
16
+ import { launchKeysPage } from "./keysPage.js";
17
+ import { extractFolderArg, openProjectFolder } from "./project.js";
18
+ const require = createRequire(import.meta.url);
19
+ const VERSION = require("../package.json").version;
20
+ function printHelp() {
21
+ console.log([
22
+ "",
23
+ bold("CodeShark") + dim(` v${VERSION} — a terminal coding agent`),
24
+ "",
25
+ " Usage:",
26
+ " codeshark start chat in the current project folder",
27
+ " codeshark <prompt…> one-shot prompt for the current project folder",
28
+ " codeshark --folder <path> open a specific project folder first",
29
+ " codeshark --folder <path> \"prompt…\" run a prompt in that folder",
30
+ " codeshark banner [--plain] print just the mascot banner",
31
+ " codeshark setup guided setup for providers / API keys",
32
+ " codeshark keys open the password-protected local key page",
33
+ " codeshark model list and switch the model catalog",
34
+ " codeshark terms read the Terms of Service",
35
+ " codeshark --version print the version",
36
+ "",
37
+ " Zero setup: works out of the box with no API keys. Add your own key",
38
+ " anytime for your own rate limits and private prompts.",
39
+ "",
40
+ ].join("\n"));
41
+ }
42
+ function isTTY() {
43
+ return Boolean(process.stdout.isTTY) && !process.env.CODESHARK_NO_BANNER;
44
+ }
45
+ async function runOneShot(prompt) {
46
+ const cfg = loadConfig();
47
+ const cwd = process.cwd();
48
+ const registry = createRegistry();
49
+ const clients = resolveClients(cfg, (m) => console.error(dim(m)));
50
+ // Show a "thinking" spinner until the first token arrives, then stream.
51
+ const stopThinking = startThinkingSpinner("Thinking");
52
+ let thinking = true;
53
+ const events = {
54
+ onText: (d) => {
55
+ if (thinking) {
56
+ thinking = false;
57
+ stopThinking();
58
+ }
59
+ process.stdout.write(d);
60
+ },
61
+ };
62
+ try {
63
+ const result = await runAgent(prompt, { clients, registry, cwd }, events);
64
+ if (thinking) {
65
+ thinking = false;
66
+ stopThinking();
67
+ }
68
+ if (result.streamedText) {
69
+ if (!result.streamedText.endsWith("\n"))
70
+ process.stdout.write("\n");
71
+ }
72
+ else {
73
+ process.stdout.write(result.text.endsWith("\n") ? result.text : result.text + "\n");
74
+ }
75
+ }
76
+ catch (e) {
77
+ if (thinking)
78
+ stopThinking();
79
+ console.error(hex("#f87171", `✗ ${errorMessage(e)}`));
80
+ process.exitCode = 1;
81
+ }
82
+ }
83
+ async function main() {
84
+ const parsed = extractFolderArg(process.argv.slice(2));
85
+ const args = parsed.args;
86
+ const [command, ...rest] = args;
87
+ // These informational commands do not touch a project and work anywhere.
88
+ if (command === "--version" || command === "-v") {
89
+ console.log(VERSION);
90
+ return;
91
+ }
92
+ if (command === "--help" || command === "-h" || command === "help") {
93
+ printHelp();
94
+ return;
95
+ }
96
+ if (command === "banner") {
97
+ printBanner({ plain: rest[0] === "--plain" });
98
+ return;
99
+ }
100
+ if (command === "terms") {
101
+ console.log(readTerms());
102
+ return;
103
+ }
104
+ // Every operational command is explicitly project-scoped. Requiring the
105
+ // folder argument makes it impossible to start the agent in the wrong place.
106
+ if (!parsed.folder) {
107
+ throw new Error("CodeShark needs a project folder. Open your project in a terminal, then run `codeshark --folder .` (or pass its full path).");
108
+ }
109
+ const projectFolder = openProjectFolder(parsed.folder);
110
+ switch (command) {
111
+ case "setup":
112
+ await runSetup();
113
+ return;
114
+ case "keys": {
115
+ const url = await launchKeysPage();
116
+ console.log(`CodeShark key vault: ${url}`);
117
+ console.log("Keep this terminal open while using the page. Press Ctrl+C to stop it.");
118
+ await new Promise(() => { });
119
+ return;
120
+ }
121
+ case "model": {
122
+ // `codeshark model` lists the catalog; `codeshark model <name>` switches.
123
+ if (rest.length) {
124
+ switchModel(rest.join(" "));
125
+ return;
126
+ }
127
+ printModelInfo();
128
+ return;
129
+ }
130
+ case "chat":
131
+ break;
132
+ default:
133
+ if (command !== undefined && !command.startsWith("-")) {
134
+ await runOneShot(args.join(" "));
135
+ return;
136
+ }
137
+ break;
138
+ }
139
+ // Interactive mode.
140
+ const cfg = loadConfig();
141
+ const cwd = projectFolder;
142
+ if (isTTY()) {
143
+ // One-time Terms of Service acceptance before anything else runs.
144
+ if (!cfg.termsAccepted) {
145
+ const rl = createInterface({ input, output });
146
+ const answer = (await rl.question(" CodeShark is provided as-is with no warranty. By continuing you agree to the Terms of Service — type `codeshark terms` to read them. Continue? [y/N]: "))
147
+ .trim()
148
+ .toLowerCase();
149
+ rl.close();
150
+ if (answer !== "y" && answer !== "yes") {
151
+ console.log(dim(" Terms not accepted — nothing was changed. Run `codeshark terms` to read them."));
152
+ return;
153
+ }
154
+ cfg.termsAccepted = true;
155
+ saveConfig(cfg);
156
+ console.log("");
157
+ }
158
+ // First run: let the user pick between the shared gateway, their own
159
+ // API key (recommended), or local Ollama — the same flow as `codeshark setup`.
160
+ const hasProviderSetup = Boolean(cfg.provider) ||
161
+ Boolean(cfg.unorouterApiKey || cfg.openrouterApiKey || cfg.nvidiaApiKey || cfg.geminiApiKey) ||
162
+ Boolean(process.env.UNOROUTER_API_KEY ||
163
+ process.env.OPENROUTER_API_KEY ||
164
+ process.env.NVIDIA_API_KEY ||
165
+ process.env.GEMINI_API_KEY);
166
+ if (!hasProviderSetup) {
167
+ const rl = createInterface({ input, output });
168
+ await runSetupFlow(cfg, rl, { title: "🦈 Welcome to CodeShark" });
169
+ rl.close();
170
+ console.log("");
171
+ }
172
+ // The REPL prints the input instructions once after the banner. Keeping
173
+ // them out of the banner avoids the duplicated startup line. The loading
174
+ // screen types itself out with a spinner, then hands off to the REPL.
175
+ printBanner();
176
+ await showLoading([
177
+ "Opening project folder",
178
+ "Loading model catalog",
179
+ "Starting the agent",
180
+ ]);
181
+ console.log("");
182
+ }
183
+ const registry = createRegistry();
184
+ const getClients = () => resolveClients(loadConfig());
185
+ await startRepl({ cwd, registry, getClients });
186
+ }
187
+ main().catch((e) => {
188
+ console.error(hex("#f87171", `✗ ${errorMessage(e)}`));
189
+ process.exit(1);
190
+ });
@@ -0,0 +1,233 @@
1
+ import { createServer } from "node:http";
2
+ import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
3
+ import { existsSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { exec } from "node:child_process";
6
+ import { configPath, loadConfig, saveConfig } from "./config.js";
7
+ const DEFAULT_PORT = 4317;
8
+ const SESSION_TTL_MS = 30 * 60 * 1000;
9
+ const MAX_BODY_BYTES = 32_000;
10
+ let pageServer;
11
+ let pageUrl;
12
+ let authFilePath;
13
+ const sessions = new Map();
14
+ function authPath() {
15
+ return process.env.CODESHARK_AUTH ?? join(dirname(configPath()), ".codeshark-auth.json");
16
+ }
17
+ function readAuth() {
18
+ const p = authPath();
19
+ authFilePath = p;
20
+ try {
21
+ if (!existsSync(p))
22
+ return undefined;
23
+ return JSON.parse(readFileSync(p, "utf8"));
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ function saveAuth(password) {
30
+ const salt = randomBytes(16).toString("hex");
31
+ const passwordHash = scryptSync(password, salt, 32).toString("hex");
32
+ const p = authPath();
33
+ authFilePath = p;
34
+ writeFileSync(p, JSON.stringify({ salt, passwordHash }, null, 2) + "\n", "utf8");
35
+ try {
36
+ chmodSync(p, 0o600);
37
+ }
38
+ catch {
39
+ // Windows permissions are controlled by the user account.
40
+ }
41
+ }
42
+ function validPassword(password) {
43
+ const auth = readAuth();
44
+ if (!auth)
45
+ return false;
46
+ try {
47
+ const actual = scryptSync(password, auth.salt, 32);
48
+ const expected = Buffer.from(auth.passwordHash, "hex");
49
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ function htmlEscape(value) {
56
+ return value.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
57
+ }
58
+ function mask(value) {
59
+ if (!value)
60
+ return "";
61
+ if (value.length <= 8)
62
+ return "•".repeat(value.length);
63
+ return `${value.slice(0, 5)}${"•".repeat(Math.min(16, value.length - 8))}${value.slice(-3)}`;
64
+ }
65
+ function cookieValue(req, name) {
66
+ const cookies = req.headers.cookie?.split(";").map((x) => x.trim()) ?? [];
67
+ const found = cookies.find((x) => x.startsWith(`${name}=`));
68
+ return found ? decodeURIComponent(found.slice(name.length + 1)) : undefined;
69
+ }
70
+ function currentSession(req) {
71
+ const token = cookieValue(req, "codeshark_session");
72
+ const session = token ? sessions.get(token) : undefined;
73
+ if (!session || session.expiresAt < Date.now()) {
74
+ if (token)
75
+ sessions.delete(token);
76
+ return undefined;
77
+ }
78
+ return session;
79
+ }
80
+ function send(res, status, body, headers = {}) {
81
+ res.writeHead(status, {
82
+ "content-type": "text/html; charset=utf-8",
83
+ "cache-control": "no-store",
84
+ ...headers,
85
+ });
86
+ res.end(body);
87
+ }
88
+ function redirect(res, location, cookie) {
89
+ res.writeHead(303, {
90
+ location,
91
+ "cache-control": "no-store",
92
+ ...(cookie ? { "set-cookie": cookie } : {}),
93
+ });
94
+ res.end();
95
+ }
96
+ function page(title, body) {
97
+ return `<!doctype html>
98
+ <html lang="en">
99
+ <head>
100
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
101
+ <title>${htmlEscape(title)} · CodeShark</title>
102
+ <style>
103
+ :root{color-scheme:dark;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;background:#101318;color:#e7edf3}
104
+ body{max-width:760px;margin:0 auto;padding:42px 22px}h1{font-size:25px;margin:0 0 8px;color:#8fc5ed}h2{font-size:16px;margin-top:30px;color:#d96b43}p,.hint{color:#98a6b3;line-height:1.55}.card{background:#191f27;border:1px solid #2e3a45;border-radius:12px;padding:20px;margin:18px 0}label{display:block;color:#c6d3de;margin:15px 0 7px}input{box-sizing:border-box;width:100%;padding:12px;border-radius:7px;border:1px solid #40505d;background:#0d1117;color:#fff;font:inherit}button{margin-top:18px;padding:11px 16px;border:0;border-radius:7px;background:#4b9bd1;color:#07111a;font:inherit;font-weight:700;cursor:pointer}button.danger{background:#7d3941;color:#fff}.key{font-size:13px;word-break:break-all;color:#c6e3f8}.warning{border-left:3px solid #d96b43;padding-left:12px}.small{font-size:12px;color:#778896}a{color:#8fc5ed}
105
+ </style></head><body>${body}</body></html>`;
106
+ }
107
+ function loginPage(message = "") {
108
+ const auth = readAuth();
109
+ if (!auth) {
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
+ }
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
+ }
114
+ function vaultPage(session) {
115
+ const cfg = loadConfig();
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>🦈 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", "UnoRouter 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", "Google 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">UnoRouter: ${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
+ }
119
+ function parseBody(req) {
120
+ return new Promise((resolve, reject) => {
121
+ let body = "";
122
+ req.on("data", (chunk) => {
123
+ body += chunk.toString("utf8");
124
+ if (body.length > MAX_BODY_BYTES) {
125
+ reject(new Error("request too large"));
126
+ req.destroy();
127
+ }
128
+ });
129
+ req.on("end", () => {
130
+ const params = new URLSearchParams(body);
131
+ resolve(Object.fromEntries(params.entries()));
132
+ });
133
+ req.on("error", reject);
134
+ });
135
+ }
136
+ function isValidSessionRequest(req, body, session) {
137
+ return Boolean(session && body.csrf && body.csrf === session.csrf);
138
+ }
139
+ async function handle(req, res) {
140
+ const method = req.method ?? "GET";
141
+ const path = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
142
+ const session = currentSession(req);
143
+ if (method === "GET" && path === "/") {
144
+ send(res, 200, session ? vaultPage(session) : loginPage());
145
+ return;
146
+ }
147
+ if (method === "POST" && path === "/setup") {
148
+ const body = await parseBody(req);
149
+ if (readAuth()) {
150
+ send(res, 403, loginPage("A password already exists. Unlock the vault instead."));
151
+ return;
152
+ }
153
+ if (!body.password || body.password.length < 8 || body.password !== body.confirm) {
154
+ send(res, 400, loginPage("Passwords must match and be at least 8 characters."));
155
+ return;
156
+ }
157
+ saveAuth(body.password);
158
+ return redirect(res, "/");
159
+ }
160
+ if (method === "POST" && path === "/login") {
161
+ const body = await parseBody(req);
162
+ if (!validPassword(body.password ?? "")) {
163
+ send(res, 401, loginPage("Incorrect password."));
164
+ return;
165
+ }
166
+ const token = randomBytes(32).toString("hex");
167
+ const newSession = { token, csrf: randomBytes(24).toString("hex"), expiresAt: Date.now() + SESSION_TTL_MS };
168
+ sessions.set(token, newSession);
169
+ return redirect(res, "/", `codeshark_session=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=1800`);
170
+ }
171
+ if (method === "POST" && path === "/save") {
172
+ const body = await parseBody(req);
173
+ if (!isValidSessionRequest(req, body, session)) {
174
+ send(res, 403, loginPage("Your session expired. Unlock the vault again."));
175
+ return;
176
+ }
177
+ const cfg = loadConfig();
178
+ for (const field of ["unorouterApiKey", "openrouterApiKey", "nvidiaApiKey", "geminiApiKey"]) {
179
+ const value = body[field]?.trim();
180
+ if (body[`clear_${field}`] === "1")
181
+ delete cfg[field];
182
+ else if (value)
183
+ cfg[field] = value;
184
+ // A blank field preserves the existing secret instead of deleting it.
185
+ }
186
+ saveConfig(cfg);
187
+ return redirect(res, "/");
188
+ }
189
+ if (method === "POST" && path === "/logout") {
190
+ const body = await parseBody(req);
191
+ if (isValidSessionRequest(req, body, session)) {
192
+ sessions.delete(session.token);
193
+ return redirect(res, "/", "codeshark_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0");
194
+ }
195
+ send(res, 403, loginPage("Your session expired."));
196
+ return;
197
+ }
198
+ send(res, 404, page("Not found", "<h1>404</h1><p>That CodeShark vault page does not exist.</p>"));
199
+ }
200
+ function openBrowser(url) {
201
+ if (process.env.CODESHARK_NO_BROWSER)
202
+ return;
203
+ const command = process.platform === "win32" ? `cmd /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
204
+ exec(command, () => { });
205
+ }
206
+ export async function launchKeysPage(port = DEFAULT_PORT) {
207
+ if (pageServer && pageUrl) {
208
+ openBrowser(pageUrl);
209
+ return pageUrl;
210
+ }
211
+ pageServer = createServer((req, res) => {
212
+ void handle(req, res).catch(() => send(res, 400, page("Error", "<h1>Bad request</h1>")));
213
+ });
214
+ await new Promise((resolve, reject) => {
215
+ pageServer.once("error", reject);
216
+ pageServer.listen(port, "127.0.0.1", () => resolve());
217
+ });
218
+ const address = pageServer.address();
219
+ const actualPort = typeof address === "object" && address ? address.port : port;
220
+ pageUrl = `http://127.0.0.1:${actualPort}`;
221
+ openBrowser(pageUrl);
222
+ return pageUrl;
223
+ }
224
+ export function closeKeysPage() {
225
+ if (pageServer)
226
+ pageServer.close();
227
+ pageServer = undefined;
228
+ pageUrl = undefined;
229
+ sessions.clear();
230
+ }
231
+ export function keyVaultPath() {
232
+ return authPath();
233
+ }
@@ -0,0 +1,63 @@
1
+ import { dim, hex } from "./ansi.js";
2
+ /**
3
+ * Startup animation helpers: a typewriter-style loading screen and a
4
+ * "thinking" spinner. Zero dependencies; both degrade to static output
5
+ * when stdout is not a TTY (pipes, CI, tests).
6
+ */
7
+ // Braille dots look great in Windows Terminal and modern terminals;
8
+ // legacy consoles get the classic -\|/ frames.
9
+ const FRAMES = process.platform === "win32" && !process.env.WT_SESSION
10
+ ? ["-", "\\", "|", "/"]
11
+ : ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
12
+ const ACCENT = "#8ba3ba";
13
+ const OK = "#4ade80";
14
+ const TICK_MS = 14; // per typed character
15
+ const DONE_MS = 130; // hold the check before the next step
16
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
17
+ function canAnimate() {
18
+ return Boolean(process.stdout.isTTY) && !process.env.CODESHARK_NO_LOADING;
19
+ }
20
+ /**
21
+ * Startup loading screen. Each step's text types itself out character by
22
+ * character ("a", "am", "ame", …) while a spinner cycles beside it, then
23
+ * locks in with a green ✓ before the next step begins. Non-TTY output
24
+ * prints the steps instantly.
25
+ */
26
+ export async function showLoading(steps) {
27
+ if (!canAnimate()) {
28
+ for (const step of steps)
29
+ console.log(dim(` ${step}`));
30
+ return;
31
+ }
32
+ const write = (line) => process.stdout.write(`\r\u001b[2K${line}`);
33
+ for (const step of steps) {
34
+ let frame = 0;
35
+ for (let i = 0; i <= step.length; i++) {
36
+ const f = FRAMES[frame % FRAMES.length];
37
+ write(` ${hex(ACCENT, f)} ${step.slice(0, i)}`);
38
+ frame++;
39
+ await sleep(TICK_MS);
40
+ }
41
+ write(` ${hex(OK, "✓")} ${step}`);
42
+ await sleep(DONE_MS);
43
+ }
44
+ process.stdout.write("\n");
45
+ }
46
+ /**
47
+ * A "thinking" spinner on a single line. Returns a stop function that
48
+ * clears the line — call it as soon as the first token arrives.
49
+ */
50
+ export function startThinkingSpinner(label) {
51
+ if (!canAnimate())
52
+ return () => { };
53
+ let frame = 0;
54
+ process.stdout.write(`\r\u001b[2K ${hex(ACCENT, FRAMES[0])} ${label}`);
55
+ const timer = setInterval(() => {
56
+ frame = (frame + 1) % FRAMES.length;
57
+ process.stdout.write(`\r\u001b[2K ${hex(ACCENT, FRAMES[frame])} ${label}`);
58
+ }, 80);
59
+ return () => {
60
+ clearInterval(timer);
61
+ process.stdout.write("\r\u001b[2K");
62
+ };
63
+ }
package/dist/models.js ADDED
@@ -0,0 +1,54 @@
1
+ export const MODELS = [
2
+ {
3
+ id: "unorouter/gpt-5.6-sol",
4
+ label: "Chat-GPT 5.6 Sol",
5
+ provider: "unorouter",
6
+ model: "gpt-5.6-sol:free",
7
+ context: "400K",
8
+ notes: "OpenAI's frontier reasoning model.",
9
+ },
10
+ {
11
+ id: "unorouter/deepseek-v4-flash",
12
+ label: "DeepSeek-V4 Flash",
13
+ provider: "unorouter",
14
+ model: "deepseek-v4-flash-0731:free",
15
+ context: "256K",
16
+ notes: "DeepSeek's fast flash model, strong at code.",
17
+ },
18
+ {
19
+ id: "unorouter/glm-5.3-flash-thinking",
20
+ label: "GLM 5.3 Flash Thinking",
21
+ provider: "unorouter",
22
+ model: "glm-5.3-flash-thinking:free",
23
+ context: "1M",
24
+ notes: "Zhipu's reasoning coder with 1M context — the default.",
25
+ },
26
+ {
27
+ id: "unorouter/kimi-k3",
28
+ label: "Kimi-K3",
29
+ provider: "unorouter",
30
+ model: "kimi-k3:free",
31
+ context: "256K",
32
+ notes: "Moonshot's flagship coding model.",
33
+ },
34
+ {
35
+ id: "unorouter/gemini-3.6-flash",
36
+ label: "Gemini 3.6 Flash",
37
+ provider: "unorouter",
38
+ model: "gemini-3.6-flash:free",
39
+ context: "1M",
40
+ notes: "Google's fast flash model with a huge 1M context.",
41
+ },
42
+ ];
43
+ export const DEFAULT_MODEL_ID = "unorouter/glm-5.3-flash-thinking";
44
+ export function listModels() {
45
+ return [...MODELS];
46
+ }
47
+ export function findModel(idOrSlug) {
48
+ const s = idOrSlug.trim();
49
+ return MODELS.find((m) => m.id === s || m.model === s);
50
+ }
51
+ /** Map a catalog id (or raw slug) to the raw slug the provider API expects. */
52
+ export function toApiSlug(idOrSlug) {
53
+ return findModel(idOrSlug)?.model ?? idOrSlug;
54
+ }