gnosys 4.3.1 → 4.4.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.
@@ -0,0 +1,679 @@
1
+ /**
2
+ * Gnosys Interactive Setup Wizard.
3
+ *
4
+ * Guides users through provider selection, model tier, API key storage,
5
+ * IDE integration, and optional web knowledge base configuration.
6
+ *
7
+ * Uses Node.js built-in readline/promises — no external dependencies.
8
+ */
9
+ import { createInterface } from "readline/promises";
10
+ import { stdin, stdout } from "process";
11
+ import fs from "fs/promises";
12
+ import fsSync from "fs";
13
+ import path from "path";
14
+ import os from "os";
15
+ import { execSync } from "child_process";
16
+ // ─── ANSI Colors ────────────────────────────────────────────────────────────
17
+ const BOLD = "\x1b[1m";
18
+ const DIM = "\x1b[2m";
19
+ const GREEN = "\x1b[32m";
20
+ const CYAN = "\x1b[36m";
21
+ const YELLOW = "\x1b[33m";
22
+ const RED = "\x1b[31m";
23
+ const RESET = "\x1b[0m";
24
+ const CHECK = `${GREEN}\u2713${RESET}`;
25
+ const WARN = `${YELLOW}\u26A0${RESET}`;
26
+ const CROSS = `${RED}\u2717${RESET}`;
27
+ // ─── Version (read from package.json at runtime) ───────────────────────────
28
+ function getVersion() {
29
+ try {
30
+ const pkgPath = path.resolve(new URL(".", import.meta.url).pathname, "../../package.json");
31
+ const pkg = JSON.parse(fsSync.readFileSync(pkgPath, "utf-8"));
32
+ return pkg.version ?? "0.0.0";
33
+ }
34
+ catch {
35
+ return "0.0.0";
36
+ }
37
+ }
38
+ // ─── Provider Tiers ─────────────────────────────────────────────────────────
39
+ export const PROVIDER_TIERS = {
40
+ anthropic: [
41
+ { name: "Budget", model: "claude-haiku-4-5", input: 0.80, output: 4.00, recommended: false },
42
+ { name: "Balanced", model: "claude-sonnet-4-6", input: 3.00, output: 15.00, recommended: true },
43
+ { name: "Premium", model: "claude-opus-4-6", input: 5.00, output: 25.00, recommended: false },
44
+ ],
45
+ openai: [
46
+ { name: "Nano", model: "gpt-5.4-nano", input: 0.20, output: 1.25, recommended: false },
47
+ { name: "Mini", model: "gpt-5.4-mini", input: 0.75, output: 4.50, recommended: true },
48
+ { name: "Standard", model: "gpt-5.4", input: 2.50, output: 15.00, recommended: false },
49
+ ],
50
+ groq: [
51
+ { name: "Small (8B)", model: "llama-3.1-8b-instant", input: 0.05, output: 0.08, recommended: false },
52
+ { name: "Large (70B)", model: "llama-3.3-70b-versatile", input: 0.59, output: 0.79, recommended: true },
53
+ ],
54
+ xai: [
55
+ { name: "Mini", model: "grok-3-mini", input: 0.10, output: 0.40, recommended: false },
56
+ { name: "Standard", model: "grok-4.0", input: 0.40, output: 1.60, recommended: false },
57
+ { name: "Flagship", model: "grok-4.20", input: 0.80, output: 3.20, recommended: true },
58
+ ],
59
+ mistral: [
60
+ { name: "Tiny", model: "mistral-tiny", input: 0.05, output: 0.05, recommended: false },
61
+ { name: "Small", model: "mistral-small-4", input: 0.20, output: 0.80, recommended: true },
62
+ { name: "Large", model: "mistral-large-latest", input: 2.00, output: 8.00, recommended: false },
63
+ ],
64
+ ollama: [
65
+ { name: "Llama 3.2 (default)", model: "llama3.2", input: 0, output: 0, recommended: true },
66
+ { name: "Mistral", model: "mistral", input: 0, output: 0, recommended: false },
67
+ { name: "Gemma 2", model: "gemma2", input: 0, output: 0, recommended: false },
68
+ ],
69
+ lmstudio: [
70
+ { name: "Default", model: "default", input: 0, output: 0, recommended: true },
71
+ ],
72
+ custom: [],
73
+ };
74
+ // ─── Provider display names and env var mapping ─────────────────────────────
75
+ const PROVIDER_DISPLAY = {
76
+ anthropic: "Anthropic (Claude)",
77
+ openai: "OpenAI (GPT-5.4)",
78
+ ollama: "Ollama (local, free)",
79
+ groq: "Groq (fast, cheap)",
80
+ xai: "xAI (Grok)",
81
+ mistral: "Mistral",
82
+ lmstudio: "LM Studio (local, free)",
83
+ custom: "Custom (any OpenAI-compatible API)",
84
+ };
85
+ const PROVIDER_ENV_VAR = {
86
+ anthropic: "ANTHROPIC_API_KEY",
87
+ openai: "OPENAI_API_KEY",
88
+ groq: "GROQ_API_KEY",
89
+ xai: "XAI_API_KEY",
90
+ mistral: "MISTRAL_API_KEY",
91
+ custom: "GNOSYS_LLM_API_KEY",
92
+ };
93
+ // Ordered list for the menu
94
+ const PROVIDER_ORDER = [
95
+ "anthropic",
96
+ "openai",
97
+ "ollama",
98
+ "groq",
99
+ "xai",
100
+ "mistral",
101
+ "lmstudio",
102
+ "custom",
103
+ ];
104
+ // ─── Exported Helpers ───────────────────────────────────────────────────────
105
+ /**
106
+ * Returns the cheapest capable model for structuring tasks.
107
+ * Structuring (keyword extraction, tagging) doesn't need a flagship model.
108
+ */
109
+ export function getStructuringModel(provider, chosenModel) {
110
+ switch (provider) {
111
+ case "anthropic":
112
+ return "claude-haiku-4-5";
113
+ case "openai":
114
+ return "gpt-5.4-nano";
115
+ default:
116
+ // groq, xai, mistral, ollama, lmstudio, custom — already cheap enough
117
+ return chosenModel;
118
+ }
119
+ }
120
+ /**
121
+ * Write an API key to ~/.config/gnosys/.env.
122
+ * Creates the directory and file if they don't exist.
123
+ * Replaces an existing key line if found, otherwise appends.
124
+ */
125
+ export async function writeApiKey(provider, key) {
126
+ const envVar = PROVIDER_ENV_VAR[provider];
127
+ if (!envVar)
128
+ return;
129
+ const configDir = path.join(os.homedir(), ".config", "gnosys");
130
+ await fs.mkdir(configDir, { recursive: true });
131
+ const envPath = path.join(configDir, ".env");
132
+ let lines = [];
133
+ try {
134
+ const existing = await fs.readFile(envPath, "utf-8");
135
+ lines = existing.split("\n");
136
+ }
137
+ catch {
138
+ // File doesn't exist yet — start fresh
139
+ }
140
+ // Check if this env var already exists and replace it
141
+ let found = false;
142
+ for (let i = 0; i < lines.length; i++) {
143
+ if (lines[i].startsWith(`${envVar}=`)) {
144
+ lines[i] = `${envVar}=${key}`;
145
+ found = true;
146
+ break;
147
+ }
148
+ }
149
+ if (!found) {
150
+ // Remove trailing empty lines before appending
151
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
152
+ lines.pop();
153
+ }
154
+ lines.push(`${envVar}=${key}`);
155
+ }
156
+ await fs.writeFile(envPath, lines.join("\n") + "\n", "utf-8");
157
+ }
158
+ /**
159
+ * Detect which IDEs are available in the given project directory.
160
+ * Returns an array like ["claude", "cursor", "codex"].
161
+ */
162
+ export async function detectIDEs(projectDir) {
163
+ const detected = [];
164
+ // Check for Claude CLI
165
+ try {
166
+ execSync("which claude", { stdio: "ignore" });
167
+ detected.push("claude");
168
+ }
169
+ catch {
170
+ // Not installed
171
+ }
172
+ // Check for .cursor/ directory
173
+ try {
174
+ const stat = await fs.stat(path.join(projectDir, ".cursor"));
175
+ if (stat.isDirectory())
176
+ detected.push("cursor");
177
+ }
178
+ catch {
179
+ // Not present
180
+ }
181
+ // Check for .codex/ directory
182
+ try {
183
+ const stat = await fs.stat(path.join(projectDir, ".codex"));
184
+ if (stat.isDirectory())
185
+ detected.push("codex");
186
+ }
187
+ catch {
188
+ // Not present
189
+ }
190
+ return detected;
191
+ }
192
+ /**
193
+ * Set up Gnosys MCP integration for a specific IDE.
194
+ */
195
+ export async function setupIDE(ide, projectDir) {
196
+ try {
197
+ switch (ide) {
198
+ case "claude": {
199
+ execSync("claude mcp add -s user gnosys -- gnosys serve", {
200
+ stdio: "pipe",
201
+ });
202
+ return { success: true, message: "Claude Code MCP server registered" };
203
+ }
204
+ case "cursor": {
205
+ const cursorDir = path.join(projectDir, ".cursor");
206
+ const mcpPath = path.join(cursorDir, "mcp.json");
207
+ await fs.mkdir(cursorDir, { recursive: true });
208
+ let config = {};
209
+ try {
210
+ const existing = await fs.readFile(mcpPath, "utf-8");
211
+ config = JSON.parse(existing);
212
+ }
213
+ catch {
214
+ // File doesn't exist or is invalid — start fresh
215
+ }
216
+ // Merge gnosys entry
217
+ const servers = (config.mcpServers ?? {});
218
+ servers.gnosys = { command: "gnosys", args: ["serve"] };
219
+ config.mcpServers = servers;
220
+ await fs.writeFile(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
221
+ return { success: true, message: "Cursor MCP config updated (.cursor/mcp.json)" };
222
+ }
223
+ case "codex": {
224
+ const codexDir = path.join(projectDir, ".codex");
225
+ const configPath = path.join(codexDir, "config.toml");
226
+ await fs.mkdir(codexDir, { recursive: true });
227
+ let content = "";
228
+ try {
229
+ content = await fs.readFile(configPath, "utf-8");
230
+ }
231
+ catch {
232
+ // File doesn't exist — start fresh
233
+ }
234
+ // Add gnosys section if not already present
235
+ if (!content.includes("[gnosys]")) {
236
+ if (content.length > 0 && !content.endsWith("\n")) {
237
+ content += "\n";
238
+ }
239
+ content += `\n[gnosys]\ncommand = "gnosys"\nargs = ["serve"]\n`;
240
+ await fs.writeFile(configPath, content, "utf-8");
241
+ }
242
+ return { success: true, message: "Codex config updated (.codex/config.toml)" };
243
+ }
244
+ default:
245
+ return { success: false, message: `Unknown IDE: ${ide}` };
246
+ }
247
+ }
248
+ catch (err) {
249
+ const msg = err instanceof Error ? err.message : String(err);
250
+ return { success: false, message: msg };
251
+ }
252
+ }
253
+ // ─── Internal Helpers ───────────────────────────────────────────────────────
254
+ /**
255
+ * Print a numbered list, read a choice, validate it, re-prompt on invalid input.
256
+ * Returns the 0-based index of the chosen option.
257
+ */
258
+ async function askChoice(rl, question, options) {
259
+ console.log();
260
+ console.log(question);
261
+ console.log();
262
+ for (let i = 0; i < options.length; i++) {
263
+ console.log(` ${BOLD}${i + 1}.${RESET} ${options[i]}`);
264
+ }
265
+ console.log();
266
+ while (true) {
267
+ const answer = await rl.question(`${DIM}>${RESET} `);
268
+ const num = parseInt(answer.trim(), 10);
269
+ if (num >= 1 && num <= options.length) {
270
+ return num - 1;
271
+ }
272
+ console.log(`${RED}Please enter a number between 1 and ${options.length}.${RESET}`);
273
+ }
274
+ }
275
+ /**
276
+ * Read a single line of input with an optional default value.
277
+ */
278
+ async function askInput(rl, prompt, opts) {
279
+ const suffix = opts?.default ? ` ${DIM}(${opts.default})${RESET}` : "";
280
+ const answer = await rl.question(`${prompt}${suffix}: `);
281
+ const trimmed = answer.trim();
282
+ return trimmed || opts?.default || "";
283
+ }
284
+ /**
285
+ * Y/n prompt. Returns true for yes.
286
+ */
287
+ async function askYesNo(rl, question, defaultYes = true) {
288
+ const hint = defaultYes ? "Y/n" : "y/N";
289
+ const answer = await rl.question(`${question} [${hint}] `);
290
+ const trimmed = answer.trim().toLowerCase();
291
+ if (trimmed === "")
292
+ return defaultYes;
293
+ return trimmed === "y" || trimmed === "yes";
294
+ }
295
+ /**
296
+ * Format a price for display: "$0.80" or "free".
297
+ */
298
+ function formatPrice(input, output) {
299
+ if (input === 0 && output === 0)
300
+ return "free";
301
+ return `$${input.toFixed(2)}\u2013$${output.toFixed(2)}/M tokens`;
302
+ }
303
+ /**
304
+ * Print a bordered box with a title and key-value rows.
305
+ */
306
+ function printBox(title, rows) {
307
+ const maxKeyLen = Math.max(...rows.map(([k]) => k.length));
308
+ const maxValLen = Math.max(...rows.map(([, v]) => v.length));
309
+ const contentWidth = Math.max(title.length, maxKeyLen + maxValLen + 2);
310
+ const innerWidth = contentWidth + 4; // 2 padding each side
311
+ const border = "\u2500".repeat(innerWidth);
312
+ console.log();
313
+ console.log(`\u250C${border}\u2510`);
314
+ console.log(`\u2502 ${BOLD}${title}${RESET}${" ".repeat(innerWidth - title.length - 2)}\u2502`);
315
+ console.log(`\u251C${border}\u2524`);
316
+ for (const [key, val] of rows) {
317
+ const line = `${key.padEnd(maxKeyLen)} ${val}`;
318
+ console.log(`\u2502 ${line}${" ".repeat(innerWidth - line.length - 2)}\u2502`);
319
+ }
320
+ console.log(`\u2514${border}\u2518`);
321
+ console.log();
322
+ }
323
+ /**
324
+ * Mask a key: show first 7 chars, replace the rest with dots.
325
+ */
326
+ function maskKey(key) {
327
+ if (key.length <= 7)
328
+ return key;
329
+ return key.slice(0, 7) + "\u2026" + "*".repeat(Math.min(key.length - 7, 12));
330
+ }
331
+ /**
332
+ * Read the central projects.json or DB to find registered projects.
333
+ */
334
+ async function getRegisteredProjects() {
335
+ const projects = [];
336
+ // Try central DB via dynamic import
337
+ try {
338
+ const { GnosysDB } = await import("./db.js");
339
+ const db = GnosysDB.openCentral();
340
+ const all = db.getAllProjects();
341
+ for (const p of all) {
342
+ projects.push({
343
+ name: p.name,
344
+ directory: p.working_directory,
345
+ id: p.id,
346
+ });
347
+ }
348
+ db.close();
349
+ }
350
+ catch {
351
+ // Central DB not available or module not built yet — that's okay
352
+ }
353
+ return projects;
354
+ }
355
+ // ─── Main Setup Wizard ──────────────────────────────────────────────────────
356
+ export async function runSetup(opts) {
357
+ const version = getVersion();
358
+ const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
359
+ // ─── Non-interactive mode ─────────────────────────────────────────────
360
+ if (opts.nonInteractive) {
361
+ const provider = "anthropic";
362
+ const model = "claude-sonnet-4-6";
363
+ const structuringModel = getStructuringModel(provider, model);
364
+ console.log(`${BOLD}Gnosys v${version}${RESET} — non-interactive setup`);
365
+ console.log(` Provider: ${provider}`);
366
+ console.log(` Model: ${model}`);
367
+ console.log(` Structuring: ${structuringModel}`);
368
+ console.log(` API key: skipped`);
369
+ console.log(` IDE setup: skipped`);
370
+ console.log(` Mode: agent`);
371
+ return {
372
+ provider,
373
+ model,
374
+ structuringModel,
375
+ apiKeyWritten: false,
376
+ ides: [],
377
+ mode: "agent",
378
+ upgraded: false,
379
+ };
380
+ }
381
+ // ─── Interactive mode ─────────────────────────────────────────────────
382
+ const rl = createInterface({ input: stdin, output: stdout });
383
+ let setupCompleted = false;
384
+ // Handle Ctrl+C gracefully — only show "cancelled" if setup didn't finish
385
+ rl.on("close", () => {
386
+ if (!setupCompleted) {
387
+ console.log("\n\nSetup cancelled.");
388
+ process.exit(0);
389
+ }
390
+ });
391
+ let upgraded = false;
392
+ try {
393
+ // ─── Banner ───────────────────────────────────────────────────────
394
+ const tagline = "Persistent Memory for AI Agents";
395
+ const versionStr = `Gnosys v${version}`;
396
+ const bannerContentWidth = Math.max(versionStr.length, tagline.length);
397
+ const bannerInner = bannerContentWidth + 4;
398
+ const bannerBorder = "\u2500".repeat(bannerInner);
399
+ console.log();
400
+ console.log(`\u250C${bannerBorder}\u2510`);
401
+ console.log(`\u2502 ${BOLD}${CYAN}Gnosys${RESET} v${version}${" ".repeat(bannerInner - versionStr.length - 2)}\u2502`);
402
+ console.log(`\u2502 ${DIM}${tagline}${RESET}${" ".repeat(bannerInner - tagline.length - 2)}\u2502`);
403
+ console.log(`\u2514${bannerBorder}\u2518`);
404
+ console.log();
405
+ // ─── Pre-check: Upgrade detection ─────────────────────────────────
406
+ const centralDbPath = path.join(os.homedir(), ".gnosys", "gnosys.db");
407
+ const centralDbExists = fsSync.existsSync(centralDbPath);
408
+ if (centralDbExists) {
409
+ const projects = await getRegisteredProjects();
410
+ if (projects.length > 0) {
411
+ const shouldUpgrade = await askYesNo(rl, `Found ${projects.length} project${projects.length === 1 ? "" : "s"}. Upgrade to v${version}?`, true);
412
+ if (shouldUpgrade) {
413
+ const { createProjectIdentity } = await import("./projectIdentity.js");
414
+ for (const project of projects) {
415
+ // Check directory still exists on disk
416
+ try {
417
+ const stat = await fs.stat(project.directory);
418
+ if (stat.isDirectory()) {
419
+ await createProjectIdentity(project.directory, {
420
+ projectName: project.name,
421
+ });
422
+ console.log(` ${CHECK} ${project.name} (${project.directory})`);
423
+ }
424
+ else {
425
+ console.log(` ${CROSS} ${project.name} — directory missing`);
426
+ }
427
+ }
428
+ catch {
429
+ console.log(` ${CROSS} ${project.name} — ${project.directory} not found`);
430
+ }
431
+ }
432
+ // Sync global rules
433
+ try {
434
+ const { syncToTarget } = await import("./rulesGen.js");
435
+ const { GnosysDB } = await import("./db.js");
436
+ const db = GnosysDB.openCentral();
437
+ await syncToTarget(db, projectDir, "global", null);
438
+ db.close();
439
+ console.log(` ${CHECK} Global rules synced (~/.claude/CLAUDE.md)`);
440
+ }
441
+ catch {
442
+ console.log(` ${WARN} Could not sync global rules`);
443
+ }
444
+ upgraded = true;
445
+ console.log();
446
+ }
447
+ }
448
+ }
449
+ // ─── Step 1/5 — Usage mode ────────────────────────────────────────
450
+ const modeIndex = await askChoice(rl, `${BOLD}Step 1/5${RESET} ${DIM}\u2014${RESET} How will you use Gnosys?`, [
451
+ "Agent memory (IDE + CLI)",
452
+ "Web knowledge base (serverless chatbots)",
453
+ "Both",
454
+ ]);
455
+ const mode = modeIndex === 0 ? "agent" : modeIndex === 1 ? "web" : "both";
456
+ // ─── Step 2/5 — Provider ──────────────────────────────────────────
457
+ const providerOptions = PROVIDER_ORDER.map((key) => {
458
+ const tiers = PROVIDER_TIERS[key];
459
+ const display = PROVIDER_DISPLAY[key];
460
+ if (tiers.length === 0)
461
+ return display;
462
+ const minIn = Math.min(...tiers.map((t) => t.input));
463
+ const maxOut = Math.max(...tiers.map((t) => t.output));
464
+ if (minIn === 0 && maxOut === 0)
465
+ return display;
466
+ return `${display} ${DIM}$${minIn.toFixed(2)}\u2013$${maxOut.toFixed(2)}/M tokens${RESET}`;
467
+ });
468
+ // Add "Skip" option
469
+ providerOptions.push("Skip (core memory works without LLM)");
470
+ const providerIndex = await askChoice(rl, `${BOLD}Step 2/5${RESET} ${DIM}\u2014${RESET} Choose your LLM provider`, providerOptions);
471
+ const isSkip = providerIndex === PROVIDER_ORDER.length; // last option
472
+ const provider = isSkip ? "skip" : PROVIDER_ORDER[providerIndex];
473
+ // ─── Step 3/5 — Model tier ────────────────────────────────────────
474
+ let model = "";
475
+ if (!isSkip && provider !== "custom") {
476
+ const tiers = PROVIDER_TIERS[provider];
477
+ if (tiers.length > 0) {
478
+ const isLocal = provider === "ollama" || provider === "lmstudio";
479
+ const tierOptions = tiers.map((t) => {
480
+ const rec = t.recommended ? ` ${CYAN}<- recommended${RESET}` : "";
481
+ if (isLocal) {
482
+ return `${t.name}${rec}`;
483
+ }
484
+ return `${t.name} (${t.model}) ${DIM}${formatPrice(t.input, t.output)}${RESET}${rec}`;
485
+ });
486
+ const tierIndex = await askChoice(rl, `${BOLD}Step 3/5${RESET} ${DIM}\u2014${RESET} Choose model tier`, tierOptions);
487
+ model = tiers[tierIndex].model;
488
+ }
489
+ }
490
+ else if (provider === "custom") {
491
+ // Custom: ask for base URL and model name
492
+ console.log();
493
+ console.log(`${BOLD}Step 3/5${RESET} ${DIM}\u2014${RESET} Custom provider details`);
494
+ console.log();
495
+ const baseUrl = await askInput(rl, "Base URL (OpenAI-compatible)");
496
+ model = await askInput(rl, "Model name");
497
+ if (baseUrl) {
498
+ // Write GNOSYS_LLM_BASE_URL to env file
499
+ const configDir = path.join(os.homedir(), ".config", "gnosys");
500
+ await fs.mkdir(configDir, { recursive: true });
501
+ const envPath = path.join(configDir, ".env");
502
+ let lines = [];
503
+ try {
504
+ const existing = await fs.readFile(envPath, "utf-8");
505
+ lines = existing.split("\n");
506
+ }
507
+ catch {
508
+ // File doesn't exist
509
+ }
510
+ let found = false;
511
+ for (let i = 0; i < lines.length; i++) {
512
+ if (lines[i].startsWith("GNOSYS_LLM_BASE_URL=")) {
513
+ lines[i] = `GNOSYS_LLM_BASE_URL=${baseUrl}`;
514
+ found = true;
515
+ break;
516
+ }
517
+ }
518
+ if (!found) {
519
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
520
+ lines.pop();
521
+ }
522
+ lines.push(`GNOSYS_LLM_BASE_URL=${baseUrl}`);
523
+ }
524
+ await fs.writeFile(envPath, lines.join("\n") + "\n", "utf-8");
525
+ }
526
+ }
527
+ else if (isSkip) {
528
+ // Skip step 3 entirely
529
+ console.log();
530
+ console.log(`${DIM}Step 3/5 \u2014 Model tier: skipped${RESET}`);
531
+ }
532
+ // ─── Step 4/5 — API key ───────────────────────────────────────────
533
+ let apiKeyWritten = false;
534
+ const needsKey = !isSkip &&
535
+ provider !== "ollama" &&
536
+ provider !== "lmstudio";
537
+ if (needsKey) {
538
+ console.log();
539
+ console.log(`${BOLD}Step 4/5${RESET} ${DIM}\u2014${RESET} API Key`);
540
+ console.log();
541
+ const providerLabel = provider === "custom" ? "API" : PROVIDER_DISPLAY[provider]?.split(" ")[0] ?? provider;
542
+ const key = await askInput(rl, `Enter your ${providerLabel} API key (press Enter to skip)`);
543
+ if (key) {
544
+ await writeApiKey(provider, key);
545
+ console.log(`${CHECK} Key saved to ~/.config/gnosys/.env (${maskKey(key)})`);
546
+ apiKeyWritten = true;
547
+ }
548
+ else {
549
+ console.log(`${DIM}Skipped. You can set it later in ~/.config/gnosys/.env${RESET}`);
550
+ }
551
+ }
552
+ else {
553
+ console.log();
554
+ console.log(`${DIM}Step 4/5 \u2014 API key: not needed${RESET}`);
555
+ }
556
+ // ─── Step 5/5 — IDE integration ───────────────────────────────────
557
+ const detectedIdes = await detectIDEs(projectDir);
558
+ const configuredIdes = [];
559
+ if (detectedIdes.length > 0) {
560
+ const ideLabels = {
561
+ claude: "Claude Code",
562
+ cursor: "Cursor",
563
+ codex: "Codex",
564
+ };
565
+ const detectedNames = detectedIdes.map((id) => ideLabels[id] ?? id).join(", ");
566
+ console.log();
567
+ console.log(`${BOLD}Step 5/5${RESET} ${DIM}\u2014${RESET} IDE Integration`);
568
+ console.log();
569
+ console.log(`Detected: ${GREEN}${detectedNames}${RESET}`);
570
+ const ideOptions = [];
571
+ for (const ide of detectedIdes) {
572
+ ideOptions.push(`${ideLabels[ide] ?? ide} only`);
573
+ }
574
+ if (detectedIdes.length > 1) {
575
+ ideOptions.push("All detected");
576
+ }
577
+ ideOptions.push("Skip");
578
+ const ideIndex = await askChoice(rl, "", ideOptions);
579
+ let idesToSetup = [];
580
+ if (ideIndex < detectedIdes.length) {
581
+ // Individual IDE selected
582
+ idesToSetup = [detectedIdes[ideIndex]];
583
+ }
584
+ else if (detectedIdes.length > 1 && ideIndex === detectedIdes.length) {
585
+ // "All detected"
586
+ idesToSetup = [...detectedIdes];
587
+ }
588
+ // Last option is always "Skip"
589
+ for (const ide of idesToSetup) {
590
+ const result = await setupIDE(ide, projectDir);
591
+ if (result.success) {
592
+ console.log(` ${CHECK} ${result.message}`);
593
+ configuredIdes.push(ide);
594
+ }
595
+ else {
596
+ console.log(` ${CROSS} ${ideLabels[ide] ?? ide}: ${result.message}`);
597
+ }
598
+ }
599
+ // Sync global rules
600
+ if (idesToSetup.length > 0) {
601
+ try {
602
+ const { syncToTarget } = await import("./rulesGen.js");
603
+ const { GnosysDB } = await import("./db.js");
604
+ const db = GnosysDB.openCentral();
605
+ await syncToTarget(db, projectDir, "global", null);
606
+ db.close();
607
+ }
608
+ catch {
609
+ // Non-critical — rules sync is best-effort during setup
610
+ }
611
+ }
612
+ }
613
+ else {
614
+ console.log();
615
+ console.log(`${DIM}Step 5/5 \u2014 IDE integration: no IDEs detected${RESET}`);
616
+ }
617
+ // ─── Web extra steps (only if mode is web or both) ────────────────
618
+ let sitemapUrl = "";
619
+ let outputDir = "";
620
+ let llmEnrich = true;
621
+ if (mode === "web" || mode === "both") {
622
+ console.log();
623
+ console.log(`${BOLD}Web Knowledge Base Configuration${RESET}`);
624
+ console.log();
625
+ sitemapUrl = await askInput(rl, "Sitemap URL (or Enter to skip)");
626
+ outputDir = await askInput(rl, "Output directory", { default: "./knowledge" });
627
+ llmEnrich = await askYesNo(rl, "Enable LLM enrichment for ingested pages?", true);
628
+ console.log();
629
+ if (sitemapUrl) {
630
+ console.log(` ${CHECK} Sitemap: ${sitemapUrl}`);
631
+ }
632
+ console.log(` ${CHECK} Output: ${outputDir}`);
633
+ console.log(` ${CHECK} LLM enrichment: ${llmEnrich ? "enabled" : "disabled"}`);
634
+ }
635
+ // ─── Compute structuring model ────────────────────────────────────
636
+ const structuringModel = isSkip ? "" : getStructuringModel(provider, model);
637
+ // ─── Summary ──────────────────────────────────────────────────────
638
+ const summaryRows = [
639
+ ["Provider:", isSkip ? "none" : provider],
640
+ ["Model:", model || "none"],
641
+ ["Structuring:", structuringModel || "n/a"],
642
+ ["API key:", apiKeyWritten ? "~/.config/gnosys/.env" : "not set"],
643
+ ];
644
+ if (configuredIdes.length > 0) {
645
+ const ideLabels = {
646
+ claude: "Claude Code",
647
+ cursor: "Cursor",
648
+ codex: "Codex",
649
+ };
650
+ const ideNames = configuredIdes.map((id) => ideLabels[id] ?? id).join(", ");
651
+ summaryRows.push(["IDEs:", ideNames]);
652
+ }
653
+ if (mode === "web" || mode === "both") {
654
+ summaryRows.push(["Mode:", mode]);
655
+ if (sitemapUrl)
656
+ summaryRows.push(["Sitemap:", sitemapUrl]);
657
+ summaryRows.push(["Output:", outputDir || "./knowledge"]);
658
+ }
659
+ printBox("Setup Complete", summaryRows);
660
+ console.log(`Next: Run ${CYAN}gnosys init${RESET} in any project to start using memory.`);
661
+ console.log();
662
+ setupCompleted = true;
663
+ rl.close();
664
+ return {
665
+ provider: isSkip ? "skip" : provider,
666
+ model,
667
+ structuringModel,
668
+ apiKeyWritten,
669
+ ides: configuredIdes,
670
+ mode,
671
+ upgraded,
672
+ };
673
+ }
674
+ catch (err) {
675
+ rl.close();
676
+ throw err;
677
+ }
678
+ }
679
+ //# sourceMappingURL=setup.js.map