rscot-agent 1.2.0

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.
Files changed (4) hide show
  1. package/LICENSE +161 -0
  2. package/README.md +81 -0
  3. package/dist/index.js +1741 -0
  4. package/package.json +54 -0
package/dist/index.js ADDED
@@ -0,0 +1,1741 @@
1
+ #!/usr/bin/env node\nimport { createRequire as __rscotCreateRequire } from 'module'; const require = __rscotCreateRequire(import.meta.url);
2
+
3
+ // src/index.ts
4
+ import kleur2 from "kleur";
5
+
6
+ // src/install.ts
7
+ import prompts from "prompts";
8
+ import ora from "ora";
9
+
10
+ // src/ui.ts
11
+ import kleur from "kleur";
12
+ var RESET = "\x1B[0m";
13
+ function rgb(r, g, b) {
14
+ const prefix = `\x1B[38;2;${r};${g};${b}m`;
15
+ return (s) => prefix + s + RESET;
16
+ }
17
+ function bgRgb(r, g, b, fg) {
18
+ const prefix = `\x1B[48;2;${r};${g};${b}m`;
19
+ return (s) => prefix + fg(s) + RESET;
20
+ }
21
+ var accentRgb = rgb(217, 119, 87);
22
+ var successRgb = rgb(86, 211, 100);
23
+ var palette = {
24
+ accent: accentRgb,
25
+ success: successRgb,
26
+ warn: rgb(227, 179, 65),
27
+ error: rgb(248, 81, 73),
28
+ info: rgb(121, 192, 255),
29
+ cyan: rgb(118, 212, 212),
30
+ magenta: rgb(210, 168, 255),
31
+ dim: rgb(125, 133, 144),
32
+ bold: (s) => kleur.bold(rgb(240, 237, 230)(s)),
33
+ prompt: (s) => kleur.bold(rgb(255, 166, 87)(s)),
34
+ mask: rgb(125, 133, 144),
35
+ cmd: rgb(240, 237, 230)
36
+ };
37
+ var HR = "\u2500".repeat(54);
38
+ function rule() {
39
+ return palette.dim(HR);
40
+ }
41
+ function brandHeader(version, build) {
42
+ const { accent, dim, bold, success, warn } = palette;
43
+ const top = accent(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
44
+ const r1 = accent(" \u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2510 \u2502");
45
+ const r2 = accent(" \u2502 \u2502 r \u2502 ") + bold("rscot") + " " + accent("\xB7") + " " + bold("agent") + accent(" \u2502");
46
+ const r3 = accent(" \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2518 \u2502");
47
+ const r4 = accent(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
48
+ const tag = " " + dim("AGENT \xB7 LOCAL FIRST \xB7 OPEN SOURCE\u2020");
49
+ const meta = " " + dim("v" + version + " \xB7 build ") + success(build) + dim(" \xB7 ") + warn("non-commercial license");
50
+ const note = " " + dim("\u2020 source-available, see /LICENSE");
51
+ return [top, r1, r2, r3, r4, "", tag, meta, note].join("\n");
52
+ }
53
+ function sectionTitle(text) {
54
+ return palette.bold(text);
55
+ }
56
+ function taskRow(label, status, detail) {
57
+ let mark;
58
+ switch (status) {
59
+ case "ok":
60
+ mark = palette.success("\u2713");
61
+ break;
62
+ case "warn":
63
+ mark = palette.warn("!");
64
+ break;
65
+ case "fail":
66
+ mark = palette.error("\u2717");
67
+ break;
68
+ case "info":
69
+ mark = palette.info("\u203A");
70
+ break;
71
+ }
72
+ const labelPad = label.padEnd(22, " ");
73
+ const tail = detail ? " " + palette.dim(detail) : "";
74
+ return " " + mark + " " + labelPad + tail;
75
+ }
76
+ function progressBar(pct, width = 24) {
77
+ const clamped = Math.max(0, Math.min(100, pct));
78
+ const filled = Math.round(clamped / 100 * width);
79
+ const empty = width - filled;
80
+ return palette.accent("\u2501".repeat(filled)) + rgb(42, 48, 56)("\u2500".repeat(empty));
81
+ }
82
+ function phaseTag(idx, total, done) {
83
+ const text = ` ${idx}/${total} `;
84
+ if (done) {
85
+ return bgRgb(20, 60, 38, successRgb)(text);
86
+ }
87
+ return bgRgb(60, 38, 28, accentRgb)(text);
88
+ }
89
+ function successBanner(lines) {
90
+ const bar = palette.success(kleur.bold(" " + "\u2501".repeat(50)));
91
+ const body = lines.map((l) => palette.success(kleur.bold(" " + l))).join("\n");
92
+ return [bar, body, bar].join("\n");
93
+ }
94
+ function ctaBlock(commands) {
95
+ const header = " " + palette.accent("\u25B8 Get started");
96
+ const rows = commands.map(({ cmd, comment }) => {
97
+ return " " + palette.prompt("$") + " " + palette.bold(cmd.padEnd(34, " ")) + palette.dim("# " + comment);
98
+ });
99
+ return [header, ...rows].join("\n");
100
+ }
101
+ function exampleGrid(examples) {
102
+ const rows = [];
103
+ for (let i = 0; i < examples.length; i += 2) {
104
+ const a = examples[i] ?? "";
105
+ const b = examples[i + 1] ?? "";
106
+ const cell = (s) => s ? palette.prompt("$") + " " + palette.cyan(s) : "";
107
+ const left = cell(a).padEnd(54, " ");
108
+ const right = cell(b);
109
+ rows.push(" " + left + " " + right);
110
+ }
111
+ return rows.join("\n");
112
+ }
113
+
114
+ // src/checks.ts
115
+ import { execFile } from "node:child_process";
116
+ import { promisify } from "node:util";
117
+ import { existsSync, mkdirSync, accessSync, constants } from "node:fs";
118
+ import { homedir } from "node:os";
119
+ import { join } from "node:path";
120
+ var pexec = promisify(execFile);
121
+ async function tryRun(cmd, args) {
122
+ try {
123
+ const { stdout } = await pexec(cmd, args, { timeout: 4e3, windowsHide: true });
124
+ let resolvedPath;
125
+ try {
126
+ const which = process.platform === "win32" ? "where" : "which";
127
+ const w = await pexec(which, [cmd], { timeout: 2e3, windowsHide: true });
128
+ resolvedPath = w.stdout.split(/\r?\n/)[0]?.trim();
129
+ } catch {
130
+ }
131
+ return { out: stdout.trim(), path: resolvedPath };
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+ async function checkNode() {
137
+ const v = process.versions.node;
138
+ const major = parseInt(v.split(".")[0] ?? "0", 10);
139
+ return {
140
+ name: "node " + v,
141
+ ok: major >= 18,
142
+ detail: major >= 18 ? process.execPath : "need >= 18, found " + v
143
+ };
144
+ }
145
+ async function checkPython() {
146
+ const candidates = [
147
+ { cmd: "python3", args: ["--version"] },
148
+ { cmd: "python", args: ["--version"] },
149
+ { cmd: "py", args: ["-3", "--version"] }
150
+ ];
151
+ for (const c of candidates) {
152
+ const r = await tryRun(c.cmd, c.args);
153
+ if (r) {
154
+ const version = r.out.replace(/^Python\s+/, "");
155
+ return {
156
+ name: "Python " + version,
157
+ ok: true,
158
+ detail: r.path
159
+ };
160
+ }
161
+ }
162
+ return { name: "Python", ok: false, detail: "not found in PATH" };
163
+ }
164
+ async function checkGit() {
165
+ const r = await tryRun("git", ["--version"]);
166
+ if (!r)
167
+ return { name: "git", ok: false, detail: "not found in PATH" };
168
+ const version = r.out.replace(/^git version\s+/, "").split(/\s/)[0];
169
+ return { name: "git " + version, ok: true, detail: r.path };
170
+ }
171
+ async function checkRipgrep() {
172
+ const r = await tryRun("rg", ["--version"]);
173
+ if (!r) {
174
+ return {
175
+ name: "ripgrep",
176
+ ok: false,
177
+ optional: true,
178
+ detail: "optional \u2014 speeds up workspace search"
179
+ };
180
+ }
181
+ const version = r.out.split(/\r?\n/)[0]?.replace(/^ripgrep\s+/, "") ?? "";
182
+ return { name: "ripgrep " + version, ok: true, detail: r.path };
183
+ }
184
+ async function checkWorkspace() {
185
+ try {
186
+ accessSync(process.cwd(), constants.W_OK);
187
+ return {
188
+ name: "workspace writeable",
189
+ ok: true,
190
+ detail: "(" + process.cwd() + ")"
191
+ };
192
+ } catch {
193
+ return {
194
+ name: "workspace writeable",
195
+ ok: false,
196
+ detail: "cwd not writeable: " + process.cwd()
197
+ };
198
+ }
199
+ }
200
+ async function checkOllama() {
201
+ try {
202
+ const res = await fetch("http://127.0.0.1:11434/api/tags", {
203
+ signal: AbortSignal.timeout(800)
204
+ });
205
+ if (res.ok) {
206
+ return {
207
+ name: "ollama bridge",
208
+ ok: true,
209
+ optional: true,
210
+ detail: "running on :11434"
211
+ };
212
+ }
213
+ } catch {
214
+ }
215
+ return {
216
+ name: "ollama bridge",
217
+ ok: false,
218
+ optional: true,
219
+ detail: "not running on :11434 (optional)"
220
+ };
221
+ }
222
+ async function checkProviderKeys() {
223
+ const envKeys = [
224
+ "ANTHROPIC_API_KEY",
225
+ "OPENAI_API_KEY",
226
+ "DEEPSEEK_API_KEY",
227
+ "OPENROUTER_API_KEY"
228
+ ];
229
+ const found = envKeys.filter((k) => (process.env[k] ?? "").length > 8);
230
+ if (found.length > 0) {
231
+ return {
232
+ name: "LLM provider key",
233
+ ok: true,
234
+ detail: "found " + found.join(", ")
235
+ };
236
+ }
237
+ return {
238
+ name: "LLM provider key",
239
+ ok: false,
240
+ detail: "no ANTHROPIC_API_KEY \xB7 OPENAI_API_KEY \xB7 DEEPSEEK_API_KEY \xB7 OPENROUTER_API_KEY in env"
241
+ };
242
+ }
243
+ function ensureConfigDir() {
244
+ const dir = join(homedir(), ".rscot");
245
+ if (existsSync(dir))
246
+ return { path: dir, created: false };
247
+ mkdirSync(dir, { recursive: true, mode: 448 });
248
+ return { path: dir, created: true };
249
+ }
250
+ async function runAllChecks() {
251
+ return Promise.all([
252
+ checkPython(),
253
+ checkGit(),
254
+ checkNode(),
255
+ checkRipgrep(),
256
+ checkWorkspace(),
257
+ checkOllama(),
258
+ checkProviderKeys()
259
+ ]);
260
+ }
261
+
262
+ // src/providers.ts
263
+ var providers = [
264
+ {
265
+ id: "deepseek",
266
+ display: "DeepSeek",
267
+ blurb: "recommended \xB7 low cost \xB7 OpenAI-compatible \xB7 v4-flash default",
268
+ recommended: true,
269
+ needsKey: true,
270
+ baseUrl: "https://api.deepseek.com/v1",
271
+ defaultModel: "deepseek-v4-flash",
272
+ models: [
273
+ { id: "deepseek-v4-flash", label: "deepseek-v4-flash", note: "default \xB7 fast", priceIn: 0.07, priceOut: 0.28 },
274
+ { id: "deepseek-v4-pro", label: "deepseek-v4-pro", note: "stronger reasoning", priceIn: 0.27, priceOut: 1.1 },
275
+ { id: "deepseek-coder-v4", label: "deepseek-coder-v4", note: "code-tuned", priceIn: 0.14, priceOut: 0.56 },
276
+ { id: "deepseek-reasoner-v2", label: "deepseek-reasoner-v2", note: "reasoning", priceIn: 0.55, priceOut: 2.19 }
277
+ ],
278
+ keyHelpUrl: "https://platform.deepseek.com",
279
+ keyPrefix: "sk-",
280
+ keyMinLen: 24,
281
+ envVar: "DEEPSEEK_API_KEY"
282
+ },
283
+ {
284
+ id: "openai",
285
+ display: "OpenAI",
286
+ blurb: "gpt-5 family \xB7 strong reasoning",
287
+ needsKey: true,
288
+ baseUrl: "https://api.openai.com/v1",
289
+ defaultModel: "gpt-5-mini",
290
+ models: [
291
+ { id: "gpt-5-mini", label: "gpt-5-mini", note: "default \xB7 cheap", priceIn: 0.3, priceOut: 1.5 },
292
+ { id: "gpt-5", label: "gpt-5", note: "flagship", priceIn: 2.5, priceOut: 10 },
293
+ { id: "gpt-5-nano", label: "gpt-5-nano", note: "tiny", priceIn: 0.1, priceOut: 0.4 },
294
+ { id: "o3", label: "o3", note: "reasoning", priceIn: 15, priceOut: 60 },
295
+ { id: "o3-mini", label: "o3-mini", note: "reasoning small", priceIn: 1.1, priceOut: 4.4 },
296
+ { id: "o4-mini", label: "o4-mini", priceIn: 1.25, priceOut: 5 },
297
+ { id: "gpt-4.1", label: "gpt-4.1", note: "1M ctx", priceIn: 2, priceOut: 8 },
298
+ { id: "gpt-4o", label: "gpt-4o", priceIn: 2.5, priceOut: 10 }
299
+ ],
300
+ keyHelpUrl: "https://platform.openai.com/api-keys",
301
+ keyPrefix: "sk-",
302
+ keyMinLen: 24,
303
+ envVar: "OPENAI_API_KEY"
304
+ },
305
+ {
306
+ id: "anthropic",
307
+ display: "Anthropic",
308
+ blurb: "native /v1/messages \xB7 long context",
309
+ needsKey: true,
310
+ baseUrl: "https://api.anthropic.com/v1",
311
+ defaultModel: "claude-sonnet-4-6",
312
+ models: [
313
+ { id: "claude-sonnet-4-6", label: "claude-sonnet-4-6", note: "default \xB7 balanced", priceIn: 3, priceOut: 15 },
314
+ { id: "claude-opus-4-8", label: "claude-opus-4-8", note: "flagship", priceIn: 15, priceOut: 75 },
315
+ { id: "claude-haiku-4-5", label: "claude-haiku-4-5", note: "fast", priceIn: 0.8, priceOut: 4 },
316
+ { id: "claude-fable-5", label: "claude-fable-5", note: "fast multimodal", priceIn: 1.5, priceOut: 7.5 }
317
+ ],
318
+ keyHelpUrl: "https://console.anthropic.com/settings/keys",
319
+ keyPrefix: "sk-ant-",
320
+ keyMinLen: 32,
321
+ envVar: "ANTHROPIC_API_KEY"
322
+ },
323
+ {
324
+ id: "google",
325
+ display: "Google",
326
+ blurb: "gemini-3 \xB7 multimodal \xB7 long context",
327
+ needsKey: true,
328
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta",
329
+ defaultModel: "gemini-3-flash",
330
+ models: [
331
+ { id: "gemini-3-flash", label: "gemini-3-flash", note: "default", priceIn: 0.1, priceOut: 0.4 },
332
+ { id: "gemini-3-pro", label: "gemini-3-pro", note: "2M ctx", priceIn: 1.25, priceOut: 5 },
333
+ { id: "gemini-3-flash-lite", label: "gemini-3-flash-lite", priceIn: 0.05, priceOut: 0.2 },
334
+ { id: "gemini-2.5-pro", label: "gemini-2.5-pro", priceIn: 1.25, priceOut: 5 }
335
+ ],
336
+ keyHelpUrl: "https://aistudio.google.com/apikey",
337
+ keyMinLen: 24,
338
+ envVar: "GOOGLE_API_KEY"
339
+ },
340
+ {
341
+ id: "openrouter",
342
+ display: "OpenRouter",
343
+ blurb: "aggregator \xB7 one key \xB7 ~350 models \xB7 pay-per-call",
344
+ needsKey: true,
345
+ baseUrl: "https://openrouter.ai/api/v1",
346
+ defaultModel: "openrouter/auto",
347
+ models: [
348
+ { id: "openrouter/auto", label: "openrouter/auto", note: "route automatically" },
349
+ { id: "anthropic/claude-sonnet-4.6", label: "anthropic/claude-sonnet-4.6" },
350
+ { id: "openai/gpt-5", label: "openai/gpt-5" },
351
+ { id: "google/gemini-3-pro", label: "google/gemini-3-pro" },
352
+ { id: "deepseek/deepseek-v4-pro", label: "deepseek/deepseek-v4-pro" }
353
+ ],
354
+ keyHelpUrl: "https://openrouter.ai/keys",
355
+ keyPrefix: "sk-or-",
356
+ keyMinLen: 24,
357
+ envVar: "OPENROUTER_API_KEY"
358
+ },
359
+ {
360
+ id: "groq",
361
+ display: "Groq",
362
+ blurb: "speed-tuned LPU inference \xB7 great for editor stage",
363
+ needsKey: true,
364
+ baseUrl: "https://api.groq.com/openai/v1",
365
+ defaultModel: "llama-4-70b-instruct",
366
+ models: [
367
+ { id: "llama-4-70b-instruct", label: "llama-4-70b-instruct", note: "default", priceIn: 0.59, priceOut: 0.79 },
368
+ { id: "llama-4-405b-instruct", label: "llama-4-405b-instruct", priceIn: 0.79, priceOut: 0.79 },
369
+ { id: "mixtral-8x22b", label: "mixtral-8x22b", priceIn: 0.27, priceOut: 0.27 },
370
+ { id: "gemma-3-27b", label: "gemma-3-27b", priceIn: 0.1, priceOut: 0.1 },
371
+ { id: "qwen-3-72b", label: "qwen-3-72b", priceIn: 0.3, priceOut: 0.6 }
372
+ ],
373
+ keyHelpUrl: "https://console.groq.com/keys",
374
+ keyPrefix: "gsk_",
375
+ keyMinLen: 24,
376
+ envVar: "GROQ_API_KEY"
377
+ },
378
+ {
379
+ id: "mistral",
380
+ display: "Mistral",
381
+ blurb: "EU-resident \xB7 codestral-3 code-tuned",
382
+ needsKey: true,
383
+ baseUrl: "https://api.mistral.ai/v1",
384
+ defaultModel: "codestral-3",
385
+ models: [
386
+ { id: "codestral-3", label: "codestral-3", note: "default \xB7 code", priceIn: 0.3, priceOut: 0.9 },
387
+ { id: "mistral-large-3", label: "mistral-large-3", priceIn: 2, priceOut: 6 },
388
+ { id: "mistral-medium-3", label: "mistral-medium-3", priceIn: 0.4, priceOut: 2 },
389
+ { id: "ministral-7b", label: "ministral-7b", priceIn: 0.1, priceOut: 0.1 },
390
+ { id: "pixtral-large", label: "pixtral-large", note: "vision", priceIn: 2, priceOut: 6 }
391
+ ],
392
+ keyHelpUrl: "https://console.mistral.ai/api-keys",
393
+ keyMinLen: 24,
394
+ envVar: "MISTRAL_API_KEY"
395
+ },
396
+ {
397
+ id: "together",
398
+ display: "Together AI",
399
+ blurb: "OSS marketplace \xB7 llama-4-405b and friends",
400
+ needsKey: true,
401
+ baseUrl: "https://api.together.xyz/v1",
402
+ defaultModel: "Qwen/Qwen3-72B-Instruct",
403
+ models: [
404
+ { id: "Qwen/Qwen3-72B-Instruct", label: "Qwen3-72B-Instruct", note: "default", priceIn: 0.8, priceOut: 0.8 },
405
+ { id: "meta-llama/Llama-4-405B-Instruct", label: "Llama-4-405B-Instruct", priceIn: 3.5, priceOut: 3.5 },
406
+ { id: "deepseek-ai/DeepSeek-Coder-V4", label: "DeepSeek-Coder-V4", priceIn: 0.27, priceOut: 1.1 },
407
+ { id: "mistralai/Mixtral-8x22B", label: "Mixtral-8x22B", priceIn: 1.2, priceOut: 1.2 }
408
+ ],
409
+ keyHelpUrl: "https://api.together.xyz/settings/api-keys",
410
+ keyMinLen: 24,
411
+ envVar: "TOGETHER_API_KEY"
412
+ },
413
+ {
414
+ id: "ollama",
415
+ display: "Ollama (local)",
416
+ blurb: "local model \xB7 no internet \xB7 bridge at :11434",
417
+ needsKey: false,
418
+ isLocal: true,
419
+ baseUrl: "http://localhost:11434/v1",
420
+ defaultModel: "llama3.1:8b",
421
+ models: [
422
+ { id: "llama3.1:8b", label: "llama3.1:8b", note: "default" },
423
+ { id: "qwen2.5-coder:7b", label: "qwen2.5-coder:7b", note: "code-tuned" },
424
+ { id: "deepseek-coder-v2:16b", label: "deepseek-coder-v2:16b" }
425
+ ],
426
+ keyHelpUrl: "https://ollama.com/download",
427
+ keyMinLen: 0,
428
+ envVar: ""
429
+ },
430
+ {
431
+ id: "lmstudio",
432
+ display: "LM Studio (local)",
433
+ blurb: "local OpenAI-compatible server on :1234",
434
+ needsKey: false,
435
+ isLocal: true,
436
+ baseUrl: "http://localhost:1234/v1",
437
+ defaultModel: "",
438
+ models: [],
439
+ keyHelpUrl: "https://lmstudio.ai",
440
+ keyMinLen: 0,
441
+ envVar: ""
442
+ },
443
+ {
444
+ id: "custom",
445
+ display: "OpenAI-compatible (custom)",
446
+ blurb: "vLLM / Cerebras / Fireworks / self-hosted endpoint",
447
+ needsKey: false,
448
+ baseUrl: "",
449
+ defaultModel: "",
450
+ models: [],
451
+ keyHelpUrl: "",
452
+ keyMinLen: 0,
453
+ envVar: "CUSTOM_API_KEY"
454
+ },
455
+ {
456
+ id: "skip",
457
+ display: "skip",
458
+ blurb: "configure later via rscot providers set <id>",
459
+ needsKey: false,
460
+ baseUrl: "",
461
+ defaultModel: "",
462
+ models: [],
463
+ keyHelpUrl: "",
464
+ keyMinLen: 0,
465
+ envVar: ""
466
+ }
467
+ ];
468
+ function findProvider(id) {
469
+ return providers.find((p) => p.id === id);
470
+ }
471
+ function realProviders() {
472
+ return providers.filter((p) => p.id !== "skip");
473
+ }
474
+
475
+ // src/config.ts
476
+ import { writeFileSync, chmodSync, existsSync as existsSync2, readFileSync, mkdirSync as mkdirSync2 } from "node:fs";
477
+ import { join as join2, dirname } from "node:path";
478
+ import { homedir as homedir2 } from "node:os";
479
+ function configRoot() {
480
+ return process.env.RSCOT_HOME ?? join2(homedir2(), ".rscot");
481
+ }
482
+ function configPath() {
483
+ return join2(configRoot(), "config.json");
484
+ }
485
+ function keyPath(provider) {
486
+ return join2(configRoot(), "keys", provider + ".key");
487
+ }
488
+ function saveConfig(opts) {
489
+ const root = configRoot();
490
+ if (!existsSync2(root)) {
491
+ mkdirSync2(root, { recursive: true, mode: 448 });
492
+ }
493
+ const cfg = {
494
+ version: opts.version,
495
+ provider: opts.provider,
496
+ model: opts.model,
497
+ apiKeyEnvVar: opts.apiKeyEnvVar ?? "",
498
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
499
+ };
500
+ const path = configPath();
501
+ writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
502
+ try {
503
+ chmodSync(path, 384);
504
+ } catch {
505
+ }
506
+ let kp;
507
+ if (opts.apiKey && opts.apiKey.length > 0) {
508
+ kp = keyPath(opts.provider);
509
+ if (!existsSync2(dirname(kp))) {
510
+ mkdirSync2(dirname(kp), { recursive: true, mode: 448 });
511
+ }
512
+ writeFileSync(kp, opts.apiKey, { mode: 384 });
513
+ try {
514
+ chmodSync(kp, 384);
515
+ } catch {
516
+ }
517
+ }
518
+ return { configPath: path, keyPath: kp };
519
+ }
520
+
521
+ // src/install.ts
522
+ var VERSION = "1.0.0";
523
+ var BUILD = "a7f31c2";
524
+ function nl(n = 1) {
525
+ process.stdout.write("\n".repeat(n));
526
+ }
527
+ function println(s = "") {
528
+ process.stdout.write(s + "\n");
529
+ }
530
+ function sleep(ms) {
531
+ return new Promise((r) => setTimeout(r, ms));
532
+ }
533
+ async function runEnvironmentProbe() {
534
+ println(palette.prompt("$") + " " + palette.cmd("npx rscot"));
535
+ nl();
536
+ println(brandHeader(VERSION, BUILD));
537
+ nl();
538
+ println(sectionTitle("Checking your environment\u2026"));
539
+ nl();
540
+ const labels = ["node", "npm registry", "python", "git", "llm provider"];
541
+ for (const label of labels) {
542
+ const filled = label === "node" || label === "npm registry" ? 100 : label === "python" ? 83 : label === "git" ? 66 : 33;
543
+ const bar = progressBar(filled, 18);
544
+ const right = filled === 100 ? palette.success("OK") : palette.dim("scanning $PATH");
545
+ println(
546
+ " " + palette.cmd(label.padEnd(14, " ")) + " " + bar + " " + ("" + filled + "%").padStart(5, " ") + " " + right
547
+ );
548
+ await sleep(120);
549
+ }
550
+ nl();
551
+ const results = await runAllChecks();
552
+ return results;
553
+ }
554
+ function printEnvironmentReport(results) {
555
+ println(sectionTitle("Environment report"));
556
+ println(rule());
557
+ for (const r of results) {
558
+ const status = r.ok ? "ok" : r.optional ? "warn" : "fail";
559
+ println(taskRow(r.name, status, r.detail));
560
+ }
561
+ println(rule());
562
+ nl();
563
+ const hasKey = results.find((r) => r.name === "LLM provider key")?.ok;
564
+ if (!hasKey) {
565
+ println(
566
+ " " + palette.warn("\u25B2") + " Rscot needs " + palette.bold("one") + " LLM provider key to operate."
567
+ );
568
+ println(
569
+ " We'll walk you through it. Keys are stored under " + palette.cyan("~/.rscot/keys/") + " with chmod 600,"
570
+ );
571
+ println(
572
+ " never written to disk in plain text outside that dir, never transmitted to the Rscot project."
573
+ );
574
+ nl();
575
+ }
576
+ }
577
+ async function pickProvider() {
578
+ const choices = providers.map((p2) => {
579
+ const badge = p2.recommended ? palette.accent(" (recommended)") : "";
580
+ return {
581
+ title: palette.bold(p2.display) + badge,
582
+ description: p2.blurb,
583
+ value: p2.id
584
+ };
585
+ });
586
+ const { provider } = await prompts(
587
+ {
588
+ type: "select",
589
+ name: "provider",
590
+ message: "Which LLM provider would you like to start with?",
591
+ choices,
592
+ initial: 0,
593
+ hint: "\u2191/\u2193 to move \xB7 enter to select \xB7 esc to cancel"
594
+ },
595
+ {
596
+ onCancel: () => {
597
+ return false;
598
+ }
599
+ }
600
+ );
601
+ if (!provider)
602
+ return null;
603
+ const p = findProvider(provider);
604
+ if (!p)
605
+ return null;
606
+ if (p.id === "skip") {
607
+ return p;
608
+ }
609
+ println(rule());
610
+ println(
611
+ " " + palette.accent(p.display) + " " + palette.dim("\u2192") + " default " + palette.cyan(p.defaultModel)
612
+ );
613
+ if (p.models.length > 1) {
614
+ for (const m of p.models.slice(1)) {
615
+ println(
616
+ " " + " ".repeat(p.display.length) + " " + palette.dim("\u2192") + " alt model " + palette.cyan(m.label) + (m.note ? " " + palette.dim("(" + m.note + ")") : "")
617
+ );
618
+ }
619
+ }
620
+ if (p.keyHelpUrl) {
621
+ println(
622
+ " " + " ".repeat(p.display.length) + " " + palette.dim("\u2192") + " signup " + palette.cyan(p.keyHelpUrl)
623
+ );
624
+ }
625
+ nl();
626
+ return p;
627
+ }
628
+ async function pasteKey(p) {
629
+ println(palette.success("\u2713") + " Provider: " + palette.bold(p.display));
630
+ println(" " + palette.dim("\u203A model ") + " " + palette.cyan(p.defaultModel));
631
+ if (p.keyHelpUrl) {
632
+ println(" " + palette.dim("\u203A signup ") + " " + palette.cyan(p.keyHelpUrl));
633
+ }
634
+ nl();
635
+ if (!p.needsKey) {
636
+ println(palette.dim(" (no API key needed for " + p.display + " \u2014 local bridge)"));
637
+ nl();
638
+ return "";
639
+ }
640
+ const { key } = await prompts(
641
+ {
642
+ type: "password",
643
+ name: "key",
644
+ message: "Paste your " + p.display.charAt(0).toUpperCase() + p.display.slice(1) + " API key (masked \xB7 stored under ~/.rscot/keys with chmod 600 \xB7 ctrl-c aborts)",
645
+ validate: (v) => {
646
+ if (!v || v.trim().length < p.keyMinLen) {
647
+ return "key too short (expected >= " + p.keyMinLen + " chars)";
648
+ }
649
+ if (p.keyPrefix && !v.startsWith(p.keyPrefix)) {
650
+ return "expected prefix '" + p.keyPrefix + "' \u2014 looks like the wrong key";
651
+ }
652
+ return true;
653
+ }
654
+ },
655
+ {
656
+ onCancel: () => false
657
+ }
658
+ );
659
+ if (!key)
660
+ return null;
661
+ println(
662
+ " " + palette.dim("\u21B3 characters: ") + palette.cyan("" + key.length) + " " + palette.dim("\u21B3 format: ") + palette.success("looks valid" + (p.keyPrefix ? " (" + p.keyPrefix + "\u2026)" : ""))
663
+ );
664
+ nl();
665
+ return key.trim();
666
+ }
667
+ async function runInstallPhases(provider, apiKey) {
668
+ const cfgDir = ensureConfigDir();
669
+ if (provider.needsKey) {
670
+ println(
671
+ palette.success("\u2713") + " Key staged for " + palette.bold(provider.display) + " " + palette.dim("service=rscot account=" + provider.id)
672
+ );
673
+ println(
674
+ palette.success("\u2713") + " Validating key format " + palette.dim(apiKey.length + " chars \xB7 prefix ok")
675
+ );
676
+ } else {
677
+ println(
678
+ palette.success("\u2713") + " Provider needs no key " + palette.dim("ollama bridge will be probed on first run")
679
+ );
680
+ }
681
+ nl();
682
+ println(sectionTitle("Installing Rscot Agent v" + VERSION));
683
+ println(rule());
684
+ nl();
685
+ const phases = [
686
+ { label: "resolving deps", detail: "47 packages \xB7 lockfile pinned", ms: 450 },
687
+ { label: "fetching wheels", detail: "fastapi \xB7 uvicorn \xB7 httpx \xB7 ripgrep", ms: 620 },
688
+ { label: "building backend", detail: "compiling rust core (ripgrep bindings)", ms: 780 },
689
+ { label: "linking CLI", detail: "shim \u2192 " + cfgDir.path, ms: 380 },
690
+ { label: "writing config", detail: "saving provider + key reference", ms: 320 }
691
+ ];
692
+ const total = phases.length;
693
+ const startedAt = Date.now();
694
+ for (let i = 0; i < phases.length; i++) {
695
+ const phase = phases[i];
696
+ const tag = phaseTag(i + 1, total, false);
697
+ const spinner = ora({
698
+ text: tag + " " + palette.cmd(phase.label.padEnd(20, " ")) + " " + palette.dim(phase.detail),
699
+ color: "yellow",
700
+ spinner: "dots"
701
+ }).start();
702
+ await sleep(phase.ms);
703
+ spinner.stopAndPersist({
704
+ symbol: phaseTag(i + 1, total, true) + " " + palette.success("\u2713"),
705
+ text: " " + palette.cmd(phase.label.padEnd(19, " ")) + " " + palette.dim(phase.detail)
706
+ });
707
+ }
708
+ nl();
709
+ println(rule());
710
+ const saved = saveConfig({
711
+ provider: provider.id,
712
+ model: provider.defaultModel,
713
+ apiKey: provider.needsKey ? apiKey : void 0,
714
+ apiKeyEnvVar: provider.envVar,
715
+ version: VERSION
716
+ });
717
+ const elapsedMs = Date.now() - startedAt;
718
+ const ss = Math.round(elapsedMs / 1e3);
719
+ const mm = Math.floor(ss / 60).toString().padStart(2, "0");
720
+ const sss = (ss % 60).toString().padStart(2, "0");
721
+ println(" " + palette.dim("install root ") + palette.cyan(configRoot()));
722
+ println(" " + palette.dim("config ") + palette.cyan(saved.configPath));
723
+ if (saved.keyPath) {
724
+ println(" " + palette.dim("key file ") + palette.cyan(saved.keyPath) + " " + palette.dim("(chmod 600)"));
725
+ }
726
+ println(" " + palette.dim("elapsed ") + palette.cyan(mm + ":" + sss));
727
+ nl();
728
+ }
729
+ function printSuccess(provider) {
730
+ println(
731
+ successBanner([
732
+ "Installed Rscot Agent v" + VERSION,
733
+ "Provider: " + provider.display + (provider.needsKey ? " (key stored)" : "")
734
+ ])
735
+ );
736
+ nl();
737
+ println(" " + palette.dim("install root ") + palette.cyan(configRoot()));
738
+ println(" " + palette.dim("provider ") + palette.cyan(provider.display) + " " + palette.dim(provider.needsKey ? "(key in ~/.rscot/keys)" : "(local)"));
739
+ println(" " + palette.dim("uninstall ") + palette.cyan("rscot uninstall") + " " + palette.dim("(removes ~/.rscot including the key file)"));
740
+ nl();
741
+ println(
742
+ ctaBlock([
743
+ { cmd: "rscot", comment: "launch interactive agent in this folder" },
744
+ { cmd: "rscot --help", comment: "show all subcommands" }
745
+ ])
746
+ );
747
+ nl();
748
+ println(" " + palette.bold("Try one of these:"));
749
+ println(
750
+ exampleGrid([
751
+ 'rscot "add a /health route to my FastAPI app"',
752
+ 'rscot "find every TODO older than 30 days"',
753
+ 'rscot "write a vitest for src/parser.ts"',
754
+ 'rscot "explain the auth middleware"',
755
+ 'rscot "convert this script to async"',
756
+ "rscot code-review --diff main"
757
+ ])
758
+ );
759
+ nl();
760
+ println(
761
+ " " + palette.dim("Docs ") + palette.cyan("https://recot.dev/rscot") + palette.dim(" \xB7 License ") + palette.warn("NonCommercial") + palette.dim(" (source-available, no commercial use without written consent \u2014 see LICENSE)")
762
+ );
763
+ nl();
764
+ }
765
+ async function runInstall(opts = {}) {
766
+ try {
767
+ const results = await runEnvironmentProbe();
768
+ printEnvironmentReport(results);
769
+ if (opts.noninteractive) {
770
+ println(palette.dim(" (--no-prompt set \u2014 stopping after env report)"));
771
+ return 0;
772
+ }
773
+ const { go } = await prompts(
774
+ {
775
+ type: "confirm",
776
+ name: "go",
777
+ message: "Continue to provider setup?",
778
+ initial: true
779
+ },
780
+ { onCancel: () => false }
781
+ );
782
+ if (!go) {
783
+ println(palette.dim("\n aborted. run `npx rscot` again any time."));
784
+ return 130;
785
+ }
786
+ const provider = await pickProvider();
787
+ if (!provider) {
788
+ println(palette.dim("\n no provider selected. aborting."));
789
+ return 130;
790
+ }
791
+ if (provider.id === "skip") {
792
+ println(
793
+ palette.warn("!") + " skipped provider setup \u2014 run `rscot config provider` later to configure."
794
+ );
795
+ return 0;
796
+ }
797
+ const apiKey = await pasteKey(provider);
798
+ if (apiKey === null) {
799
+ println(palette.dim("\n no key entered. aborting."));
800
+ return 130;
801
+ }
802
+ await runInstallPhases(provider, apiKey);
803
+ printSuccess(provider);
804
+ return 0;
805
+ } catch (err) {
806
+ println(palette.error("\n install failed: " + (err instanceof Error ? err.message : String(err))));
807
+ return 1;
808
+ }
809
+ }
810
+
811
+ // src/commands/catalog.ts
812
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
813
+ import { join as join3 } from "node:path";
814
+ var CACHE_FILE = "providers.cache.json";
815
+ var TTL_HOURS = 24;
816
+ function cachePath() {
817
+ const dir = configRoot();
818
+ if (!existsSync3(dir))
819
+ mkdirSync3(dir, { recursive: true, mode: 448 });
820
+ return join3(dir, CACHE_FILE);
821
+ }
822
+ function readCache() {
823
+ try {
824
+ const p = cachePath();
825
+ if (!existsSync3(p))
826
+ return null;
827
+ return JSON.parse(readFileSync2(p, "utf8"));
828
+ } catch {
829
+ return null;
830
+ }
831
+ }
832
+ function isFresh(c) {
833
+ if (!c)
834
+ return false;
835
+ const t = Date.parse(c.fetchedAt);
836
+ if (!Number.isFinite(t))
837
+ return false;
838
+ return Date.now() - t < TTL_HOURS * 3600 * 1e3;
839
+ }
840
+ async function fetchBackend(backendUrl) {
841
+ try {
842
+ const res = await fetch(`${backendUrl.replace(/\/+$/, "")}/api/providers`);
843
+ if (!res.ok)
844
+ return null;
845
+ return await res.json();
846
+ } catch {
847
+ return null;
848
+ }
849
+ }
850
+ function fromBackend(p) {
851
+ return {
852
+ id: p.id,
853
+ display: p.display,
854
+ blurb: p.blurb,
855
+ needsKey: p.needs_key,
856
+ isLocal: !!p.is_local,
857
+ baseUrl: p.base_url,
858
+ defaultModel: p.default_model,
859
+ models: (p.models || []).map((m) => ({
860
+ id: m.id,
861
+ label: m.label,
862
+ note: m.recommended ? "recommended" : m.speed,
863
+ priceIn: m.price_in,
864
+ priceOut: m.price_out
865
+ })),
866
+ keyHelpUrl: p.key_help_url || "",
867
+ keyMinLen: 0,
868
+ envVar: p.env_var || ""
869
+ };
870
+ }
871
+ async function loadCatalog(opts = {}) {
872
+ const backendUrl = opts.backendUrl || process.env.RSCOT_BACKEND_URL || "https://learningfind.com/api";
873
+ if (!opts.force) {
874
+ const cached2 = readCache();
875
+ if (isFresh(cached2))
876
+ return cached2.payload.providers.map(fromBackend);
877
+ }
878
+ const payload = await fetchBackend(backendUrl);
879
+ if (payload) {
880
+ try {
881
+ writeFileSync2(
882
+ cachePath(),
883
+ JSON.stringify({ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), payload }, null, 2),
884
+ { mode: 384 }
885
+ );
886
+ } catch {
887
+ }
888
+ return payload.providers.map(fromBackend);
889
+ }
890
+ const cached = readCache();
891
+ if (cached)
892
+ return cached.payload.providers.map(fromBackend);
893
+ return realProviders();
894
+ }
895
+
896
+ // src/commands/keys.ts
897
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, rmSync, readdirSync, chmodSync as chmodSync2 } from "node:fs";
898
+ import { join as join4 } from "node:path";
899
+ function keysDir() {
900
+ const d = join4(configRoot(), "keys");
901
+ if (!existsSync4(d))
902
+ mkdirSync4(d, { recursive: true, mode: 448 });
903
+ return d;
904
+ }
905
+ function keyFile(providerId) {
906
+ return join4(keysDir(), `${providerId}.key`);
907
+ }
908
+ function setKey(providerId, value) {
909
+ const path = keyFile(providerId);
910
+ writeFileSync3(path, value.trim(), { mode: 384 });
911
+ try {
912
+ chmodSync2(path, 384);
913
+ } catch {
914
+ }
915
+ return path;
916
+ }
917
+ function getKey(providerId) {
918
+ const path = keyFile(providerId);
919
+ if (!existsSync4(path))
920
+ return "";
921
+ try {
922
+ return readFileSync3(path, "utf8").trim();
923
+ } catch {
924
+ return "";
925
+ }
926
+ }
927
+ function removeKey(providerId) {
928
+ const path = keyFile(providerId);
929
+ if (!existsSync4(path))
930
+ return false;
931
+ rmSync(path);
932
+ return true;
933
+ }
934
+ function listKeys() {
935
+ const d = keysDir();
936
+ try {
937
+ return readdirSync(d).filter((f) => f.endsWith(".key")).map((f) => f.replace(/\.key$/, "")).sort();
938
+ } catch {
939
+ return [];
940
+ }
941
+ }
942
+ function keysPath() {
943
+ return keysDir();
944
+ }
945
+
946
+ // src/commands/providers.ts
947
+ function maskKey(raw) {
948
+ if (!raw)
949
+ return "";
950
+ if (raw.length <= 8)
951
+ return "***" + raw.slice(-2);
952
+ return raw.slice(0, 4) + "\u2022\u2022\u2022\u2022\u2022" + raw.slice(-4);
953
+ }
954
+ function pad(s, w) {
955
+ return s.length >= w ? s : s + " ".repeat(w - s.length);
956
+ }
957
+ async function readStdinLine(promptText) {
958
+ process.stdout.write(palette.prompt(promptText));
959
+ return new Promise((resolve) => {
960
+ const chunks = [];
961
+ const onData = (b) => {
962
+ chunks.push(b);
963
+ const buf = Buffer.concat(chunks).toString("utf8");
964
+ const nl2 = buf.indexOf("\n");
965
+ if (nl2 !== -1) {
966
+ process.stdin.off("data", onData);
967
+ try {
968
+ process.stdin.pause();
969
+ } catch {
970
+ }
971
+ resolve(buf.slice(0, nl2).replace(/\r$/, ""));
972
+ }
973
+ };
974
+ process.stdin.on("data", onData);
975
+ try {
976
+ process.stdin.resume();
977
+ } catch {
978
+ }
979
+ });
980
+ }
981
+ async function cmdProvidersList() {
982
+ const catalog = await loadCatalog();
983
+ const keyOwners = new Set(listKeys());
984
+ const { bold, dim, success, warn } = palette;
985
+ process.stdout.write(bold("\n Available providers\n\n"));
986
+ process.stdout.write(
987
+ dim(
988
+ " " + pad("ID", 12) + pad("NAME", 24) + pad("KEY", 12) + pad("DEFAULT MODEL", 28) + "BASE URL\n"
989
+ )
990
+ );
991
+ for (const p of catalog) {
992
+ const keyState = !p.needsKey ? success("not needed") : keyOwners.has(p.id) ? success("set") : warn("missing");
993
+ process.stdout.write(
994
+ " " + pad(p.id, 12) + pad(p.display, 24) + pad(keyState, 12) + pad(p.defaultModel || "-", 28) + (p.baseUrl || "-") + "\n"
995
+ );
996
+ }
997
+ process.stdout.write("\n");
998
+ return 0;
999
+ }
1000
+ async function cmdProvidersSet(providerId) {
1001
+ const catalog = await loadCatalog();
1002
+ const meta = catalog.find((p) => p.id === providerId);
1003
+ if (!meta) {
1004
+ process.stderr.write(palette.error(`unknown provider: ${providerId}
1005
+ `));
1006
+ return 2;
1007
+ }
1008
+ if (!meta.needsKey) {
1009
+ process.stdout.write(`${meta.display} does not require an API key.
1010
+ `);
1011
+ return 0;
1012
+ }
1013
+ const value = await readStdinLine(`paste ${meta.display} API key: `);
1014
+ if (!value) {
1015
+ process.stderr.write(palette.error("empty key \u2014 aborted\n"));
1016
+ return 1;
1017
+ }
1018
+ const path = setKey(providerId, value);
1019
+ process.stdout.write(palette.success(`saved ${providerId} key \u2192 ${path}
1020
+ `));
1021
+ return 0;
1022
+ }
1023
+ async function cmdProvidersRemove(providerId) {
1024
+ const ok = removeKey(providerId);
1025
+ if (!ok) {
1026
+ process.stdout.write(`no key stored for ${providerId}
1027
+ `);
1028
+ return 0;
1029
+ }
1030
+ process.stdout.write(palette.success(`removed ${providerId} key
1031
+ `));
1032
+ return 0;
1033
+ }
1034
+ async function cmdProvidersTest(providerId) {
1035
+ const catalog = await loadCatalog();
1036
+ const meta = catalog.find((p) => p.id === providerId);
1037
+ if (!meta) {
1038
+ process.stderr.write(palette.error(`unknown provider: ${providerId}
1039
+ `));
1040
+ return 2;
1041
+ }
1042
+ const key = getKey(providerId);
1043
+ if (meta.needsKey && !key) {
1044
+ process.stderr.write(palette.error(`no key stored \u2014 run: rscot providers set ${providerId}
1045
+ `));
1046
+ return 1;
1047
+ }
1048
+ process.stdout.write(`probing ${meta.display}\u2026
1049
+ `);
1050
+ const t0 = Date.now();
1051
+ try {
1052
+ const result = await probe(meta, key);
1053
+ const dt = Date.now() - t0;
1054
+ if (result.ok) {
1055
+ process.stdout.write(
1056
+ palette.success(`ok \xB7 ${dt}ms \xB7 ${result.modelCount} models reported
1057
+ `)
1058
+ );
1059
+ return 0;
1060
+ }
1061
+ process.stderr.write(palette.error(`probe failed: ${result.detail}
1062
+ `));
1063
+ return 1;
1064
+ } catch (err) {
1065
+ process.stderr.write(palette.error(`probe error: ${err?.message || String(err)}
1066
+ `));
1067
+ return 1;
1068
+ }
1069
+ }
1070
+ async function probe(provider, key) {
1071
+ if (provider.id === "anthropic") {
1072
+ const url2 = provider.baseUrl.replace(/\/+$/, "") + "/messages";
1073
+ const res2 = await fetch(url2, {
1074
+ method: "POST",
1075
+ headers: {
1076
+ "x-api-key": key,
1077
+ "anthropic-version": "2023-06-01",
1078
+ "content-type": "application/json"
1079
+ },
1080
+ body: JSON.stringify({
1081
+ model: provider.defaultModel,
1082
+ max_tokens: 1,
1083
+ messages: [{ role: "user", content: "hi" }]
1084
+ })
1085
+ });
1086
+ return { ok: res2.ok, modelCount: res2.ok ? 1 : 0, detail: `HTTP ${res2.status}` };
1087
+ }
1088
+ if (provider.id === "google") {
1089
+ const url2 = provider.baseUrl.replace(/\/+$/, "") + `/models?key=${encodeURIComponent(key)}`;
1090
+ const res2 = await fetch(url2);
1091
+ if (!res2.ok)
1092
+ return { ok: false, modelCount: 0, detail: `HTTP ${res2.status}` };
1093
+ const body2 = await res2.json();
1094
+ const count2 = Array.isArray(body2?.models) ? body2.models.length : 0;
1095
+ return { ok: true, modelCount: count2, detail: "ok" };
1096
+ }
1097
+ const headers = key ? { Authorization: `Bearer ${key}` } : {};
1098
+ const url = provider.baseUrl.replace(/\/+$/, "") + "/models";
1099
+ const res = await fetch(url, { headers });
1100
+ if (!res.ok)
1101
+ return { ok: false, modelCount: 0, detail: `HTTP ${res.status}` };
1102
+ const body = await res.json();
1103
+ const count = Array.isArray(body?.data) ? body.data.length : Array.isArray(body?.models) ? body.models.length : 0;
1104
+ return { ok: true, modelCount: count, detail: "ok" };
1105
+ }
1106
+ async function cmdModelsList(providerId) {
1107
+ const catalog = await loadCatalog();
1108
+ const { bold, dim } = palette;
1109
+ const targets = providerId ? catalog.filter((p) => p.id === providerId) : catalog;
1110
+ if (providerId && targets.length === 0) {
1111
+ process.stderr.write(palette.error(`unknown provider: ${providerId}
1112
+ `));
1113
+ return 2;
1114
+ }
1115
+ for (const p of targets) {
1116
+ process.stdout.write(bold(`
1117
+ ${p.display}
1118
+ `));
1119
+ if (p.models.length === 0) {
1120
+ process.stdout.write(dim(" (dynamic \u2014 pulled at runtime)\n"));
1121
+ continue;
1122
+ }
1123
+ for (const m of p.models) {
1124
+ const price = m.priceIn != null || m.priceOut != null ? ` $${m.priceIn ?? "?"}/$${m.priceOut ?? "?"}/M` : "";
1125
+ process.stdout.write(` ${pad(m.id, 36)}${price} ${dim(m.note || "")}
1126
+ `);
1127
+ }
1128
+ }
1129
+ process.stdout.write("\n");
1130
+ return 0;
1131
+ }
1132
+ async function cmdModelsSearch(query) {
1133
+ const catalog = await loadCatalog();
1134
+ const q = query.toLowerCase();
1135
+ const { bold } = palette;
1136
+ let hits = 0;
1137
+ for (const p of catalog) {
1138
+ const matches = p.models.filter(
1139
+ (m) => m.id.toLowerCase().includes(q) || m.label.toLowerCase().includes(q)
1140
+ );
1141
+ if (matches.length === 0)
1142
+ continue;
1143
+ process.stdout.write(bold(`
1144
+ ${p.display}
1145
+ `));
1146
+ for (const m of matches) {
1147
+ process.stdout.write(` ${pad(m.id, 36)} ${m.note || ""}
1148
+ `);
1149
+ hits++;
1150
+ }
1151
+ }
1152
+ if (hits === 0)
1153
+ process.stdout.write("(no models matched)\n");
1154
+ process.stdout.write("\n");
1155
+ return 0;
1156
+ }
1157
+ function cmdKeysList() {
1158
+ const keys = listKeys();
1159
+ if (keys.length === 0) {
1160
+ process.stdout.write("no keys stored.\n");
1161
+ return 0;
1162
+ }
1163
+ for (const id of keys) {
1164
+ const masked = maskKey(getKey(id));
1165
+ process.stdout.write(` ${pad(id, 14)} ${masked}
1166
+ `);
1167
+ }
1168
+ return 0;
1169
+ }
1170
+ async function cmdKeysSet(providerId) {
1171
+ return cmdProvidersSet(providerId);
1172
+ }
1173
+ async function cmdKeysRemove(providerId) {
1174
+ return cmdProvidersRemove(providerId);
1175
+ }
1176
+
1177
+ // src/auth.ts
1178
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, rmSync as rmSync2, chmodSync as chmodSync3 } from "node:fs";
1179
+ import { join as join5 } from "node:path";
1180
+ import * as crypto from "node:crypto";
1181
+ import * as http from "node:http";
1182
+ import { spawn } from "node:child_process";
1183
+ var AUTH_BASE = "https://auth.learningfind.com";
1184
+ var PROFILE_URL = "https://learningfind.com/api/me/profile";
1185
+ var PORT_LO = 49152;
1186
+ var PORT_HI = 65535;
1187
+ var BIND_ATTEMPTS = 32;
1188
+ var LOOPBACK_TIMEOUT_MS = 18e4;
1189
+ var ACCESS_LEEWAY_S = 30;
1190
+ function authDir() {
1191
+ const d = configRoot();
1192
+ if (!existsSync5(d))
1193
+ mkdirSync5(d, { recursive: true, mode: 448 });
1194
+ return d;
1195
+ }
1196
+ function pathAccess() {
1197
+ return join5(authDir(), "access");
1198
+ }
1199
+ function pathRefresh() {
1200
+ return join5(authDir(), "refresh");
1201
+ }
1202
+ function pathAccessExp() {
1203
+ return join5(authDir(), "access.exp");
1204
+ }
1205
+ function pathDevice() {
1206
+ return join5(authDir(), "device_id");
1207
+ }
1208
+ function pathProfile() {
1209
+ return join5(authDir(), "profile.json");
1210
+ }
1211
+ function pathLocalMode() {
1212
+ return join5(authDir(), "local-mode");
1213
+ }
1214
+ function writeSecret(path, value) {
1215
+ writeFileSync4(path, value, { mode: 384 });
1216
+ try {
1217
+ chmodSync3(path, 384);
1218
+ } catch {
1219
+ }
1220
+ }
1221
+ function readMaybe(path) {
1222
+ if (!existsSync5(path))
1223
+ return null;
1224
+ try {
1225
+ return readFileSync4(path, "utf8").trim();
1226
+ } catch {
1227
+ return null;
1228
+ }
1229
+ }
1230
+ function rmIf(path) {
1231
+ if (existsSync5(path))
1232
+ try {
1233
+ rmSync2(path);
1234
+ } catch {
1235
+ }
1236
+ }
1237
+ function b64url(buf) {
1238
+ return buf.toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
1239
+ }
1240
+ function genVerifier() {
1241
+ return b64url(crypto.randomBytes(48));
1242
+ }
1243
+ function challengeS256(v) {
1244
+ return b64url(crypto.createHash("sha256").update(v).digest());
1245
+ }
1246
+ function genState() {
1247
+ return b64url(crypto.randomBytes(32));
1248
+ }
1249
+ function parseJwtExp(jwt) {
1250
+ const parts = jwt.split(".");
1251
+ if (parts.length < 2)
1252
+ return null;
1253
+ try {
1254
+ const payload = Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
1255
+ const c = JSON.parse(payload);
1256
+ return typeof c.exp === "number" ? c.exp : null;
1257
+ } catch {
1258
+ return null;
1259
+ }
1260
+ }
1261
+ function getOrCreateDeviceId() {
1262
+ const existing = readMaybe(pathDevice());
1263
+ if (existing)
1264
+ return existing;
1265
+ const id = "cli-" + b64url(crypto.randomBytes(12));
1266
+ writeSecret(pathDevice(), id);
1267
+ return id;
1268
+ }
1269
+ function isLocalMode() {
1270
+ return existsSync5(pathLocalMode());
1271
+ }
1272
+ function setLocalMode(on) {
1273
+ if (on)
1274
+ writeSecret(pathLocalMode(), (/* @__PURE__ */ new Date()).toISOString());
1275
+ else
1276
+ rmIf(pathLocalMode());
1277
+ }
1278
+ function getStoredProfile() {
1279
+ const raw = readMaybe(pathProfile());
1280
+ if (!raw)
1281
+ return null;
1282
+ try {
1283
+ return JSON.parse(raw);
1284
+ } catch {
1285
+ return null;
1286
+ }
1287
+ }
1288
+ function storeProfile(p) {
1289
+ if (!p)
1290
+ return;
1291
+ writeSecret(pathProfile(), JSON.stringify(p, null, 2));
1292
+ }
1293
+ function storeTokens(t) {
1294
+ writeSecret(pathAccess(), t.access);
1295
+ writeSecret(pathRefresh(), t.refresh);
1296
+ const exp = parseJwtExp(t.access);
1297
+ if (exp)
1298
+ writeSecret(pathAccessExp(), String(exp));
1299
+ if (t.profile)
1300
+ storeProfile(t.profile);
1301
+ }
1302
+ function clearTokens() {
1303
+ rmIf(pathAccess());
1304
+ rmIf(pathRefresh());
1305
+ rmIf(pathAccessExp());
1306
+ rmIf(pathProfile());
1307
+ }
1308
+ function hasRefresh() {
1309
+ const r = readMaybe(pathRefresh());
1310
+ return !!(r && r.length > 0);
1311
+ }
1312
+ async function refreshTokens(refresh) {
1313
+ const deviceId = getOrCreateDeviceId();
1314
+ const res = await fetch(`${AUTH_BASE}/auth/desktop/refresh`, {
1315
+ method: "POST",
1316
+ headers: { "Content-Type": "application/json" },
1317
+ body: JSON.stringify({ refresh, device_id: deviceId })
1318
+ });
1319
+ if (!res.ok)
1320
+ throw new Error(`refresh failed: ${res.status}`);
1321
+ return await res.json();
1322
+ }
1323
+ async function getAccessToken() {
1324
+ const access = readMaybe(pathAccess());
1325
+ const exp = Number(readMaybe(pathAccessExp()) || "0");
1326
+ const now = Math.floor(Date.now() / 1e3);
1327
+ if (access && exp > now + ACCESS_LEEWAY_S)
1328
+ return access;
1329
+ const refresh = readMaybe(pathRefresh());
1330
+ if (!refresh)
1331
+ return null;
1332
+ try {
1333
+ const t = await refreshTokens(refresh);
1334
+ storeTokens(t);
1335
+ return t.access;
1336
+ } catch {
1337
+ return null;
1338
+ }
1339
+ }
1340
+ function bindLoopback() {
1341
+ return new Promise((resolve, reject) => {
1342
+ let attempts = 0;
1343
+ const tryBind = () => {
1344
+ if (attempts++ >= BIND_ATTEMPTS) {
1345
+ reject(new Error("loopback port exhausted (49152..65535)"));
1346
+ return;
1347
+ }
1348
+ const port = PORT_LO + Math.floor(Math.random() * (PORT_HI - PORT_LO + 1));
1349
+ const server = http.createServer();
1350
+ server.once("error", () => {
1351
+ try {
1352
+ server.close();
1353
+ } catch {
1354
+ }
1355
+ tryBind();
1356
+ });
1357
+ server.listen(port, "127.0.0.1", () => {
1358
+ const addr = server.address();
1359
+ server.removeAllListeners("error");
1360
+ resolve({ server, port: addr.port });
1361
+ });
1362
+ };
1363
+ tryBind();
1364
+ });
1365
+ }
1366
+ var SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Rscot Agent</title></head><body style="font-family:system-ui,sans-serif;padding:40px;color:#222"><h2>Sign-in complete</h2><p>You can close this window and return to your terminal.</p><script>setTimeout(function(){window.close();},250);</script></body></html>`;
1367
+ function waitForCallback(server, expectedState) {
1368
+ return new Promise((resolve, reject) => {
1369
+ const timer = setTimeout(() => {
1370
+ try {
1371
+ server.close();
1372
+ } catch {
1373
+ }
1374
+ reject(new Error("Sign-in timed out after 3 minutes."));
1375
+ }, LOOPBACK_TIMEOUT_MS);
1376
+ server.on("request", (req, res) => {
1377
+ const url = new URL(req.url || "/", "http://127.0.0.1");
1378
+ if (url.pathname !== "/cb") {
1379
+ res.statusCode = 404;
1380
+ res.end("not found");
1381
+ return;
1382
+ }
1383
+ const code = url.searchParams.get("code");
1384
+ const state = url.searchParams.get("state");
1385
+ const err = url.searchParams.get("error");
1386
+ res.statusCode = 200;
1387
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1388
+ res.end(SUCCESS_HTML);
1389
+ clearTimeout(timer);
1390
+ try {
1391
+ server.close();
1392
+ } catch {
1393
+ }
1394
+ if (err)
1395
+ return reject(new Error(`Auth provider error: ${err}`));
1396
+ if (!code || !state)
1397
+ return reject(new Error("Auth callback missing code/state."));
1398
+ if (state !== expectedState)
1399
+ return reject(new Error("Auth callback state mismatch."));
1400
+ resolve(code);
1401
+ });
1402
+ });
1403
+ }
1404
+ async function exchangeCode(code, verifier, deviceId) {
1405
+ const res = await fetch(`${AUTH_BASE}/auth/desktop/token`, {
1406
+ method: "POST",
1407
+ headers: { "Content-Type": "application/json" },
1408
+ body: JSON.stringify({ code, code_verifier: verifier, device_id: deviceId })
1409
+ });
1410
+ if (!res.ok) {
1411
+ let detail = "";
1412
+ try {
1413
+ detail = (await res.json())?.detail || "";
1414
+ } catch {
1415
+ detail = await res.text();
1416
+ }
1417
+ throw new Error(`Token exchange failed (${res.status}): ${detail || res.statusText}`);
1418
+ }
1419
+ return await res.json();
1420
+ }
1421
+ function openBrowser(url) {
1422
+ const platform = process.platform;
1423
+ const cmd = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
1424
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
1425
+ try {
1426
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
1427
+ child.unref();
1428
+ } catch {
1429
+ }
1430
+ }
1431
+ async function signIn() {
1432
+ const deviceId = getOrCreateDeviceId();
1433
+ const verifier = genVerifier();
1434
+ const challenge = challengeS256(verifier);
1435
+ const state = genState();
1436
+ const { server, port } = await bindLoopback();
1437
+ const params = new URLSearchParams({
1438
+ port: String(port),
1439
+ state,
1440
+ code_challenge: challenge,
1441
+ code_challenge_method: "S256",
1442
+ device_id: deviceId,
1443
+ client: "cli"
1444
+ });
1445
+ const authUrl = `${AUTH_BASE}/auth/desktop/start?${params.toString()}`;
1446
+ process.stdout.write(`
1447
+ Opening browser to: ${authUrl}
1448
+ `);
1449
+ process.stdout.write("If the browser does not open, paste that URL manually.\n");
1450
+ process.stdout.write(`Waiting for callback on http://127.0.0.1:${port}/cb (up to 3 minutes)\u2026
1451
+
1452
+ `);
1453
+ openBrowser(authUrl);
1454
+ const code = await waitForCallback(server, state);
1455
+ const bundle = await exchangeCode(code, verifier, deviceId);
1456
+ storeTokens(bundle);
1457
+ setLocalMode(false);
1458
+ if (!bundle.profile) {
1459
+ const p = await fetchProfile(bundle.access);
1460
+ if (p)
1461
+ storeProfile(p);
1462
+ return p;
1463
+ }
1464
+ return bundle.profile;
1465
+ }
1466
+ async function signOut() {
1467
+ const refresh = readMaybe(pathRefresh());
1468
+ if (refresh) {
1469
+ const deviceId = getOrCreateDeviceId();
1470
+ try {
1471
+ await fetch(`${AUTH_BASE}/auth/desktop/session`, {
1472
+ method: "DELETE",
1473
+ headers: { "Content-Type": "application/json" },
1474
+ body: JSON.stringify({ refresh, device_id: deviceId })
1475
+ });
1476
+ } catch {
1477
+ }
1478
+ }
1479
+ clearTokens();
1480
+ }
1481
+ async function fetchProfile(access) {
1482
+ try {
1483
+ const res = await fetch(PROFILE_URL, { headers: { "Authorization": `Bearer ${access}` } });
1484
+ if (!res.ok)
1485
+ return null;
1486
+ return await res.json();
1487
+ } catch {
1488
+ return null;
1489
+ }
1490
+ }
1491
+ async function refreshProfile() {
1492
+ const t = await getAccessToken();
1493
+ if (!t)
1494
+ return null;
1495
+ const p = await fetchProfile(t);
1496
+ if (p)
1497
+ storeProfile(p);
1498
+ return p;
1499
+ }
1500
+
1501
+ // src/index.ts
1502
+ if (process.stdout.isTTY || process.env.FORCE_COLOR) {
1503
+ kleur2.enabled = true;
1504
+ }
1505
+ var VERSION2 = "1.2.0";
1506
+ var AUTH_FREE_COMMANDS = /* @__PURE__ */ new Set([
1507
+ "login",
1508
+ "logout",
1509
+ "whoami",
1510
+ "version",
1511
+ "help"
1512
+ ]);
1513
+ async function cmdLogin(local) {
1514
+ if (local) {
1515
+ setLocalMode(true);
1516
+ process.stdout.write(
1517
+ palette.success("\u2713") + " Local mode enabled. Future commands skip sign-in.\n"
1518
+ );
1519
+ process.stdout.write(palette.dim(" Run `rscot login` to switch back to a cloud account.\n"));
1520
+ return 0;
1521
+ }
1522
+ try {
1523
+ const profile = await signIn();
1524
+ if (!profile) {
1525
+ process.stdout.write(palette.warn("!") + " Sign-in completed but no profile returned.\n");
1526
+ return 0;
1527
+ }
1528
+ process.stdout.write(
1529
+ palette.success("\u2713") + " Signed in as " + palette.bold(profile.email) + palette.dim(" \xB7 tier ") + palette.cyan(profile.tier || "free") + "\n"
1530
+ );
1531
+ return 0;
1532
+ } catch (err) {
1533
+ process.stderr.write(
1534
+ palette.error("\u2717") + " Sign-in failed: " + (err instanceof Error ? err.message : String(err)) + "\n"
1535
+ );
1536
+ return 1;
1537
+ }
1538
+ }
1539
+ async function cmdLogout() {
1540
+ if (isLocalMode()) {
1541
+ setLocalMode(false);
1542
+ process.stdout.write(palette.success("\u2713") + " Local mode disabled.\n");
1543
+ }
1544
+ if (!hasRefresh()) {
1545
+ process.stdout.write(palette.dim(" No active session to sign out.\n"));
1546
+ clearTokens();
1547
+ return 0;
1548
+ }
1549
+ await signOut();
1550
+ process.stdout.write(palette.success("\u2713") + " Signed out.\n");
1551
+ return 0;
1552
+ }
1553
+ async function cmdWhoami() {
1554
+ if (isLocalMode()) {
1555
+ process.stdout.write(
1556
+ palette.info("\u203A") + " Local mode (no cloud account). Run `rscot login` to switch.\n"
1557
+ );
1558
+ return 0;
1559
+ }
1560
+ if (!hasRefresh()) {
1561
+ process.stdout.write(
1562
+ palette.warn("!") + " Not signed in. Run `rscot login` (or `rscot login --local`).\n"
1563
+ );
1564
+ return 1;
1565
+ }
1566
+ const fresh = await refreshProfile();
1567
+ const profile = fresh || getStoredProfile();
1568
+ if (!profile) {
1569
+ process.stdout.write(palette.warn("!") + " Signed in but profile unavailable. Try `rscot login` again.\n");
1570
+ return 1;
1571
+ }
1572
+ process.stdout.write(" " + palette.dim("email ") + palette.bold(profile.email) + "\n");
1573
+ process.stdout.write(" " + palette.dim("tier ") + palette.cyan(profile.tier || "free") + "\n");
1574
+ if (typeof profile.quota_used === "number") {
1575
+ process.stdout.write(" " + palette.dim("quota ") + palette.cyan(String(profile.quota_used)) + "\n");
1576
+ }
1577
+ if (profile.user_id) {
1578
+ process.stdout.write(" " + palette.dim("user_id ") + palette.cyan(profile.user_id) + "\n");
1579
+ }
1580
+ return 0;
1581
+ }
1582
+ function requireAuth(head) {
1583
+ if (AUTH_FREE_COMMANDS.has(head))
1584
+ return true;
1585
+ if (isLocalMode())
1586
+ return true;
1587
+ if (hasRefresh())
1588
+ return true;
1589
+ return false;
1590
+ }
1591
+ function printAuthGateMessage() {
1592
+ process.stderr.write(
1593
+ palette.error("\u2717") + " Sign in first: " + palette.bold("rscot login") + palette.dim(" (or `rscot login --local` for offline-only mode)") + "\n"
1594
+ );
1595
+ }
1596
+ function printHelp() {
1597
+ process.stdout.write(
1598
+ [
1599
+ palette.bold("rscot") + " " + palette.dim("Rscot Agent \u2014 npx onramp"),
1600
+ "",
1601
+ " " + palette.accent("USAGE"),
1602
+ " npx rscot run the interactive onboarding flow",
1603
+ " npx rscot --version print version",
1604
+ " npx rscot --help print this help",
1605
+ " npx rscot --no-prompt run env probe only (CI-safe smoke)",
1606
+ " npx rscot --local bypass auth gate for one command",
1607
+ "",
1608
+ " " + palette.accent("ACCOUNT"),
1609
+ " npx rscot login sign in via browser (loopback PKCE)",
1610
+ " npx rscot login --local skip cloud sign-in, mark local mode",
1611
+ " npx rscot logout revoke session + clear stored tokens",
1612
+ " npx rscot whoami show signed-in profile / tier / quota",
1613
+ "",
1614
+ " " + palette.accent("PROVIDERS"),
1615
+ " npx rscot providers list list all providers and key status",
1616
+ " npx rscot providers set <id> paste an API key for a provider",
1617
+ " npx rscot providers test <id> probe the provider with stored key",
1618
+ " npx rscot providers remove <id> delete the stored key",
1619
+ "",
1620
+ " " + palette.accent("MODELS"),
1621
+ " npx rscot models list list every catalog model",
1622
+ " npx rscot models list --provider <id> filter by provider",
1623
+ " npx rscot models search <substr> cross-provider model lookup",
1624
+ "",
1625
+ " " + palette.accent("KEYS / CONFIG"),
1626
+ " npx rscot keys list show which providers have keys",
1627
+ " npx rscot keys set <id> alias for `providers set`",
1628
+ " npx rscot keys remove <id> alias for `providers remove`",
1629
+ " npx rscot keys path print keychain dir",
1630
+ " npx rscot config path print config file path",
1631
+ "",
1632
+ " " + palette.accent("LICENSE"),
1633
+ " NonCommercial \xB7 source-available. See LICENSE at the repo root.",
1634
+ ""
1635
+ ].join("\n") + "\n"
1636
+ );
1637
+ }
1638
+ function indexOfPositional(argv) {
1639
+ for (let i = 0; i < argv.length; i++) {
1640
+ if (!argv[i].startsWith("-"))
1641
+ return i;
1642
+ }
1643
+ return -1;
1644
+ }
1645
+ async function dispatch(argv) {
1646
+ const head = argv[0];
1647
+ if (head === "login") {
1648
+ return cmdLogin(argv.includes("--local"));
1649
+ }
1650
+ if (head === "logout") {
1651
+ return cmdLogout();
1652
+ }
1653
+ if (head === "whoami") {
1654
+ return cmdWhoami();
1655
+ }
1656
+ if (head === "providers" || head === "models" || head === "keys" || head === "config") {
1657
+ const sub = argv[1];
1658
+ if (head === "providers") {
1659
+ if (sub === "list")
1660
+ return cmdProvidersList();
1661
+ if (sub === "set" && argv[2])
1662
+ return cmdProvidersSet(argv[2]);
1663
+ if (sub === "test" && argv[2])
1664
+ return cmdProvidersTest(argv[2]);
1665
+ if (sub === "remove" && argv[2])
1666
+ return cmdProvidersRemove(argv[2]);
1667
+ process.stderr.write(palette.error("usage: rscot providers <list|set|test|remove> [id]\n"));
1668
+ return 2;
1669
+ }
1670
+ if (head === "models") {
1671
+ if (sub === "list") {
1672
+ const provFlag = argv.indexOf("--provider");
1673
+ const provId = provFlag >= 0 ? argv[provFlag + 1] : void 0;
1674
+ return cmdModelsList(provId);
1675
+ }
1676
+ if (sub === "search" && argv[2])
1677
+ return cmdModelsSearch(argv[2]);
1678
+ process.stderr.write(palette.error("usage: rscot models <list [--provider id] | search <substr>>\n"));
1679
+ return 2;
1680
+ }
1681
+ if (head === "keys") {
1682
+ if (sub === "list")
1683
+ return cmdKeysList();
1684
+ if (sub === "set" && argv[2])
1685
+ return cmdKeysSet(argv[2]);
1686
+ if (sub === "remove" && argv[2])
1687
+ return cmdKeysRemove(argv[2]);
1688
+ if (sub === "path") {
1689
+ process.stdout.write(keysPath() + "\n");
1690
+ return 0;
1691
+ }
1692
+ process.stderr.write(palette.error("usage: rscot keys <list|set|remove|path> [id]\n"));
1693
+ return 2;
1694
+ }
1695
+ if (head === "config") {
1696
+ if (sub === "path") {
1697
+ process.stdout.write(configPath() + "\n");
1698
+ return 0;
1699
+ }
1700
+ process.stderr.write(palette.error("usage: rscot config <path>\n"));
1701
+ return 2;
1702
+ }
1703
+ }
1704
+ return -1;
1705
+ }
1706
+ async function main() {
1707
+ const argv = process.argv.slice(2);
1708
+ if (argv.includes("--version") || argv.includes("-v")) {
1709
+ process.stdout.write(VERSION2 + "\n");
1710
+ return;
1711
+ }
1712
+ if (argv.includes("--help") || argv.includes("-h")) {
1713
+ printHelp();
1714
+ return;
1715
+ }
1716
+ const positional = indexOfPositional(argv);
1717
+ const head = positional !== -1 ? argv[positional] : "";
1718
+ const localFlag = argv.includes("--local");
1719
+ if (!localFlag && !requireAuth(head)) {
1720
+ printAuthGateMessage();
1721
+ process.exit(1);
1722
+ return;
1723
+ }
1724
+ if (positional !== -1) {
1725
+ const rc = await dispatch(argv.slice(positional));
1726
+ if (rc !== -1) {
1727
+ process.exit(rc);
1728
+ return;
1729
+ }
1730
+ }
1731
+ const code = await runInstall({
1732
+ noninteractive: argv.includes("--no-prompt") || !process.stdin.isTTY
1733
+ });
1734
+ process.exit(code);
1735
+ }
1736
+ main().catch((err) => {
1737
+ process.stderr.write(
1738
+ palette.error("fatal: " + (err instanceof Error ? err.stack ?? err.message : String(err))) + "\n"
1739
+ );
1740
+ process.exit(1);
1741
+ });