octocode-cli 1.0.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.
@@ -0,0 +1,3600 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import os, { homedir } from "node:os";
4
+ import path from "node:path";
5
+ import { spawnSync, execSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ const colors = {
8
+ reset: "\x1B[0m",
9
+ bright: "\x1B[1m",
10
+ dim: "\x1B[2m",
11
+ underscore: "\x1B[4m",
12
+ red: "\x1B[31m",
13
+ green: "\x1B[32m",
14
+ yellow: "\x1B[33m",
15
+ blue: "\x1B[34m",
16
+ magenta: "\x1B[35m",
17
+ cyan: "\x1B[36m",
18
+ white: "\x1B[37m",
19
+ bgRed: "\x1B[41m",
20
+ bgGreen: "\x1B[42m",
21
+ bgYellow: "\x1B[43m",
22
+ bgBlue: "\x1B[44m",
23
+ bgMagenta: "\x1B[45m"
24
+ };
25
+ const c = (color, text) => `${colors[color]}${text}${colors.reset}`;
26
+ const bold = (text) => c("bright", text);
27
+ const dim = (text) => c("dim", text);
28
+ const isWindows = os.platform() === "win32";
29
+ const isMac = os.platform() === "darwin";
30
+ os.platform() === "linux";
31
+ const HOME = os.homedir();
32
+ function getAppDataPath() {
33
+ if (isWindows) {
34
+ return process.env.APPDATA || path.join(HOME, "AppData", "Roaming");
35
+ }
36
+ return HOME;
37
+ }
38
+ function clearScreen() {
39
+ const clearSequence = "\x1B[2J\x1B[3J\x1B[H";
40
+ process.stdout.write(clearSequence);
41
+ }
42
+ function openFile(filePath, editor) {
43
+ try {
44
+ let command;
45
+ let args;
46
+ if (editor) {
47
+ command = editor;
48
+ args = [filePath];
49
+ } else if (isMac) {
50
+ command = "open";
51
+ args = [filePath];
52
+ } else if (isWindows) {
53
+ command = "cmd";
54
+ args = ["/c", "start", '""', filePath];
55
+ } else {
56
+ command = "xdg-open";
57
+ args = [filePath];
58
+ }
59
+ const result = spawnSync(command, args, {
60
+ stdio: "ignore",
61
+ shell: isWindows && !editor
62
+ });
63
+ return result.status === 0;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+ function openInEditor(filePath, ide) {
69
+ switch (ide) {
70
+ case "cursor":
71
+ return openFile(filePath, "cursor");
72
+ case "vscode":
73
+ return openFile(filePath, "code");
74
+ case "default":
75
+ default:
76
+ return openFile(filePath);
77
+ }
78
+ }
79
+ const notLoadedError = () => {
80
+ throw new Error("Inquirer not loaded. Call loadInquirer() first.");
81
+ };
82
+ let select = notLoadedError;
83
+ let confirm = notLoadedError;
84
+ let input = notLoadedError;
85
+ let checkbox = notLoadedError;
86
+ let Separator = class {
87
+ type = "separator";
88
+ separator = "";
89
+ constructor() {
90
+ throw new Error("Inquirer not loaded. Call loadInquirer() first.");
91
+ }
92
+ };
93
+ let loaded = false;
94
+ async function loadInquirer() {
95
+ if (loaded) return;
96
+ try {
97
+ const inquirer = await import("@inquirer/prompts");
98
+ select = inquirer.select;
99
+ confirm = inquirer.confirm;
100
+ input = inquirer.input;
101
+ checkbox = inquirer.checkbox;
102
+ Separator = inquirer.Separator;
103
+ loaded = true;
104
+ } catch {
105
+ console.error("\n ❌ Missing dependency: @inquirer/prompts");
106
+ console.error(" Please install it first:\n");
107
+ console.error(" npm install @inquirer/prompts\n");
108
+ process.exit(1);
109
+ }
110
+ }
111
+ const prompts = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
112
+ __proto__: null,
113
+ get Separator() {
114
+ return Separator;
115
+ },
116
+ get checkbox() {
117
+ return checkbox;
118
+ },
119
+ get confirm() {
120
+ return confirm;
121
+ },
122
+ get input() {
123
+ return input;
124
+ },
125
+ loadInquirer,
126
+ get select() {
127
+ return select;
128
+ }
129
+ }, Symbol.toStringTag, { value: "Module" }));
130
+ function runCommand(command, args = []) {
131
+ try {
132
+ const result = spawnSync(command, args, {
133
+ encoding: "utf8",
134
+ stdio: ["pipe", "pipe", "pipe"],
135
+ shell: false,
136
+ timeout: 3e4
137
+ // 30s timeout
138
+ });
139
+ return {
140
+ success: result.status === 0,
141
+ stdout: result.stdout?.trim() || "",
142
+ stderr: result.stderr?.trim() || "",
143
+ exitCode: result.status
144
+ };
145
+ } catch (error) {
146
+ return {
147
+ success: false,
148
+ stdout: "",
149
+ stderr: error instanceof Error ? error.message : "Unknown error",
150
+ exitCode: null
151
+ };
152
+ }
153
+ }
154
+ function commandExists(command) {
155
+ const checkCommand = process.platform === "win32" ? "where" : "which";
156
+ const result = runCommand(checkCommand, [command]);
157
+ return result.success;
158
+ }
159
+ function getAppContext() {
160
+ return {
161
+ cwd: getShortCwd(),
162
+ ide: detectIDE(),
163
+ git: detectGit()
164
+ };
165
+ }
166
+ function getShortCwd() {
167
+ const cwd = process.cwd();
168
+ const home = homedir();
169
+ if (cwd.startsWith(home)) {
170
+ return "~" + cwd.slice(home.length);
171
+ }
172
+ return cwd;
173
+ }
174
+ function detectIDE() {
175
+ const env = process.env;
176
+ if (env.CURSOR_AGENT || env.CURSOR_TRACE_ID) {
177
+ return "Cursor";
178
+ }
179
+ if (env.TERM_PROGRAM === "vscode" || env.VSCODE_PID) {
180
+ return "VS Code";
181
+ }
182
+ if (env.TERM_PROGRAM === "Apple_Terminal") {
183
+ return "Terminal";
184
+ }
185
+ return "Terminal";
186
+ }
187
+ function detectGit() {
188
+ const root = runCommand("git", ["rev-parse", "--show-toplevel"]);
189
+ if (!root.success) return void 0;
190
+ const branch = runCommand("git", ["branch", "--show-current"]);
191
+ return {
192
+ root: root.stdout.split("/").pop() || "repo",
193
+ // Just the repo name
194
+ branch: branch.success ? branch.stdout : "HEAD"
195
+ };
196
+ }
197
+ function printLogo() {
198
+ const logo = [
199
+ " ▄▄██████▄▄",
200
+ " ▄██████████████▄",
201
+ " ▐████████████████▌",
202
+ " ▐██▀ ▀████▀ ▀██▌",
203
+ " ▐██ ▄ ████ ▄ ██▌",
204
+ " ▐████▄▄▀▀▀▀▄▄████▌",
205
+ " ▀██████████████▀",
206
+ " ▄▄▄████▀▀ ▀▀████▄▄▄",
207
+ " ▄████▀▀▄▄▄██████▄▄▄▀▀████▄",
208
+ "▐██▌ ▄██▀▀ ▀▀██▄ ▐██▌",
209
+ " ▀▀ ▐██▌ ▐██▌ ▀▀",
210
+ " ▀▀ ▀▀"
211
+ ];
212
+ for (const line of logo) {
213
+ console.log(c("magenta", " " + line));
214
+ }
215
+ }
216
+ function printTitle() {
217
+ const title = [
218
+ " ██████╗ ██████╗████████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
219
+ "██╔═══██╗██╔════╝╚══██╔══╝██╔═══██╗██╔════╝██╔═══██╗██╔══██╗██╔════╝",
220
+ "██║ ██║██║ ██║ ██║ ██║██║ ██║ ██║██║ ██║█████╗ ",
221
+ "██║ ██║██║ ██║ ██║ ██║██║ ██║ ██║██║ ██║██╔══╝ ",
222
+ "╚██████╔╝╚██████╗ ██║ ╚██████╔╝╚██████╗╚██████╔╝██████╔╝███████╗",
223
+ " ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝"
224
+ ];
225
+ for (const line of title) {
226
+ console.log(c("magenta", " " + line));
227
+ }
228
+ }
229
+ function printWelcome() {
230
+ console.log();
231
+ printLogo();
232
+ console.log();
233
+ printTitle();
234
+ console.log();
235
+ console.log();
236
+ try {
237
+ const ctx = getAppContext();
238
+ console.log(` ${dim("📂")} ${ctx.cwd}`);
239
+ console.log();
240
+ let envLine = ` ${dim("💻")} ${bold(ctx.ide)}`;
241
+ if (ctx.git) {
242
+ envLine += ` ${dim("🐙")} ${ctx.git.root} ${dim("(")}${ctx.git.branch}${dim(")")}`;
243
+ }
244
+ console.log(envLine);
245
+ console.log();
246
+ } catch {
247
+ console.log();
248
+ }
249
+ }
250
+ function printGoodbye() {
251
+ console.log();
252
+ console.log(c("magenta", "─".repeat(66)));
253
+ console.log(c("magenta", " Thanks for using Octocode CLI! 👋"));
254
+ console.log(c("magenta", ` 🔍🐙 ${c("underscore", "https://octocode.ai")}`));
255
+ console.log(c("magenta", "─".repeat(66)));
256
+ console.log();
257
+ }
258
+ const activeSpinners = /* @__PURE__ */ new Set();
259
+ function ensureCursorRestored() {
260
+ process.stdout.write("\x1B[?25h");
261
+ }
262
+ let cleanupRegistered = false;
263
+ function registerCleanupHandlers() {
264
+ if (cleanupRegistered) return;
265
+ cleanupRegistered = true;
266
+ process.on("exit", ensureCursorRestored);
267
+ process.on("SIGINT", () => {
268
+ ensureCursorRestored();
269
+ process.exit(0);
270
+ });
271
+ process.on("SIGTERM", () => {
272
+ ensureCursorRestored();
273
+ process.exit(0);
274
+ });
275
+ process.on("uncaughtException", (err) => {
276
+ ensureCursorRestored();
277
+ console.error("Uncaught exception:", err);
278
+ process.exit(1);
279
+ });
280
+ }
281
+ class Spinner {
282
+ text;
283
+ frames;
284
+ i;
285
+ timer;
286
+ constructor(text = "") {
287
+ this.text = text;
288
+ this.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
289
+ this.i = 0;
290
+ this.timer = null;
291
+ }
292
+ start(text) {
293
+ if (text) this.text = text;
294
+ registerCleanupHandlers();
295
+ activeSpinners.add(this);
296
+ process.stdout.write("\x1B[?25l");
297
+ this.timer = setInterval(() => {
298
+ const frame = this.frames[this.i++ % this.frames.length];
299
+ process.stdout.write(`\r${c("cyan", frame)} ${this.text}`);
300
+ }, 80);
301
+ return this;
302
+ }
303
+ stop(symbol = "✓", color = "green") {
304
+ if (this.timer) {
305
+ clearInterval(this.timer);
306
+ this.timer = null;
307
+ }
308
+ activeSpinners.delete(this);
309
+ process.stdout.write(`\r\x1B[2K${c(color, symbol)} ${this.text}
310
+ `);
311
+ process.stdout.write("\x1B[?25h");
312
+ return this;
313
+ }
314
+ succeed(text) {
315
+ if (text) this.text = text;
316
+ return this.stop("✓", "green");
317
+ }
318
+ fail(text) {
319
+ if (text) this.text = text;
320
+ return this.stop("✗", "red");
321
+ }
322
+ info(text) {
323
+ if (text) this.text = text;
324
+ return this.stop("ℹ", "blue");
325
+ }
326
+ warn(text) {
327
+ if (text) this.text = text;
328
+ return this.stop("⚠", "yellow");
329
+ }
330
+ }
331
+ const IDE_INFO = {
332
+ cursor: {
333
+ name: "Cursor",
334
+ description: "AI-first code editor",
335
+ url: "https://cursor.sh"
336
+ },
337
+ claude: {
338
+ name: "Claude Desktop",
339
+ description: "Anthropic's Claude desktop app",
340
+ url: "https://claude.ai/download"
341
+ }
342
+ };
343
+ const INSTALL_METHOD_INFO = {
344
+ direct: {
345
+ name: "Direct (curl)",
346
+ description: "Download and run directly from octocodeai.com",
347
+ pros: ["Always latest version", "No npm required"],
348
+ cons: ["Requires curl (or PowerShell on Windows)", "Slower startup"]
349
+ },
350
+ npx: {
351
+ name: "NPX",
352
+ description: "Run via npx from npm registry",
353
+ pros: ["Standard npm workflow", "Faster after first run (cached)"],
354
+ cons: ["Requires Node.js/npm"]
355
+ }
356
+ };
357
+ function dirExists(dirPath) {
358
+ try {
359
+ return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();
360
+ } catch {
361
+ return false;
362
+ }
363
+ }
364
+ function fileExists(filePath) {
365
+ try {
366
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
367
+ } catch {
368
+ return false;
369
+ }
370
+ }
371
+ function readFileContent(filePath) {
372
+ try {
373
+ if (fileExists(filePath)) {
374
+ return fs.readFileSync(filePath, "utf8");
375
+ }
376
+ } catch {
377
+ }
378
+ return null;
379
+ }
380
+ function writeFileContent(filePath, content) {
381
+ try {
382
+ const dir = path.dirname(filePath);
383
+ if (!dirExists(dir)) {
384
+ fs.mkdirSync(dir, { recursive: true });
385
+ }
386
+ fs.writeFileSync(filePath, content, "utf8");
387
+ return true;
388
+ } catch {
389
+ return false;
390
+ }
391
+ }
392
+ function backupFile(filePath) {
393
+ if (!fileExists(filePath)) {
394
+ return null;
395
+ }
396
+ try {
397
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
398
+ const backupPath = `${filePath}.backup-${timestamp}`;
399
+ fs.copyFileSync(filePath, backupPath);
400
+ return backupPath;
401
+ } catch {
402
+ return null;
403
+ }
404
+ }
405
+ function readJsonFile(filePath) {
406
+ const content = readFileContent(filePath);
407
+ if (!content) return null;
408
+ try {
409
+ return JSON.parse(content);
410
+ } catch {
411
+ return null;
412
+ }
413
+ }
414
+ function writeJsonFile(filePath, data) {
415
+ try {
416
+ const content = JSON.stringify(data, null, 2) + "\n";
417
+ return writeFileContent(filePath, content);
418
+ } catch {
419
+ return false;
420
+ }
421
+ }
422
+ function copyDirectory(src, dest) {
423
+ try {
424
+ if (!dirExists(src)) {
425
+ return false;
426
+ }
427
+ if (!dirExists(dest)) {
428
+ fs.mkdirSync(dest, { recursive: true });
429
+ }
430
+ const entries = fs.readdirSync(src, { withFileTypes: true });
431
+ for (const entry of entries) {
432
+ const srcPath = path.join(src, entry.name);
433
+ const destPath = path.join(dest, entry.name);
434
+ if (entry.isDirectory()) {
435
+ copyDirectory(srcPath, destPath);
436
+ } else {
437
+ fs.copyFileSync(srcPath, destPath);
438
+ }
439
+ }
440
+ return true;
441
+ } catch {
442
+ return false;
443
+ }
444
+ }
445
+ function listSubdirectories(dirPath) {
446
+ try {
447
+ if (!dirExists(dirPath)) {
448
+ return [];
449
+ }
450
+ return fs.readdirSync(dirPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
451
+ } catch {
452
+ return [];
453
+ }
454
+ }
455
+ function getAppSupportDir() {
456
+ if (isWindows) {
457
+ return getAppDataPath();
458
+ }
459
+ if (isMac) {
460
+ return path.join(HOME, "Library", "Application Support");
461
+ }
462
+ return process.env.XDG_CONFIG_HOME || path.join(HOME, ".config");
463
+ }
464
+ function getVSCodeGlobalStoragePath() {
465
+ const appSupport = getAppSupportDir();
466
+ if (isWindows) {
467
+ return path.join(appSupport, "Code", "User", "globalStorage");
468
+ }
469
+ if (isMac) {
470
+ return path.join(appSupport, "Code", "User", "globalStorage");
471
+ }
472
+ return path.join(appSupport, "Code", "User", "globalStorage");
473
+ }
474
+ const MCP_CLIENTS = {
475
+ cursor: {
476
+ id: "cursor",
477
+ name: "Cursor",
478
+ description: "AI-first code editor",
479
+ category: "ide",
480
+ url: "https://cursor.sh",
481
+ envVars: ["CURSOR_AGENT", "CURSOR_TRACE_ID", "CURSOR_SESSION_ID", "CURSOR"]
482
+ },
483
+ "claude-desktop": {
484
+ id: "claude-desktop",
485
+ name: "Claude Desktop",
486
+ description: "Anthropic's desktop app",
487
+ category: "desktop",
488
+ url: "https://claude.ai/download"
489
+ },
490
+ "claude-code": {
491
+ id: "claude-code",
492
+ name: "Claude Code",
493
+ description: "Claude CLI for terminal",
494
+ category: "cli",
495
+ url: "https://docs.anthropic.com/claude-code",
496
+ envVars: ["CLAUDE_CODE"]
497
+ },
498
+ "vscode-cline": {
499
+ id: "vscode-cline",
500
+ name: "Cline (VS Code)",
501
+ description: "AI coding assistant extension",
502
+ category: "extension",
503
+ url: "https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev",
504
+ envVars: ["VSCODE_PID", "TERM_PROGRAM"]
505
+ },
506
+ "vscode-roo": {
507
+ id: "vscode-roo",
508
+ name: "Roo-Cline (VS Code)",
509
+ description: "Roo AI coding extension",
510
+ category: "extension",
511
+ envVars: ["VSCODE_PID"]
512
+ },
513
+ windsurf: {
514
+ id: "windsurf",
515
+ name: "Windsurf",
516
+ description: "Codeium AI IDE",
517
+ category: "ide",
518
+ url: "https://codeium.com/windsurf",
519
+ envVars: ["WINDSURF_SESSION"]
520
+ },
521
+ trae: {
522
+ id: "trae",
523
+ name: "Trae",
524
+ description: "Adaptive AI IDE",
525
+ category: "ide",
526
+ url: "https://trae.ai"
527
+ },
528
+ antigravity: {
529
+ id: "antigravity",
530
+ name: "Antigravity",
531
+ description: "Gemini-powered AI IDE",
532
+ category: "ide"
533
+ },
534
+ "vscode-continue": {
535
+ id: "vscode-continue",
536
+ name: "Continue (VS Code)",
537
+ description: "Open-source AI assistant",
538
+ category: "extension",
539
+ url: "https://continue.dev",
540
+ envVars: ["VSCODE_PID"]
541
+ },
542
+ zed: {
543
+ id: "zed",
544
+ name: "Zed",
545
+ description: "High-performance code editor",
546
+ category: "ide",
547
+ url: "https://zed.dev",
548
+ envVars: ["ZED_TERM"]
549
+ },
550
+ custom: {
551
+ id: "custom",
552
+ name: "Custom Path",
553
+ description: "Specify your own MCP config path",
554
+ category: "cli"
555
+ }
556
+ };
557
+ function getMCPConfigPath(client, customPath) {
558
+ if (client === "custom" && customPath) {
559
+ return customPath;
560
+ }
561
+ const appSupport = getAppSupportDir();
562
+ const vsCodeStorage = getVSCodeGlobalStoragePath();
563
+ switch (client) {
564
+ case "cursor":
565
+ if (isWindows) {
566
+ return path.join(getAppDataPath(), "Cursor", "mcp.json");
567
+ }
568
+ return path.join(HOME, ".cursor", "mcp.json");
569
+ case "claude-desktop":
570
+ if (isWindows) {
571
+ return path.join(appSupport, "Claude", "claude_desktop_config.json");
572
+ }
573
+ if (isMac) {
574
+ return path.join(appSupport, "Claude", "claude_desktop_config.json");
575
+ }
576
+ return path.join(appSupport, "claude", "claude_desktop_config.json");
577
+ case "claude-code":
578
+ return path.join(HOME, ".claude.json");
579
+ case "vscode-cline":
580
+ return path.join(
581
+ vsCodeStorage,
582
+ "saoudrizwan.claude-dev",
583
+ "settings",
584
+ "cline_mcp_settings.json"
585
+ );
586
+ case "vscode-roo":
587
+ return path.join(
588
+ vsCodeStorage,
589
+ "rooveterinaryinc.roo-cline",
590
+ "settings",
591
+ "cline_mcp_settings.json"
592
+ );
593
+ case "windsurf":
594
+ return path.join(HOME, ".codeium", "windsurf", "mcp_config.json");
595
+ case "trae":
596
+ if (isWindows) {
597
+ return path.join(getAppDataPath(), "Trae", "mcp.json");
598
+ }
599
+ if (isMac) {
600
+ return path.join(appSupport, "Trae", "mcp.json");
601
+ }
602
+ return path.join(appSupport, "Trae", "mcp.json");
603
+ case "antigravity":
604
+ return path.join(HOME, ".gemini", "antigravity", "mcp_config.json");
605
+ case "vscode-continue":
606
+ return path.join(HOME, ".continue", "config.json");
607
+ case "zed":
608
+ if (isWindows) {
609
+ return path.join(getAppDataPath(), "Zed", "settings.json");
610
+ }
611
+ if (isMac) {
612
+ return path.join(HOME, ".config", "zed", "settings.json");
613
+ }
614
+ return path.join(appSupport, "zed", "settings.json");
615
+ case "custom":
616
+ throw new Error("Custom path requires customPath parameter");
617
+ default:
618
+ throw new Error(`Unknown MCP client: ${client}`);
619
+ }
620
+ }
621
+ function clientConfigExists(client, customPath) {
622
+ try {
623
+ const configPath = getMCPConfigPath(client, customPath);
624
+ const configDir = path.dirname(configPath);
625
+ return dirExists(configDir);
626
+ } catch {
627
+ return false;
628
+ }
629
+ }
630
+ function configFileExists(client, customPath) {
631
+ try {
632
+ const configPath = getMCPConfigPath(client, customPath);
633
+ return fileExists(configPath);
634
+ } catch {
635
+ return false;
636
+ }
637
+ }
638
+ function detectCurrentClient() {
639
+ const env = process.env;
640
+ if (env.CURSOR_AGENT || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID || env.CURSOR) {
641
+ return "cursor";
642
+ }
643
+ if (env.WINDSURF_SESSION) {
644
+ return "windsurf";
645
+ }
646
+ if (env.CLAUDE_CODE) {
647
+ return "claude-code";
648
+ }
649
+ if (env.ZED_TERM || env.ZED) {
650
+ return "zed";
651
+ }
652
+ if (env.VSCODE_PID || env.TERM_PROGRAM === "vscode") {
653
+ return "vscode-cline";
654
+ }
655
+ return null;
656
+ }
657
+ function ideConfigExists(ide) {
658
+ const clientMap = {
659
+ cursor: "cursor",
660
+ claude: "claude-desktop"
661
+ };
662
+ return clientConfigExists(clientMap[ide]);
663
+ }
664
+ function readMCPConfig(configPath) {
665
+ if (!fileExists(configPath)) {
666
+ return { mcpServers: {} };
667
+ }
668
+ return readJsonFile(configPath);
669
+ }
670
+ function writeMCPConfig(configPath, config, createBackup = true) {
671
+ try {
672
+ let backupPath;
673
+ if (createBackup && fileExists(configPath)) {
674
+ const backup = backupFile(configPath);
675
+ if (backup) {
676
+ backupPath = backup;
677
+ }
678
+ }
679
+ const dir = path.dirname(configPath);
680
+ if (!dirExists(dir)) {
681
+ fs.mkdirSync(dir, { recursive: true });
682
+ }
683
+ const success = writeJsonFile(configPath, config);
684
+ if (!success) {
685
+ return { success: false, error: "Failed to write config file" };
686
+ }
687
+ return { success: true, backupPath };
688
+ } catch (error) {
689
+ return {
690
+ success: false,
691
+ error: error instanceof Error ? error.message : "Unknown error"
692
+ };
693
+ }
694
+ }
695
+ function getOctocodeServerConfig(method, envOptions) {
696
+ let config;
697
+ switch (method) {
698
+ case "direct":
699
+ config = {
700
+ command: "bash",
701
+ args: [
702
+ "-c",
703
+ "curl -sL https://octocodeai.com/octocode/latest/index.js -o /tmp/index.js && node /tmp/index.js"
704
+ ]
705
+ };
706
+ break;
707
+ case "npx":
708
+ config = {
709
+ command: "npx",
710
+ args: ["octocode-mcp@latest"]
711
+ };
712
+ break;
713
+ default:
714
+ throw new Error(`Unknown install method: ${method}`);
715
+ }
716
+ if (envOptions) {
717
+ const env = {};
718
+ if (envOptions.enableLocal) {
719
+ env.ENABLE_LOCAL = "true";
720
+ }
721
+ if (envOptions.githubToken) {
722
+ env.GITHUB_TOKEN = envOptions.githubToken;
723
+ }
724
+ if (Object.keys(env).length > 0) {
725
+ config.env = env;
726
+ }
727
+ }
728
+ return config;
729
+ }
730
+ function getOctocodeServerConfigWindows(method, envOptions) {
731
+ if (method === "direct") {
732
+ const config = {
733
+ command: "powershell",
734
+ args: [
735
+ "-Command",
736
+ "Invoke-WebRequest -Uri 'https://octocodeai.com/octocode/latest/index.js' -OutFile $env:TEMP\\index.js; node $env:TEMP\\index.js"
737
+ ]
738
+ };
739
+ if (envOptions) {
740
+ const env = {};
741
+ if (envOptions.enableLocal) {
742
+ env.ENABLE_LOCAL = "true";
743
+ }
744
+ if (envOptions.githubToken) {
745
+ env.GITHUB_TOKEN = envOptions.githubToken;
746
+ }
747
+ if (Object.keys(env).length > 0) {
748
+ config.env = env;
749
+ }
750
+ }
751
+ return config;
752
+ }
753
+ return getOctocodeServerConfig(method, envOptions);
754
+ }
755
+ function mergeOctocodeConfig(config, method, envOptions) {
756
+ const serverConfig = isWindows ? getOctocodeServerConfigWindows(method, envOptions) : getOctocodeServerConfig(method, envOptions);
757
+ return {
758
+ ...config,
759
+ mcpServers: {
760
+ ...config.mcpServers,
761
+ octocode: serverConfig
762
+ }
763
+ };
764
+ }
765
+ function isOctocodeConfigured(config) {
766
+ return Boolean(config.mcpServers?.octocode);
767
+ }
768
+ function getConfiguredMethod(config) {
769
+ const octocode = config.mcpServers?.octocode;
770
+ if (!octocode) return null;
771
+ if (octocode.command === "npx") return "npx";
772
+ if (octocode.command === "bash" || octocode.command === "powershell") {
773
+ return "direct";
774
+ }
775
+ return null;
776
+ }
777
+ function getClientInstallStatus(client, customPath) {
778
+ const configPath = getMCPConfigPath(client, customPath);
779
+ const configExists = configFileExists(client, customPath);
780
+ let octocodeInstalled = false;
781
+ let method = null;
782
+ if (configExists) {
783
+ const config = readMCPConfig(configPath);
784
+ if (config) {
785
+ octocodeInstalled = isOctocodeConfigured(config);
786
+ method = getConfiguredMethod(config);
787
+ }
788
+ }
789
+ return {
790
+ client,
791
+ configExists,
792
+ octocodeInstalled,
793
+ method,
794
+ configPath
795
+ };
796
+ }
797
+ function getAllClientInstallStatus() {
798
+ const clients = [
799
+ "cursor",
800
+ "claude-desktop",
801
+ "claude-code",
802
+ "vscode-cline",
803
+ "vscode-roo",
804
+ "vscode-continue",
805
+ "windsurf",
806
+ "zed"
807
+ ];
808
+ return clients.map((client) => getClientInstallStatus(client));
809
+ }
810
+ function getClientStatusIndicator(status) {
811
+ if (status.octocodeInstalled) {
812
+ return c("green", "✓ Installed");
813
+ }
814
+ if (status.configExists) {
815
+ return c("blue", "○ Ready");
816
+ }
817
+ if (clientConfigExists(status.client)) {
818
+ return c("dim", "○ Available");
819
+ }
820
+ return c("dim", "○ Not found");
821
+ }
822
+ function getAllClientsWithStatus() {
823
+ const clientOrder = [
824
+ "cursor",
825
+ "claude-desktop",
826
+ "claude-code",
827
+ "windsurf",
828
+ "trae",
829
+ "antigravity",
830
+ "zed",
831
+ "vscode-cline",
832
+ "vscode-roo",
833
+ "vscode-continue"
834
+ ];
835
+ return clientOrder.map((clientId) => ({
836
+ clientId,
837
+ status: getClientInstallStatus(clientId),
838
+ isAvailable: clientConfigExists(clientId)
839
+ }));
840
+ }
841
+ async function selectMCPClient() {
842
+ const currentClient = detectCurrentClient();
843
+ const allClients = getAllClientsWithStatus();
844
+ const installedClients = allClients.filter((c2) => c2.status.octocodeInstalled);
845
+ const availableClients = allClients.filter(
846
+ (c2) => c2.isAvailable && !c2.status.octocodeInstalled
847
+ );
848
+ if (installedClients.length === 0) {
849
+ return await promptNoConfigurationsFound(availableClients, currentClient);
850
+ }
851
+ return await promptExistingConfigurations(
852
+ installedClients,
853
+ availableClients,
854
+ currentClient
855
+ );
856
+ }
857
+ async function promptNoConfigurationsFound(availableClients, currentClient) {
858
+ console.log();
859
+ console.log(c("yellow", " ┌" + "─".repeat(60) + "┐"));
860
+ console.log(
861
+ c("yellow", " │ ") + `${c("yellow", "ℹ")} No octocode configurations found` + " ".repeat(24) + c("yellow", "│")
862
+ );
863
+ console.log(c("yellow", " └" + "─".repeat(60) + "┘"));
864
+ console.log();
865
+ console.log(` ${dim("Octocode is not configured in any MCP client yet.")}`);
866
+ console.log();
867
+ if (availableClients.length === 0) {
868
+ console.log(
869
+ ` ${c("red", "✗")} ${dim("No MCP clients detected on this system.")}`
870
+ );
871
+ console.log();
872
+ console.log(` ${dim("Supported clients:")}`);
873
+ console.log(` ${dim("• Cursor, Claude Desktop, Claude Code")}`);
874
+ console.log(` ${dim("• Windsurf, Zed, VS Code (Cline/Roo/Continue)")}`);
875
+ console.log();
876
+ console.log(` ${dim("Install a supported client and try again,")}`);
877
+ console.log(` ${dim('or use "Custom Path" to specify a config file.')}`);
878
+ console.log();
879
+ const choices2 = [
880
+ {
881
+ name: `${c("cyan", "⚙")} Custom Path - ${dim("Specify your own MCP config path")}`,
882
+ value: "custom"
883
+ },
884
+ new Separator(),
885
+ {
886
+ name: `${c("dim", "← Back")}`,
887
+ value: "back"
888
+ }
889
+ ];
890
+ const selected2 = await select({
891
+ message: "What would you like to do?",
892
+ choices: choices2,
893
+ loop: false
894
+ });
895
+ if (selected2 === "back") return null;
896
+ if (selected2 === "custom") {
897
+ const customPath = await promptCustomPath();
898
+ if (!customPath) return null;
899
+ return { client: "custom", customPath };
900
+ }
901
+ return null;
902
+ }
903
+ const choices = [];
904
+ for (const { clientId, status } of availableClients) {
905
+ const client = MCP_CLIENTS[clientId];
906
+ let name = `${client.name} - ${dim(client.description)}`;
907
+ name += ` ${getClientStatusIndicator(status)}`;
908
+ if (currentClient === clientId) {
909
+ name = `${c("green", "★")} ${name} ${c("yellow", "(Current)")}`;
910
+ }
911
+ choices.push({
912
+ name,
913
+ value: clientId
914
+ });
915
+ }
916
+ choices.sort((a, b) => {
917
+ if (currentClient === a.value) return -1;
918
+ if (currentClient === b.value) return 1;
919
+ return 0;
920
+ });
921
+ choices.push(new Separator());
922
+ choices.push({
923
+ name: `${c("cyan", "⚙")} Custom Path - ${dim("Specify your own MCP config path")}`,
924
+ value: "custom"
925
+ });
926
+ choices.push(new Separator());
927
+ choices.push({
928
+ name: `${c("dim", "← Back")}`,
929
+ value: "back"
930
+ });
931
+ const selected = await select({
932
+ message: "Select a client to install octocode:",
933
+ choices,
934
+ loop: false
935
+ });
936
+ if (selected === "back") return null;
937
+ if (selected === "custom") {
938
+ const customPath = await promptCustomPath();
939
+ if (!customPath) return null;
940
+ return { client: "custom", customPath };
941
+ }
942
+ return { client: selected };
943
+ }
944
+ async function promptExistingConfigurations(installedClients, availableClients, currentClient) {
945
+ console.log();
946
+ console.log(
947
+ ` ${c("green", "✓")} Found ${bold(String(installedClients.length))} octocode configuration${installedClients.length > 1 ? "s" : ""}`
948
+ );
949
+ console.log();
950
+ const choices = [];
951
+ for (const { clientId } of installedClients) {
952
+ const client = MCP_CLIENTS[clientId];
953
+ let name = `${c("green", "✓")} ${client.name} - ${dim("View/Edit configuration")}`;
954
+ if (currentClient === clientId) {
955
+ name += ` ${c("yellow", "(Current)")}`;
956
+ }
957
+ choices.push({
958
+ name,
959
+ value: clientId
960
+ });
961
+ }
962
+ choices.sort((a, b) => {
963
+ if (currentClient === a.value) return -1;
964
+ if (currentClient === b.value) return 1;
965
+ return 0;
966
+ });
967
+ if (availableClients.length > 0) {
968
+ choices.push(new Separator());
969
+ choices.push({
970
+ name: `${c("blue", "+")} Install to another client - ${dim(`${availableClients.length} available`)}`,
971
+ value: "install-new"
972
+ });
973
+ }
974
+ choices.push(new Separator());
975
+ choices.push({
976
+ name: `${c("cyan", "⚙")} Custom Path - ${dim("Specify your own MCP config path")}`,
977
+ value: "custom"
978
+ });
979
+ choices.push(new Separator());
980
+ choices.push({
981
+ name: `${c("dim", "← Back")}`,
982
+ value: "back"
983
+ });
984
+ const selected = await select({
985
+ message: "Select configuration to manage:",
986
+ choices,
987
+ loop: false
988
+ });
989
+ if (selected === "back") return null;
990
+ if (selected === "install-new") {
991
+ return await promptInstallToNewClient(availableClients, currentClient);
992
+ }
993
+ if (selected === "custom") {
994
+ const customPath = await promptCustomPath();
995
+ if (!customPath) return null;
996
+ return { client: "custom", customPath };
997
+ }
998
+ return { client: selected };
999
+ }
1000
+ async function promptInstallToNewClient(availableClients, currentClient) {
1001
+ console.log();
1002
+ console.log(` ${c("blue", "ℹ")} Select a client for new installation:`);
1003
+ console.log();
1004
+ const choices = [];
1005
+ for (const { clientId, status } of availableClients) {
1006
+ const client = MCP_CLIENTS[clientId];
1007
+ let name = `${client.name} - ${dim(client.description)}`;
1008
+ name += ` ${getClientStatusIndicator(status)}`;
1009
+ if (currentClient === clientId) {
1010
+ name = `${c("green", "★")} ${name} ${c("yellow", "(Current)")}`;
1011
+ }
1012
+ choices.push({
1013
+ name,
1014
+ value: clientId
1015
+ });
1016
+ }
1017
+ choices.sort((a, b) => {
1018
+ if (currentClient === a.value) return -1;
1019
+ if (currentClient === b.value) return 1;
1020
+ return 0;
1021
+ });
1022
+ choices.push(new Separator());
1023
+ choices.push({
1024
+ name: `${c("dim", "← Back to configurations")}`,
1025
+ value: "back"
1026
+ });
1027
+ const selected = await select({
1028
+ message: "Select client to install octocode:",
1029
+ choices,
1030
+ loop: false
1031
+ });
1032
+ if (selected === "back") {
1033
+ const allClients = getAllClientsWithStatus();
1034
+ const installedClients = allClients.filter((c2) => c2.status.octocodeInstalled);
1035
+ return await promptExistingConfigurations(
1036
+ installedClients,
1037
+ availableClients,
1038
+ currentClient
1039
+ );
1040
+ }
1041
+ return { client: selected };
1042
+ }
1043
+ function expandPath(inputPath) {
1044
+ if (inputPath.startsWith("~")) {
1045
+ return path.join(process.env.HOME || "", inputPath.slice(1));
1046
+ }
1047
+ return inputPath;
1048
+ }
1049
+ async function promptCustomPath() {
1050
+ console.log();
1051
+ console.log(
1052
+ ` ${c("blue", "ℹ")} Enter the full path to your MCP config file (JSON)`
1053
+ );
1054
+ console.log(` ${dim("Leave empty to go back")}`);
1055
+ console.log();
1056
+ console.log(` ${dim("Common paths:")}`);
1057
+ console.log(` ${dim("•")} ~/.cursor/mcp.json ${dim("(Cursor)")}`);
1058
+ console.log(
1059
+ ` ${dim("•")} ~/Library/Application Support/Claude/claude_desktop_config.json`
1060
+ );
1061
+ console.log(` ${dim("(Claude Desktop)")}`);
1062
+ console.log(` ${dim("•")} ~/.claude.json ${dim("(Claude Code)")}`);
1063
+ console.log(
1064
+ ` ${dim("•")} ~/.codeium/windsurf/mcp_config.json ${dim("(Windsurf)")}`
1065
+ );
1066
+ console.log(` ${dim("•")} ~/.config/zed/settings.json ${dim("(Zed)")}`);
1067
+ console.log(` ${dim("•")} ~/.continue/config.json ${dim("(Continue)")}`);
1068
+ console.log();
1069
+ const customPath = await input({
1070
+ message: "MCP config path (or press Enter to go back):",
1071
+ validate: (value) => {
1072
+ if (!value.trim()) {
1073
+ return true;
1074
+ }
1075
+ const expandedPath = expandPath(value);
1076
+ if (!expandedPath.endsWith(".json")) {
1077
+ return "Path must be a .json file (e.g., mcp.json, config.json)";
1078
+ }
1079
+ if (!path.isAbsolute(expandedPath)) {
1080
+ return "Please provide an absolute path (starting with / or ~)";
1081
+ }
1082
+ const parentDir = path.dirname(expandedPath);
1083
+ if (!dirExists(parentDir)) {
1084
+ return `Parent directory does not exist: ${parentDir}
1085
+ Create it first or choose a different location.`;
1086
+ }
1087
+ return true;
1088
+ }
1089
+ });
1090
+ if (!customPath || !customPath.trim()) return null;
1091
+ return expandPath(customPath);
1092
+ }
1093
+ async function promptLocalTools() {
1094
+ console.log();
1095
+ console.log(` ${c("blue", "ℹ")} ${bold("Local Tools")}`);
1096
+ console.log(
1097
+ ` ${dim("Enable local filesystem tools for searching and reading files")}`
1098
+ );
1099
+ console.log(` ${dim("in your local codebase.")}`);
1100
+ console.log();
1101
+ const choice = await select({
1102
+ message: "Enable local tools?",
1103
+ choices: [
1104
+ {
1105
+ name: `${c("yellow", "○")} Disable ${dim("(Recommended)")} - ${dim("Use only GitHub tools")}`,
1106
+ value: "disable"
1107
+ },
1108
+ {
1109
+ name: `${c("green", "●")} Enable - ${dim("Allow local file exploration")}`,
1110
+ value: "enable"
1111
+ },
1112
+ new Separator(),
1113
+ {
1114
+ name: `${c("dim", "← Back")}`,
1115
+ value: "back"
1116
+ }
1117
+ ],
1118
+ loop: false
1119
+ });
1120
+ if (choice === "back") return null;
1121
+ return choice === "enable";
1122
+ }
1123
+ async function promptGitHubAuth() {
1124
+ console.log();
1125
+ console.log(` ${c("blue", "ℹ")} ${bold("GitHub Authentication")}`);
1126
+ console.log(` ${dim("Required for accessing GitHub repositories.")}`);
1127
+ console.log();
1128
+ const method = await select({
1129
+ message: "How would you like to authenticate with GitHub?",
1130
+ choices: [
1131
+ {
1132
+ name: `${c("green", "●")} gh CLI ${dim("(Recommended)")} - ${dim("Uses existing gh auth")}`,
1133
+ value: "gh-cli"
1134
+ },
1135
+ {
1136
+ name: `${c("yellow", "●")} GITHUB_TOKEN - ${dim("Enter personal access token")}`,
1137
+ value: "token"
1138
+ },
1139
+ {
1140
+ name: `${c("dim", "○")} Skip - ${dim("Configure manually later")}`,
1141
+ value: "skip"
1142
+ },
1143
+ new Separator(),
1144
+ {
1145
+ name: `${c("dim", "← Back")}`,
1146
+ value: "back"
1147
+ }
1148
+ ],
1149
+ loop: false
1150
+ });
1151
+ if (method === "back") return null;
1152
+ if (method === "gh-cli") {
1153
+ console.log();
1154
+ console.log(
1155
+ ` ${c("cyan", "→")} Make sure gh CLI is installed and authenticated:`
1156
+ );
1157
+ console.log(` ${dim("https://cli.github.com/")}`);
1158
+ console.log();
1159
+ console.log(
1160
+ ` ${dim("Run")} ${c("cyan", "gh auth login")} ${dim("if not already authenticated.")}`
1161
+ );
1162
+ console.log();
1163
+ return { method: "gh-cli" };
1164
+ }
1165
+ if (method === "token") {
1166
+ console.log();
1167
+ console.log(` ${dim("Leave empty and press Enter to go back")}`);
1168
+ console.log();
1169
+ const token = await input({
1170
+ message: "Enter your GitHub personal access token:",
1171
+ validate: (value) => {
1172
+ if (!value.trim()) {
1173
+ return true;
1174
+ }
1175
+ if (value.length < 20) {
1176
+ return "Token appears too short";
1177
+ }
1178
+ return true;
1179
+ }
1180
+ });
1181
+ if (!token || !token.trim()) {
1182
+ return null;
1183
+ }
1184
+ console.log();
1185
+ console.log(` ${c("yellow", "⚠")} ${bold("Security Note:")}`);
1186
+ console.log(
1187
+ ` ${dim("Your token will be saved in the MCP configuration file.")}`
1188
+ );
1189
+ console.log(
1190
+ ` ${dim("Make sure this file is not committed to version control.")}`
1191
+ );
1192
+ console.log();
1193
+ return { method: "token", token };
1194
+ }
1195
+ return { method: "skip" };
1196
+ }
1197
+ const GH_CLI_URL = "https://cli.github.com/";
1198
+ function isGitHubCLIInstalled() {
1199
+ return commandExists("gh");
1200
+ }
1201
+ function checkGitHubAuth() {
1202
+ if (!isGitHubCLIInstalled()) {
1203
+ return {
1204
+ installed: false,
1205
+ authenticated: false,
1206
+ error: "GitHub CLI (gh) is not installed"
1207
+ };
1208
+ }
1209
+ const result = runCommand("gh", ["auth", "status"]);
1210
+ if (result.success) {
1211
+ const usernameMatch = result.stdout.match(
1212
+ /Logged in to github\.com.*account\s+(\S+)/i
1213
+ );
1214
+ const username = usernameMatch ? usernameMatch[1] : void 0;
1215
+ return {
1216
+ installed: true,
1217
+ authenticated: true,
1218
+ username
1219
+ };
1220
+ }
1221
+ return {
1222
+ installed: true,
1223
+ authenticated: false,
1224
+ error: result.stderr || "Not authenticated"
1225
+ };
1226
+ }
1227
+ function getGitHubCLIVersion() {
1228
+ const result = runCommand("gh", ["--version"]);
1229
+ if (result.success) {
1230
+ const match = result.stdout.match(/gh version ([\d.]+)/);
1231
+ return match ? match[1] : result.stdout.split("\n")[0];
1232
+ }
1233
+ return null;
1234
+ }
1235
+ function getAuthLoginCommand() {
1236
+ return "gh auth login";
1237
+ }
1238
+ function printGitHubAuthStatus() {
1239
+ const status = checkGitHubAuth();
1240
+ if (!status.installed) {
1241
+ console.log(
1242
+ ` ${c("yellow", "⚠")} GitHub: ${c("yellow", "gh CLI not found")}`
1243
+ );
1244
+ console.log(
1245
+ ` ${c("yellow", "Authenticate using gh CLI")} (${c("underscore", GH_CLI_URL)}) ${c("yellow", "OR use GITHUB_TOKEN configuration")}`
1246
+ );
1247
+ } else if (!status.authenticated) {
1248
+ console.log(
1249
+ ` ${c("yellow", "⚠")} GitHub CLI not authenticated - run ${c("yellow", getAuthLoginCommand())}`
1250
+ );
1251
+ console.log(` ${dim("or set GITHUB_TOKEN in MCP config")}`);
1252
+ } else {
1253
+ console.log(
1254
+ ` ${c("green", "✓")} GitHub: Authenticated as ${c("cyan", status.username || "unknown")}`
1255
+ );
1256
+ }
1257
+ }
1258
+ function printConfigPreview(config) {
1259
+ const hasEnv = config.env && Object.keys(config.env).length > 0;
1260
+ console.log();
1261
+ console.log(c("dim", " {"));
1262
+ console.log(c("dim", ' "mcpServers": {'));
1263
+ console.log(c("magenta", ' "octocode"') + c("dim", ": {"));
1264
+ console.log(
1265
+ c("dim", ' "command": ') + c("green", `"${config.command}"`) + c("dim", ",")
1266
+ );
1267
+ console.log(c("dim", ' "args": ['));
1268
+ config.args.forEach((arg, i) => {
1269
+ const isLast = i === config.args.length - 1;
1270
+ const truncated = arg.length > 50 ? arg.slice(0, 47) + "..." : arg;
1271
+ console.log(
1272
+ c("dim", " ") + c("green", `"${truncated}"`) + (isLast && !hasEnv ? "" : c("dim", ","))
1273
+ );
1274
+ });
1275
+ console.log(c("dim", " ]") + (hasEnv ? c("dim", ",") : ""));
1276
+ if (hasEnv && config.env) {
1277
+ console.log(c("dim", ' "env": {'));
1278
+ const envEntries = Object.entries(config.env);
1279
+ envEntries.forEach(([key, value], i) => {
1280
+ const isLast = i === envEntries.length - 1;
1281
+ const lowerKey = key.toLowerCase();
1282
+ const isSensitive = lowerKey.includes("token") || lowerKey.includes("secret");
1283
+ const displayValue = isSensitive ? "***" : value;
1284
+ console.log(
1285
+ c("dim", " ") + c("cyan", `"${key}"`) + c("dim", ": ") + c("green", `"${displayValue}"`) + (isLast ? "" : c("dim", ","))
1286
+ );
1287
+ });
1288
+ console.log(c("dim", " }"));
1289
+ }
1290
+ console.log(c("dim", " }"));
1291
+ console.log(c("dim", " }"));
1292
+ console.log(c("dim", " }"));
1293
+ console.log();
1294
+ }
1295
+ function printInstallError(result) {
1296
+ console.log();
1297
+ console.log(` ${c("red", "✗")} ${bold("Installation failed")}`);
1298
+ if (result.error) {
1299
+ console.log(` ${dim("Error:")} ${result.error}`);
1300
+ }
1301
+ console.log();
1302
+ }
1303
+ function printExistingOctocodeConfig(server) {
1304
+ const boxWidth = 60;
1305
+ console.log();
1306
+ console.log(c("cyan", " ┌" + "─".repeat(boxWidth) + "┐"));
1307
+ const commandLine = `${server.command} ${server.args.join(" ")}`;
1308
+ const maxLen = boxWidth - 4;
1309
+ const displayCommand = commandLine.length > maxLen ? commandLine.slice(0, maxLen - 3) + "..." : commandLine;
1310
+ const cmdPadding = Math.max(0, boxWidth - 2 - displayCommand.length);
1311
+ console.log(
1312
+ c("cyan", " │ ") + dim(displayCommand) + " ".repeat(cmdPadding) + c("cyan", "│")
1313
+ );
1314
+ if (server.env && Object.keys(server.env).length > 0) {
1315
+ console.log(c("cyan", " │") + " ".repeat(boxWidth) + c("cyan", "│"));
1316
+ const envLabel = "Environment:";
1317
+ const envPadding = boxWidth - 2 - envLabel.length;
1318
+ console.log(
1319
+ c("cyan", " │ ") + bold(envLabel) + " ".repeat(envPadding) + c("cyan", "│")
1320
+ );
1321
+ for (const [key, value] of Object.entries(server.env)) {
1322
+ const lowerKey = key.toLowerCase();
1323
+ const isSensitive = lowerKey.includes("token") || lowerKey.includes("secret");
1324
+ const displayValue = isSensitive ? "***" : value;
1325
+ const envLine = ` ${key}: ${displayValue}`;
1326
+ const truncatedEnv = envLine.length > maxLen ? envLine.slice(0, maxLen - 3) + "..." : envLine;
1327
+ const padding = Math.max(0, boxWidth - 2 - truncatedEnv.length);
1328
+ console.log(
1329
+ c("cyan", " │ ") + dim(truncatedEnv) + " ".repeat(padding) + c("cyan", "│")
1330
+ );
1331
+ }
1332
+ }
1333
+ console.log(c("cyan", " └" + "─".repeat(boxWidth) + "┘"));
1334
+ }
1335
+ function detectAvailableIDEs() {
1336
+ const available = [];
1337
+ if (ideConfigExists("cursor")) {
1338
+ available.push("cursor");
1339
+ }
1340
+ if (ideConfigExists("claude")) {
1341
+ available.push("claude");
1342
+ }
1343
+ return available;
1344
+ }
1345
+ function checkExistingInstallation(ide) {
1346
+ const configPath = getMCPConfigPath(ide);
1347
+ const configExists = fileExists(configPath);
1348
+ if (!configExists) {
1349
+ return { installed: false, configPath, configExists: false };
1350
+ }
1351
+ const config = readMCPConfig(configPath);
1352
+ if (!config) {
1353
+ return { installed: false, configPath, configExists: true };
1354
+ }
1355
+ return {
1356
+ installed: isOctocodeConfigured(config),
1357
+ configPath,
1358
+ configExists: true
1359
+ };
1360
+ }
1361
+ function installOctocode(options) {
1362
+ const { ide, method, force = false } = options;
1363
+ const configPath = getMCPConfigPath(ide);
1364
+ let config = readMCPConfig(configPath) || { mcpServers: {} };
1365
+ if (isOctocodeConfigured(config) && !force) {
1366
+ return {
1367
+ success: false,
1368
+ configPath,
1369
+ alreadyInstalled: true,
1370
+ error: "Octocode is already configured. Use --force to overwrite."
1371
+ };
1372
+ }
1373
+ config = mergeOctocodeConfig(config, method);
1374
+ const writeResult = writeMCPConfig(configPath, config);
1375
+ if (!writeResult.success) {
1376
+ return {
1377
+ success: false,
1378
+ configPath,
1379
+ error: writeResult.error || "Failed to write config"
1380
+ };
1381
+ }
1382
+ return {
1383
+ success: true,
1384
+ configPath,
1385
+ backupPath: writeResult.backupPath
1386
+ };
1387
+ }
1388
+ function getInstallPreview(ide, method) {
1389
+ const configPath = getMCPConfigPath(ide);
1390
+ const existing = checkExistingInstallation(ide);
1391
+ const existingConfig = readMCPConfig(configPath);
1392
+ const serverConfig = isWindows ? getOctocodeServerConfigWindows(method) : getOctocodeServerConfig(method);
1393
+ let action = "create";
1394
+ if (existing.installed) {
1395
+ action = "override";
1396
+ } else if (existing.configExists) {
1397
+ action = "add";
1398
+ }
1399
+ return {
1400
+ ide,
1401
+ method,
1402
+ configPath,
1403
+ serverConfig,
1404
+ action,
1405
+ existingMethod: existingConfig ? getConfiguredMethod(existingConfig) : null
1406
+ };
1407
+ }
1408
+ function checkExistingClientInstallation(client, customPath) {
1409
+ const configPath = client === "custom" && customPath ? customPath : getMCPConfigPath(client, customPath);
1410
+ const configExists = fileExists(configPath);
1411
+ if (!configExists) {
1412
+ return { installed: false, configPath, configExists: false };
1413
+ }
1414
+ const config = readMCPConfig(configPath);
1415
+ if (!config) {
1416
+ return { installed: false, configPath, configExists: true };
1417
+ }
1418
+ return {
1419
+ installed: isOctocodeConfigured(config),
1420
+ configPath,
1421
+ configExists: true
1422
+ };
1423
+ }
1424
+ function installOctocodeForClient(options) {
1425
+ const { client, method, customPath, force = false, envOptions } = options;
1426
+ const configPath = client === "custom" && customPath ? customPath : getMCPConfigPath(client, customPath);
1427
+ let config = readMCPConfig(configPath) || { mcpServers: {} };
1428
+ if (isOctocodeConfigured(config) && !force) {
1429
+ return {
1430
+ success: false,
1431
+ configPath,
1432
+ alreadyInstalled: true,
1433
+ error: "Octocode is already configured. Use --force to overwrite."
1434
+ };
1435
+ }
1436
+ config = mergeOctocodeConfig(config, method, envOptions);
1437
+ const writeResult = writeMCPConfig(configPath, config);
1438
+ if (!writeResult.success) {
1439
+ return {
1440
+ success: false,
1441
+ configPath,
1442
+ error: writeResult.error || "Failed to write config"
1443
+ };
1444
+ }
1445
+ return {
1446
+ success: true,
1447
+ configPath,
1448
+ backupPath: writeResult.backupPath
1449
+ };
1450
+ }
1451
+ function getInstallPreviewForClient(client, method, customPath, envOptions) {
1452
+ const configPath = client === "custom" && customPath ? customPath : getMCPConfigPath(client, customPath);
1453
+ const existing = checkExistingClientInstallation(client, customPath);
1454
+ const existingConfig = readMCPConfig(configPath);
1455
+ const serverConfig = isWindows ? getOctocodeServerConfigWindows(method, envOptions) : getOctocodeServerConfig(method, envOptions);
1456
+ let action = "create";
1457
+ if (existing.installed) {
1458
+ action = "override";
1459
+ } else if (existing.configExists) {
1460
+ action = "add";
1461
+ }
1462
+ return {
1463
+ client,
1464
+ method,
1465
+ configPath,
1466
+ serverConfig,
1467
+ action,
1468
+ existingMethod: existingConfig ? getConfiguredMethod(existingConfig) : null
1469
+ };
1470
+ }
1471
+ async function runInstallFlow() {
1472
+ await loadInquirer();
1473
+ console.log();
1474
+ console.log(c("blue", "━".repeat(66)));
1475
+ console.log(` 📦 ${bold("Configure MCP server for your environment")}`);
1476
+ console.log(c("blue", "━".repeat(66)));
1477
+ console.log();
1478
+ const state = {
1479
+ client: null,
1480
+ hasExistingOctocode: false,
1481
+ enableLocal: false,
1482
+ githubAuth: { method: "skip" }
1483
+ };
1484
+ let currentStep = "client";
1485
+ while (currentStep !== "done") {
1486
+ switch (currentStep) {
1487
+ case "client": {
1488
+ const selection = await selectMCPClient();
1489
+ if (!selection) {
1490
+ return;
1491
+ }
1492
+ state.client = selection.client;
1493
+ state.customPath = selection.customPath;
1494
+ const configPath = state.customPath || getMCPConfigPath(state.client);
1495
+ const existingConfig = readMCPConfig(configPath);
1496
+ state.hasExistingOctocode = !!existingConfig?.mcpServers?.octocode;
1497
+ if (state.hasExistingOctocode) {
1498
+ currentStep = "updateConfirm";
1499
+ } else {
1500
+ currentStep = "localTools";
1501
+ }
1502
+ break;
1503
+ }
1504
+ case "updateConfirm": {
1505
+ const configPath = state.customPath || getMCPConfigPath(state.client);
1506
+ const existingConfig = readMCPConfig(configPath);
1507
+ console.log();
1508
+ console.log(c("yellow", " ┌" + "─".repeat(60) + "┐"));
1509
+ console.log(
1510
+ c("yellow", " │ ") + `${c("yellow", "⚠")} ${bold("Octocode is already configured!")}` + " ".repeat(28) + c("yellow", "│")
1511
+ );
1512
+ console.log(c("yellow", " └" + "─".repeat(60) + "┘"));
1513
+ console.log();
1514
+ console.log(` ${bold("Current octocode configuration:")}`);
1515
+ printExistingOctocodeConfig(existingConfig.mcpServers.octocode);
1516
+ console.log();
1517
+ console.log(` ${dim("Config file:")} ${c("cyan", configPath)}`);
1518
+ console.log();
1519
+ const updateChoice = await select({
1520
+ message: "What would you like to do?",
1521
+ choices: [
1522
+ {
1523
+ name: `${c("green", "✓")} Update existing configuration`,
1524
+ value: "update"
1525
+ },
1526
+ new Separator(),
1527
+ {
1528
+ name: `${c("dim", "← Back to client selection")}`,
1529
+ value: "back"
1530
+ }
1531
+ ],
1532
+ loop: false
1533
+ });
1534
+ if (updateChoice === "back") {
1535
+ currentStep = "client";
1536
+ } else {
1537
+ currentStep = "localTools";
1538
+ }
1539
+ break;
1540
+ }
1541
+ case "localTools": {
1542
+ const enableLocal = await promptLocalTools();
1543
+ if (enableLocal === null) {
1544
+ currentStep = state.hasExistingOctocode ? "updateConfirm" : "client";
1545
+ } else {
1546
+ state.enableLocal = enableLocal;
1547
+ currentStep = "githubAuth";
1548
+ }
1549
+ break;
1550
+ }
1551
+ case "githubAuth": {
1552
+ const githubAuth = await promptGitHubAuth();
1553
+ if (githubAuth === null) {
1554
+ currentStep = "localTools";
1555
+ } else {
1556
+ state.githubAuth = githubAuth;
1557
+ currentStep = "confirm";
1558
+ }
1559
+ break;
1560
+ }
1561
+ case "confirm": {
1562
+ const shouldProceed = await showConfirmationAndPrompt(state);
1563
+ if (shouldProceed === "proceed") {
1564
+ currentStep = "install";
1565
+ } else if (shouldProceed === "back") {
1566
+ currentStep = "githubAuth";
1567
+ } else {
1568
+ console.log(` ${dim("Configuration cancelled.")}`);
1569
+ return;
1570
+ }
1571
+ break;
1572
+ }
1573
+ case "install": {
1574
+ await performInstall(state);
1575
+ currentStep = "done";
1576
+ break;
1577
+ }
1578
+ }
1579
+ }
1580
+ }
1581
+ async function showConfirmationAndPrompt(state) {
1582
+ const clientInfo = MCP_CLIENTS[state.client];
1583
+ const method = "npx";
1584
+ const envOptions = {};
1585
+ if (state.enableLocal) {
1586
+ envOptions.enableLocal = true;
1587
+ }
1588
+ if (state.githubAuth.method === "token" && state.githubAuth.token) {
1589
+ envOptions.githubToken = state.githubAuth.token;
1590
+ }
1591
+ const preview = getInstallPreviewForClient(
1592
+ state.client,
1593
+ method,
1594
+ state.customPath,
1595
+ envOptions
1596
+ );
1597
+ console.log();
1598
+ if (state.hasExistingOctocode) {
1599
+ console.log(
1600
+ ` ${c("yellow", "⚠")} Will ${c("yellow", "UPDATE")} existing octocode configuration`
1601
+ );
1602
+ } else if (preview.action === "add") {
1603
+ console.log(
1604
+ ` ${c("blue", "ℹ")} Config file exists, will ${c("green", "ADD")} octocode entry`
1605
+ );
1606
+ } else {
1607
+ console.log(
1608
+ ` ${c("green", "✓")} Will ${c("green", "CREATE")} new config file`
1609
+ );
1610
+ }
1611
+ console.log();
1612
+ console.log(c("blue", " ┌" + "─".repeat(60) + "┐"));
1613
+ console.log(
1614
+ c("blue", " │ ") + bold("Configuration to be added:") + " ".repeat(33) + c("blue", "│")
1615
+ );
1616
+ console.log(c("blue", " └" + "─".repeat(60) + "┘"));
1617
+ printConfigPreview(preview.serverConfig);
1618
+ console.log();
1619
+ console.log(` ${bold("Summary:")}`);
1620
+ console.log(` ${dim("Client:")} ${clientInfo.name}`);
1621
+ console.log(` ${dim("Method:")} npx (octocode-mcp@latest)`);
1622
+ const localStatus = state.enableLocal ? c("green", "Enabled") : c("dim", "Disabled");
1623
+ console.log(` ${dim("Local Tools:")} ${localStatus}`);
1624
+ let authStatus;
1625
+ if (state.githubAuth.method === "token") {
1626
+ authStatus = c("green", "Token configured");
1627
+ } else if (state.githubAuth.method === "gh-cli") {
1628
+ authStatus = c("cyan", "Using gh CLI");
1629
+ } else {
1630
+ authStatus = c("dim", "Not configured");
1631
+ }
1632
+ console.log(` ${dim("GitHub Auth:")} ${authStatus}`);
1633
+ let actionStatus;
1634
+ if (state.hasExistingOctocode) {
1635
+ actionStatus = c("yellow", "UPDATE");
1636
+ } else if (preview.action === "add") {
1637
+ actionStatus = c("green", "ADD");
1638
+ } else {
1639
+ actionStatus = c("green", "CREATE");
1640
+ }
1641
+ console.log(` ${dim("Action:")} ${actionStatus}`);
1642
+ console.log();
1643
+ console.log(` ${c("yellow", "⚠")} ${bold("Note:")}`);
1644
+ console.log(
1645
+ ` ${dim("Nothing is saved to any server. Configuration is stored locally at:")}`
1646
+ );
1647
+ console.log(` ${c("cyan", preview.configPath)}`);
1648
+ console.log();
1649
+ const choice = await select({
1650
+ message: "What would you like to do?",
1651
+ choices: [
1652
+ {
1653
+ name: `${c("green", "✓")} Proceed with configuration`,
1654
+ value: "proceed"
1655
+ },
1656
+ new Separator(),
1657
+ {
1658
+ name: `${c("dim", "← Back to edit options")}`,
1659
+ value: "back"
1660
+ },
1661
+ {
1662
+ name: `${c("dim", "✗ Cancel")}`,
1663
+ value: "cancel"
1664
+ }
1665
+ ],
1666
+ loop: false
1667
+ });
1668
+ return choice;
1669
+ }
1670
+ async function performInstall(state) {
1671
+ const method = "npx";
1672
+ const envOptions = {};
1673
+ if (state.enableLocal) {
1674
+ envOptions.enableLocal = true;
1675
+ }
1676
+ if (state.githubAuth.method === "token" && state.githubAuth.token) {
1677
+ envOptions.githubToken = state.githubAuth.token;
1678
+ }
1679
+ const preview = getInstallPreviewForClient(
1680
+ state.client,
1681
+ method,
1682
+ state.customPath,
1683
+ envOptions
1684
+ );
1685
+ const spinner = new Spinner("Configuring octocode-mcp...").start();
1686
+ await new Promise((resolve) => setTimeout(resolve, 500));
1687
+ const result = installOctocodeForClient({
1688
+ client: state.client,
1689
+ method,
1690
+ customPath: state.customPath,
1691
+ force: state.hasExistingOctocode,
1692
+ envOptions
1693
+ });
1694
+ if (result.success) {
1695
+ spinner.succeed("Octocode configured successfully!");
1696
+ printInstallSuccessForClient(result, state.client, preview.configPath);
1697
+ } else {
1698
+ spinner.fail("Configuration failed");
1699
+ printInstallError(result);
1700
+ }
1701
+ }
1702
+ function printInstallSuccessForClient(result, client, configPath) {
1703
+ const clientInfo = MCP_CLIENTS[client];
1704
+ console.log();
1705
+ console.log(c("green", " ┌" + "─".repeat(60) + "┐"));
1706
+ console.log(
1707
+ c("green", " │ ") + `${c("green", "✓")} ${bold("Octocode installed successfully!")}` + " ".repeat(26) + c("green", "│")
1708
+ );
1709
+ console.log(c("green", " └" + "─".repeat(60) + "┘"));
1710
+ console.log();
1711
+ console.log(` ${bold("Configuration saved to:")}`);
1712
+ console.log(` ${c("cyan", configPath)}`);
1713
+ console.log();
1714
+ if (result.backupPath) {
1715
+ console.log(` ${dim("Backup saved to:")} ${result.backupPath}`);
1716
+ console.log();
1717
+ }
1718
+ console.log(` ${bold("Next steps:")}`);
1719
+ console.log(` 1. Restart ${clientInfo?.name || client}`);
1720
+ console.log(` 2. Look for ${c("cyan", "octocode")} in MCP servers`);
1721
+ console.log();
1722
+ }
1723
+ function printNodeEnvironmentStatus(status) {
1724
+ if (status.nodeInstalled) {
1725
+ console.log(
1726
+ ` ${c("green", "✓")} Node.js: ${bold(status.nodeVersion || "unknown")}`
1727
+ );
1728
+ } else {
1729
+ console.log(` ${c("red", "✗")} Node.js: ${c("red", "Not found in PATH")}`);
1730
+ }
1731
+ if (status.npmInstalled) {
1732
+ console.log(
1733
+ ` ${c("green", "✓")} npm: ${bold(status.npmVersion || "unknown")}`
1734
+ );
1735
+ } else {
1736
+ console.log(
1737
+ ` ${c("yellow", "⚠")} npm: ${c("yellow", "Not found in PATH")}`
1738
+ );
1739
+ }
1740
+ printRegistryStatus(status.registryStatus, status.registryLatency);
1741
+ printOctocodePackageStatus(
1742
+ status.octocodePackageAvailable,
1743
+ status.octocodePackageVersion
1744
+ );
1745
+ }
1746
+ function printRegistryStatus(status, latency) {
1747
+ const latencyStr = latency !== null ? `(${latency}ms)` : "";
1748
+ switch (status) {
1749
+ case "ok":
1750
+ console.log(
1751
+ ` ${c("green", "✓")} Registry: ${c("green", "OK")} ${dim(latencyStr)}`
1752
+ );
1753
+ break;
1754
+ case "slow":
1755
+ console.log(
1756
+ ` ${c("yellow", "⚠")} Registry: ${c("yellow", "Slow")} ${dim(latencyStr)}`
1757
+ );
1758
+ break;
1759
+ case "failed":
1760
+ console.log(
1761
+ ` ${c("red", "✗")} Registry: ${c("red", "Unreachable")} ${latency !== null ? dim(latencyStr) : ""}`
1762
+ );
1763
+ break;
1764
+ }
1765
+ }
1766
+ function printOctocodePackageStatus(available, version) {
1767
+ if (available) {
1768
+ console.log(
1769
+ ` ${c("green", "✓")} octocode-mcp: ${c("green", "Available")} ${dim(`(v${version})`)}`
1770
+ );
1771
+ } else {
1772
+ console.log(
1773
+ ` ${c("red", "✗")} octocode-mcp: ${c("red", "Not found in registry")}`
1774
+ );
1775
+ }
1776
+ }
1777
+ function printNodeDoctorHint() {
1778
+ console.log(
1779
+ ` ${dim("For deeper diagnostics:")} ${c("cyan", "npx node-doctor")}`
1780
+ );
1781
+ }
1782
+ function hasEnvironmentIssues(status) {
1783
+ return !status.nodeInstalled || !status.npmInstalled || status.registryStatus === "slow" || status.registryStatus === "failed" || !status.octocodePackageAvailable;
1784
+ }
1785
+ const REGISTRY_OK_THRESHOLD = 1e3;
1786
+ const REGISTRY_SLOW_THRESHOLD = 3e3;
1787
+ function checkNodeInPath() {
1788
+ try {
1789
+ const version = execSync("node --version", {
1790
+ encoding: "utf-8",
1791
+ timeout: 5e3,
1792
+ stdio: ["pipe", "pipe", "pipe"]
1793
+ }).trim();
1794
+ return { installed: true, version };
1795
+ } catch {
1796
+ return { installed: false, version: null };
1797
+ }
1798
+ }
1799
+ function checkNpmInPath() {
1800
+ try {
1801
+ const version = execSync("npm --version", {
1802
+ encoding: "utf-8",
1803
+ timeout: 5e3,
1804
+ stdio: ["pipe", "pipe", "pipe"]
1805
+ }).trim();
1806
+ return { installed: true, version: `v${version}` };
1807
+ } catch {
1808
+ return { installed: false, version: null };
1809
+ }
1810
+ }
1811
+ async function checkNpmRegistry() {
1812
+ const registryUrl = "https://registry.npmjs.org";
1813
+ try {
1814
+ const start = Date.now();
1815
+ const controller = new AbortController();
1816
+ const timeoutId = setTimeout(
1817
+ () => controller.abort(),
1818
+ REGISTRY_SLOW_THRESHOLD
1819
+ );
1820
+ const response = await fetch(registryUrl, {
1821
+ method: "HEAD",
1822
+ signal: controller.signal
1823
+ });
1824
+ clearTimeout(timeoutId);
1825
+ const latency = Date.now() - start;
1826
+ if (!response.ok) {
1827
+ return { status: "failed", latency };
1828
+ }
1829
+ if (latency > REGISTRY_SLOW_THRESHOLD) {
1830
+ return { status: "failed", latency };
1831
+ }
1832
+ if (latency > REGISTRY_OK_THRESHOLD) {
1833
+ return { status: "slow", latency };
1834
+ }
1835
+ return { status: "ok", latency };
1836
+ } catch {
1837
+ return { status: "failed", latency: null };
1838
+ }
1839
+ }
1840
+ function checkOctocodePackage() {
1841
+ try {
1842
+ const result = execSync("npm view octocode-mcp version", {
1843
+ encoding: "utf-8",
1844
+ timeout: 1e4,
1845
+ stdio: ["pipe", "pipe", "pipe"]
1846
+ }).trim();
1847
+ return { available: true, version: result };
1848
+ } catch {
1849
+ return { available: false, version: null };
1850
+ }
1851
+ }
1852
+ async function checkNodeEnvironment() {
1853
+ const nodeCheck = checkNodeInPath();
1854
+ const npmCheck = checkNpmInPath();
1855
+ const registryCheck = await checkNpmRegistry();
1856
+ const octocodeCheck = checkOctocodePackage();
1857
+ return {
1858
+ nodeInstalled: nodeCheck.installed,
1859
+ nodeVersion: nodeCheck.version,
1860
+ npmInstalled: npmCheck.installed,
1861
+ npmVersion: npmCheck.version,
1862
+ registryStatus: registryCheck.status,
1863
+ registryLatency: registryCheck.latency,
1864
+ octocodePackageAvailable: octocodeCheck.available,
1865
+ octocodePackageVersion: octocodeCheck.version
1866
+ };
1867
+ }
1868
+ const ALL_AVAILABLE_TOOLS = {
1869
+ // GitHub tools
1870
+ github: [
1871
+ {
1872
+ id: "githubSearchCode",
1873
+ name: "Search Code",
1874
+ description: "Search for code patterns in GitHub repositories"
1875
+ },
1876
+ {
1877
+ id: "githubGetFileContent",
1878
+ name: "Get File Content",
1879
+ description: "Fetch file content from GitHub repositories"
1880
+ },
1881
+ {
1882
+ id: "githubViewRepoStructure",
1883
+ name: "View Repo Structure",
1884
+ description: "Browse repository directory structure"
1885
+ },
1886
+ {
1887
+ id: "githubSearchRepositories",
1888
+ name: "Search Repositories",
1889
+ description: "Search for GitHub repositories"
1890
+ },
1891
+ {
1892
+ id: "githubSearchPullRequests",
1893
+ name: "Search Pull Requests",
1894
+ description: "Search for pull requests and view diffs"
1895
+ },
1896
+ {
1897
+ id: "packageSearch",
1898
+ name: "Package Search",
1899
+ description: "Search npm/Python packages and find their repos"
1900
+ }
1901
+ ],
1902
+ // Local tools
1903
+ local: [
1904
+ {
1905
+ id: "localSearchCode",
1906
+ name: "Ripgrep Search",
1907
+ description: "Fast content search with regex support"
1908
+ },
1909
+ {
1910
+ id: "localViewStructure",
1911
+ name: "View Structure",
1912
+ description: "Browse local directory structure"
1913
+ },
1914
+ {
1915
+ id: "localFindFiles",
1916
+ name: "Find Files",
1917
+ description: "Find files by name, time, size, permissions"
1918
+ },
1919
+ {
1920
+ id: "localGetFileContent",
1921
+ name: "Fetch Content",
1922
+ description: "Read targeted sections of local files"
1923
+ }
1924
+ ]
1925
+ };
1926
+ const ALL_CONFIG_OPTIONS = [
1927
+ {
1928
+ id: "enableLocal",
1929
+ envVar: "ENABLE_LOCAL",
1930
+ name: "Local File Tools",
1931
+ description: "Enable local file exploration tools for searching and browsing local files",
1932
+ type: "boolean",
1933
+ defaultValue: "false"
1934
+ },
1935
+ {
1936
+ id: "githubApiUrl",
1937
+ envVar: "GITHUB_API_URL",
1938
+ name: "GitHub API URL",
1939
+ description: "Custom GitHub API endpoint (for GitHub Enterprise)",
1940
+ type: "string",
1941
+ defaultValue: "https://api.github.com"
1942
+ },
1943
+ {
1944
+ id: "toolsToRun",
1945
+ envVar: "TOOLS_TO_RUN",
1946
+ name: "Tools to Run",
1947
+ description: "Specific tools to enable (all others disabled)",
1948
+ type: "array",
1949
+ defaultValue: "",
1950
+ toolCategory: "all"
1951
+ },
1952
+ {
1953
+ id: "enableTools",
1954
+ envVar: "ENABLE_TOOLS",
1955
+ name: "Enable Tools",
1956
+ description: "Additional tools to enable",
1957
+ type: "array",
1958
+ defaultValue: "",
1959
+ toolCategory: "all"
1960
+ },
1961
+ {
1962
+ id: "disableTools",
1963
+ envVar: "DISABLE_TOOLS",
1964
+ name: "Disable Tools",
1965
+ description: "Tools to disable",
1966
+ type: "array",
1967
+ defaultValue: "",
1968
+ toolCategory: "all"
1969
+ },
1970
+ {
1971
+ id: "requestTimeout",
1972
+ envVar: "REQUEST_TIMEOUT",
1973
+ name: "Request Timeout",
1974
+ description: "API request timeout in milliseconds",
1975
+ type: "number",
1976
+ defaultValue: "30000",
1977
+ validation: { min: 3e4, max: 6e5 }
1978
+ },
1979
+ {
1980
+ id: "maxRetries",
1981
+ envVar: "MAX_RETRIES",
1982
+ name: "Max Retries",
1983
+ description: "Maximum number of API retry attempts",
1984
+ type: "number",
1985
+ defaultValue: "3",
1986
+ validation: { min: 0, max: 10 }
1987
+ }
1988
+ ];
1989
+ function getAllTools() {
1990
+ return [
1991
+ ...ALL_AVAILABLE_TOOLS.github.map((t) => ({
1992
+ ...t,
1993
+ category: "github"
1994
+ })),
1995
+ ...ALL_AVAILABLE_TOOLS.local.map((t) => ({
1996
+ ...t,
1997
+ category: "local"
1998
+ }))
1999
+ ];
2000
+ }
2001
+ function getCurrentValue(env, option) {
2002
+ const value = env[option.envVar];
2003
+ if (value === void 0 || value === null || value === "") {
2004
+ return option.defaultValue;
2005
+ }
2006
+ return value;
2007
+ }
2008
+ function formatDisplayValue(option, value) {
2009
+ if (option.type === "boolean") {
2010
+ const isEnabled = value === "1" || value.toLowerCase() === "true";
2011
+ return isEnabled ? c("green", "enabled") : c("dim", "disabled");
2012
+ }
2013
+ if (option.type === "array") {
2014
+ if (!value || value === "") {
2015
+ return c("dim", option.id === "toolsToRun" ? "(all tools)" : "(none)");
2016
+ }
2017
+ const tools = value.split(",").filter((t) => t.trim());
2018
+ return tools.length > 2 ? `${tools.slice(0, 2).join(", ")} ${c("dim", `+${tools.length - 2} more`)}` : tools.join(", ");
2019
+ }
2020
+ if (option.type === "number") {
2021
+ if (value === option.defaultValue) {
2022
+ return `${value} ${c("dim", "(default)")}`;
2023
+ }
2024
+ return value;
2025
+ }
2026
+ if (value === option.defaultValue) {
2027
+ return `${c("dim", value)}`;
2028
+ }
2029
+ return c("cyan", value);
2030
+ }
2031
+ function parseBooleanValue(value) {
2032
+ return value === "1" || value.toLowerCase() === "true";
2033
+ }
2034
+ async function showConfigMenu() {
2035
+ const choice = await select({
2036
+ message: "Configuration Options:",
2037
+ choices: [
2038
+ {
2039
+ name: "🔧 Edit configuration",
2040
+ value: "edit",
2041
+ description: "Configure all octocode-mcp settings for a client"
2042
+ },
2043
+ {
2044
+ name: "📋 View all configuration options",
2045
+ value: "view",
2046
+ description: "Show available environment variables and their defaults"
2047
+ },
2048
+ {
2049
+ name: "📄 Show current JSON config",
2050
+ value: "show-json",
2051
+ description: "Display the actual MCP config JSON for a client"
2052
+ },
2053
+ new Separator(),
2054
+ {
2055
+ name: `${c("dim", "← Back to main menu")}`,
2056
+ value: "back"
2057
+ }
2058
+ ],
2059
+ pageSize: 10,
2060
+ loop: false,
2061
+ theme: {
2062
+ prefix: " ",
2063
+ style: {
2064
+ highlight: (text) => c("cyan", text),
2065
+ message: (text) => bold(text)
2066
+ }
2067
+ }
2068
+ });
2069
+ return choice;
2070
+ }
2071
+ async function runConfigOptionsFlow() {
2072
+ await loadInquirer();
2073
+ console.log();
2074
+ console.log(c("blue", "━".repeat(66)));
2075
+ console.log(` ⚙️ ${bold("Configure Octocode Options")}`);
2076
+ console.log(c("blue", "━".repeat(66)));
2077
+ console.log();
2078
+ const choice = await showConfigMenu();
2079
+ switch (choice) {
2080
+ case "view":
2081
+ showConfigInfo();
2082
+ await pressEnterToContinue$1();
2083
+ break;
2084
+ case "edit":
2085
+ await runEditConfigFlow();
2086
+ break;
2087
+ case "show-json":
2088
+ await showCurrentJsonConfig();
2089
+ break;
2090
+ }
2091
+ }
2092
+ async function pressEnterToContinue$1() {
2093
+ const { input: input2 } = await Promise.resolve().then(() => prompts);
2094
+ console.log();
2095
+ await input2({
2096
+ message: dim("Press Enter to continue..."),
2097
+ default: ""
2098
+ });
2099
+ }
2100
+ async function promptOpenConfigFile(configPath) {
2101
+ console.log();
2102
+ const openChoice = await select({
2103
+ message: "Open config file?",
2104
+ choices: [
2105
+ {
2106
+ name: "📝 Open in Cursor",
2107
+ value: "cursor",
2108
+ description: "Open in Cursor IDE"
2109
+ },
2110
+ {
2111
+ name: "📝 Open in VS Code",
2112
+ value: "vscode",
2113
+ description: "Open in Visual Studio Code"
2114
+ },
2115
+ {
2116
+ name: "📄 Open in default app",
2117
+ value: "default",
2118
+ description: "Open with system default application"
2119
+ },
2120
+ new Separator(),
2121
+ {
2122
+ name: `${c("dim", "← Skip")}`,
2123
+ value: "no"
2124
+ }
2125
+ ],
2126
+ pageSize: 10,
2127
+ loop: false,
2128
+ theme: {
2129
+ prefix: " ",
2130
+ style: {
2131
+ highlight: (text) => c("cyan", text),
2132
+ message: (text) => bold(text)
2133
+ }
2134
+ }
2135
+ });
2136
+ if (openChoice === "no") {
2137
+ return;
2138
+ }
2139
+ const success = openInEditor(configPath, openChoice);
2140
+ if (success) {
2141
+ console.log(` ${c("green", "✓")} Opened ${configPath}`);
2142
+ } else {
2143
+ console.log(` ${c("yellow", "⚠")} Could not open file automatically`);
2144
+ console.log(` ${dim("Try opening manually:")} ${c("cyan", configPath)}`);
2145
+ }
2146
+ console.log();
2147
+ }
2148
+ async function editBooleanOption(option, currentValue) {
2149
+ const isEnabled = parseBooleanValue(currentValue);
2150
+ const currentStatus = isEnabled ? c("green", "enabled") : c("dim", "disabled");
2151
+ console.log();
2152
+ console.log(` ${bold(option.name)}`);
2153
+ console.log(` ${dim(option.description)}`);
2154
+ console.log(` ${dim("Current:")} ${currentStatus}`);
2155
+ console.log();
2156
+ const choice = await select({
2157
+ message: `${option.name}:`,
2158
+ choices: [
2159
+ {
2160
+ name: `${c("green", "✓")} Enable`,
2161
+ value: "enable"
2162
+ },
2163
+ {
2164
+ name: `${c("yellow", "○")} Disable`,
2165
+ value: "disable"
2166
+ },
2167
+ new Separator(),
2168
+ {
2169
+ name: `${c("dim", "← Cancel")}`,
2170
+ value: "cancel"
2171
+ }
2172
+ ],
2173
+ loop: false
2174
+ });
2175
+ if (choice === "cancel") return null;
2176
+ return choice === "enable" ? "1" : "false";
2177
+ }
2178
+ async function editStringOption(option, currentValue) {
2179
+ const displayCurrent = currentValue && currentValue !== option.defaultValue ? c("cyan", currentValue) : c("dim", currentValue || option.defaultValue);
2180
+ console.log();
2181
+ console.log(` ${bold(option.name)}`);
2182
+ console.log(` ${dim(option.description)}`);
2183
+ console.log(` ${dim("Current:")} ${displayCurrent}`);
2184
+ console.log(` ${dim("Default:")} ${option.defaultValue}`);
2185
+ console.log(` ${dim("(Leave empty and press Enter to cancel)")}`);
2186
+ console.log();
2187
+ const newValue = await input({
2188
+ message: `${option.name}:`,
2189
+ default: "",
2190
+ validate: (value) => {
2191
+ if (!value.trim()) {
2192
+ return true;
2193
+ }
2194
+ if (option.validation?.pattern && !option.validation.pattern.test(value)) {
2195
+ return "Invalid format";
2196
+ }
2197
+ return true;
2198
+ }
2199
+ });
2200
+ if (!newValue.trim()) {
2201
+ return null;
2202
+ }
2203
+ return newValue === option.defaultValue ? "" : newValue;
2204
+ }
2205
+ async function editNumberOption(option, currentValue) {
2206
+ const displayCurrent = currentValue && currentValue !== option.defaultValue ? c("cyan", currentValue) : c("dim", currentValue || option.defaultValue);
2207
+ console.log();
2208
+ console.log(` ${bold(option.name)}`);
2209
+ console.log(` ${dim(option.description)}`);
2210
+ console.log(` ${dim("Current:")} ${displayCurrent}`);
2211
+ if (option.validation?.min !== void 0 || option.validation?.max !== void 0) {
2212
+ const min = option.validation?.min ?? 0;
2213
+ const max = option.validation?.max ?? Infinity;
2214
+ console.log(` ${dim("Range:")} ${min} - ${max === Infinity ? "∞" : max}`);
2215
+ }
2216
+ console.log(` ${dim("Default:")} ${option.defaultValue}`);
2217
+ console.log(` ${dim("(Leave empty and press Enter to cancel)")}`);
2218
+ console.log();
2219
+ const newValue = await input({
2220
+ message: `${option.name}:`,
2221
+ default: "",
2222
+ validate: (value) => {
2223
+ if (!value.trim()) {
2224
+ return true;
2225
+ }
2226
+ const num = parseInt(value, 10);
2227
+ if (isNaN(num)) {
2228
+ return "Please enter a valid number";
2229
+ }
2230
+ if (option.validation?.min !== void 0 && num < option.validation.min) {
2231
+ return `Minimum value is ${option.validation.min}`;
2232
+ }
2233
+ if (option.validation?.max !== void 0 && num > option.validation.max) {
2234
+ return `Maximum value is ${option.validation.max}`;
2235
+ }
2236
+ return true;
2237
+ }
2238
+ });
2239
+ if (!newValue.trim()) {
2240
+ return null;
2241
+ }
2242
+ return newValue === option.defaultValue ? "" : newValue;
2243
+ }
2244
+ async function editArrayOption(option, currentValue) {
2245
+ const allTools = getAllTools();
2246
+ const currentTools = currentValue ? currentValue.split(",").map((t) => t.trim()).filter(Boolean) : [];
2247
+ const currentDisplay = currentTools.length > 0 ? currentTools.join(", ") : option.id === "toolsToRun" ? c("dim", "(all tools)") : c("dim", "(none)");
2248
+ console.log();
2249
+ console.log(` ${bold(option.name)}`);
2250
+ console.log(` ${dim(option.description)}`);
2251
+ console.log(` ${dim("Current:")} ${currentDisplay}`);
2252
+ console.log();
2253
+ const action = await select({
2254
+ message: `${option.name}:`,
2255
+ choices: [
2256
+ {
2257
+ name: "📝 Select tools",
2258
+ value: "select",
2259
+ description: "Choose which tools to include"
2260
+ },
2261
+ {
2262
+ name: `${c("yellow", "↺")} Clear all`,
2263
+ value: "clear",
2264
+ description: option.id === "toolsToRun" ? "Reset to all tools enabled" : "Remove all tools from this list"
2265
+ },
2266
+ new Separator(),
2267
+ {
2268
+ name: `${c("dim", "← Cancel")}`,
2269
+ value: "cancel"
2270
+ }
2271
+ ],
2272
+ loop: false
2273
+ });
2274
+ if (action === "cancel") {
2275
+ return null;
2276
+ }
2277
+ if (action === "clear") {
2278
+ return "";
2279
+ }
2280
+ const choices = [];
2281
+ choices.push({
2282
+ name: c("blue", "── GitHub Tools ──"),
2283
+ value: "__separator_github__",
2284
+ disabled: true
2285
+ });
2286
+ for (const tool of allTools.filter((t) => t.category === "github")) {
2287
+ choices.push({
2288
+ name: `${tool.name} ${c("dim", `(${tool.id})`)}`,
2289
+ value: tool.id,
2290
+ checked: currentTools.includes(tool.id),
2291
+ description: tool.description
2292
+ });
2293
+ }
2294
+ choices.push({
2295
+ name: c("yellow", "── Local Tools ──"),
2296
+ value: "__separator_local__",
2297
+ disabled: true
2298
+ });
2299
+ for (const tool of allTools.filter((t) => t.category === "local")) {
2300
+ choices.push({
2301
+ name: `${tool.name} ${c("dim", `(${tool.id})`)}`,
2302
+ value: tool.id,
2303
+ checked: currentTools.includes(tool.id),
2304
+ description: tool.description
2305
+ });
2306
+ }
2307
+ console.log();
2308
+ console.log(` ${dim("Use Space to select/deselect, Enter to confirm")}`);
2309
+ const selected = await checkbox({
2310
+ message: `Select tools for ${option.name}:`,
2311
+ choices,
2312
+ pageSize: 15,
2313
+ loop: false,
2314
+ theme: {
2315
+ prefix: " ",
2316
+ style: {
2317
+ highlight: (text) => c("cyan", text),
2318
+ message: (text) => bold(text)
2319
+ }
2320
+ }
2321
+ });
2322
+ const validTools = selected.filter((t) => !t.startsWith("__separator"));
2323
+ return validTools.length > 0 ? validTools.join(",") : "";
2324
+ }
2325
+ async function showEditConfigMenu(env) {
2326
+ const choices = [];
2327
+ const booleanOptions = ALL_CONFIG_OPTIONS.filter((o) => o.type === "boolean");
2328
+ const stringOptions = ALL_CONFIG_OPTIONS.filter((o) => o.type === "string");
2329
+ const numberOptions = ALL_CONFIG_OPTIONS.filter((o) => o.type === "number");
2330
+ const arrayOptions = ALL_CONFIG_OPTIONS.filter((o) => o.type === "array");
2331
+ if (booleanOptions.length > 0) {
2332
+ choices.push({
2333
+ name: c("dim", "── Features ──"),
2334
+ value: "__sep1__"
2335
+ });
2336
+ for (const option of booleanOptions) {
2337
+ const value = getCurrentValue(env, option);
2338
+ const displayValue = formatDisplayValue(option, value);
2339
+ choices.push({
2340
+ name: `${option.name}: ${displayValue}`,
2341
+ value: option.id,
2342
+ description: option.description
2343
+ });
2344
+ }
2345
+ }
2346
+ if (stringOptions.length > 0) {
2347
+ choices.push({
2348
+ name: c("dim", "── Endpoints ──"),
2349
+ value: "__sep2__"
2350
+ });
2351
+ for (const option of stringOptions) {
2352
+ const value = getCurrentValue(env, option);
2353
+ const displayValue = formatDisplayValue(option, value);
2354
+ choices.push({
2355
+ name: `${option.name}: ${displayValue}`,
2356
+ value: option.id,
2357
+ description: option.description
2358
+ });
2359
+ }
2360
+ }
2361
+ if (numberOptions.length > 0) {
2362
+ choices.push({
2363
+ name: c("dim", "── Performance ──"),
2364
+ value: "__sep3__"
2365
+ });
2366
+ for (const option of numberOptions) {
2367
+ const value = getCurrentValue(env, option);
2368
+ const displayValue = formatDisplayValue(option, value);
2369
+ choices.push({
2370
+ name: `${option.name}: ${displayValue}`,
2371
+ value: option.id,
2372
+ description: option.description
2373
+ });
2374
+ }
2375
+ }
2376
+ if (arrayOptions.length > 0) {
2377
+ choices.push({
2378
+ name: c("dim", "── Tool Selection ──"),
2379
+ value: "__sep4__"
2380
+ });
2381
+ for (const option of arrayOptions) {
2382
+ const value = getCurrentValue(env, option);
2383
+ const displayValue = formatDisplayValue(option, value);
2384
+ choices.push({
2385
+ name: `${option.name}: ${displayValue}`,
2386
+ value: option.id,
2387
+ description: option.description
2388
+ });
2389
+ }
2390
+ }
2391
+ choices.push({
2392
+ name: c("dim", "── Actions ──"),
2393
+ value: "__sep5__"
2394
+ });
2395
+ choices.push({
2396
+ name: `${c("green", "💾")} Save changes`,
2397
+ value: "save",
2398
+ description: "Save configuration and exit"
2399
+ });
2400
+ choices.push({
2401
+ name: `${c("yellow", "↺")} Reset to defaults`,
2402
+ value: "reset",
2403
+ description: "Clear all custom configuration"
2404
+ });
2405
+ choices.push({
2406
+ name: `${c("dim", "← Back")}`,
2407
+ value: "back"
2408
+ });
2409
+ const choice = await select({
2410
+ message: "Select option to configure:",
2411
+ choices,
2412
+ pageSize: 18,
2413
+ loop: false,
2414
+ theme: {
2415
+ prefix: " ",
2416
+ style: {
2417
+ highlight: (text) => c("cyan", text),
2418
+ message: (text) => bold(text)
2419
+ }
2420
+ }
2421
+ });
2422
+ return choice;
2423
+ }
2424
+ async function runEditConfigFlow() {
2425
+ const selection = await selectMCPClient();
2426
+ if (!selection) return;
2427
+ const { client, customPath } = selection;
2428
+ const clientInfo = MCP_CLIENTS[client];
2429
+ const configPath = customPath || getMCPConfigPath(client);
2430
+ const config = readMCPConfig(configPath);
2431
+ if (!config) {
2432
+ console.log();
2433
+ console.log(` ${c("red", "✗")} Failed to read config file: ${configPath}`);
2434
+ console.log();
2435
+ return;
2436
+ }
2437
+ if (!isOctocodeConfigured(config)) {
2438
+ console.log();
2439
+ console.log(
2440
+ ` ${c("yellow", "⚠")} Octocode is not configured for ${clientInfo.name}`
2441
+ );
2442
+ console.log(
2443
+ ` ${dim('Please install octocode first using "Install octocode-mcp".')}`
2444
+ );
2445
+ console.log();
2446
+ return;
2447
+ }
2448
+ console.log();
2449
+ console.log(` ${dim("Config file:")} ${c("cyan", configPath)}`);
2450
+ console.log(` ${dim("Client:")} ${clientInfo.name}`);
2451
+ console.log();
2452
+ const originalEnv = { ...config.mcpServers?.octocode?.env || {} };
2453
+ const workingEnv = { ...originalEnv };
2454
+ let editing = true;
2455
+ while (editing) {
2456
+ const choice = await showEditConfigMenu(workingEnv);
2457
+ if (choice.startsWith("__sep")) {
2458
+ continue;
2459
+ }
2460
+ switch (choice) {
2461
+ case "save": {
2462
+ const hasChanges = JSON.stringify(originalEnv) !== JSON.stringify(workingEnv);
2463
+ if (!hasChanges) {
2464
+ console.log();
2465
+ console.log(` ${dim("No changes to save.")}`);
2466
+ console.log();
2467
+ editing = false;
2468
+ break;
2469
+ }
2470
+ const spinner = new Spinner("Saving configuration...").start();
2471
+ const cleanEnv = {};
2472
+ for (const [key, value] of Object.entries(workingEnv)) {
2473
+ if (value && value !== "") {
2474
+ cleanEnv[key] = value;
2475
+ }
2476
+ }
2477
+ const updatedConfig = {
2478
+ ...config,
2479
+ mcpServers: {
2480
+ ...config.mcpServers,
2481
+ octocode: {
2482
+ ...config.mcpServers.octocode,
2483
+ env: Object.keys(cleanEnv).length > 0 ? cleanEnv : void 0
2484
+ }
2485
+ }
2486
+ };
2487
+ if (updatedConfig.mcpServers?.octocode?.env && Object.keys(updatedConfig.mcpServers.octocode.env).length === 0) {
2488
+ delete updatedConfig.mcpServers.octocode.env;
2489
+ }
2490
+ const result = writeMCPConfig(configPath, updatedConfig);
2491
+ if (result.success) {
2492
+ spinner.succeed("Configuration saved!");
2493
+ console.log();
2494
+ console.log(` ${c("green", "✓")} Config saved to: ${configPath}`);
2495
+ if (result.backupPath) {
2496
+ console.log(` ${dim("Backup:")} ${result.backupPath}`);
2497
+ }
2498
+ console.log();
2499
+ console.log(
2500
+ ` ${bold("Note:")} Restart ${clientInfo.name} for changes to take effect.`
2501
+ );
2502
+ await promptOpenConfigFile(configPath);
2503
+ } else {
2504
+ spinner.fail("Failed to save configuration");
2505
+ console.log();
2506
+ console.log(` ${c("red", "✗")} ${result.error || "Unknown error"}`);
2507
+ console.log();
2508
+ }
2509
+ editing = false;
2510
+ break;
2511
+ }
2512
+ case "reset": {
2513
+ const confirmReset = await confirm({
2514
+ message: "Reset all configuration to defaults?",
2515
+ default: false
2516
+ });
2517
+ if (confirmReset) {
2518
+ for (const key of Object.keys(workingEnv)) {
2519
+ delete workingEnv[key];
2520
+ }
2521
+ console.log(` ${c("yellow", "↺")} Configuration reset to defaults`);
2522
+ }
2523
+ break;
2524
+ }
2525
+ case "back": {
2526
+ const hasUnsavedChanges = JSON.stringify(originalEnv) !== JSON.stringify(workingEnv);
2527
+ if (hasUnsavedChanges) {
2528
+ const confirmDiscard = await confirm({
2529
+ message: "Discard unsaved changes?",
2530
+ default: false
2531
+ });
2532
+ if (!confirmDiscard) {
2533
+ break;
2534
+ }
2535
+ }
2536
+ editing = false;
2537
+ break;
2538
+ }
2539
+ default: {
2540
+ const option = ALL_CONFIG_OPTIONS.find((o) => o.id === choice);
2541
+ if (!option) break;
2542
+ const currentValue = getCurrentValue(workingEnv, option);
2543
+ let newValue = null;
2544
+ switch (option.type) {
2545
+ case "boolean":
2546
+ newValue = await editBooleanOption(option, currentValue);
2547
+ break;
2548
+ case "string":
2549
+ newValue = await editStringOption(option, currentValue);
2550
+ break;
2551
+ case "number":
2552
+ newValue = await editNumberOption(option, currentValue);
2553
+ break;
2554
+ case "array":
2555
+ newValue = await editArrayOption(option, currentValue);
2556
+ break;
2557
+ }
2558
+ if (newValue !== null) {
2559
+ if (newValue === "" || newValue === option.defaultValue) {
2560
+ delete workingEnv[option.envVar];
2561
+ } else {
2562
+ workingEnv[option.envVar] = newValue;
2563
+ }
2564
+ }
2565
+ break;
2566
+ }
2567
+ }
2568
+ }
2569
+ }
2570
+ async function showCurrentJsonConfig() {
2571
+ const selection = await selectMCPClient();
2572
+ if (!selection) return;
2573
+ const { client, customPath } = selection;
2574
+ const clientInfo = MCP_CLIENTS[client];
2575
+ const configPath = customPath || getMCPConfigPath(client);
2576
+ const config = readMCPConfig(configPath);
2577
+ if (!config) {
2578
+ console.log();
2579
+ console.log(` ${c("red", "✗")} Failed to read config file: ${configPath}`);
2580
+ console.log();
2581
+ return;
2582
+ }
2583
+ if (!isOctocodeConfigured(config)) {
2584
+ console.log();
2585
+ console.log(
2586
+ ` ${c("yellow", "⚠")} Octocode is not configured for ${clientInfo.name}`
2587
+ );
2588
+ console.log(
2589
+ ` ${dim('Please install octocode first using "Install octocode-mcp".')}`
2590
+ );
2591
+ console.log();
2592
+ return;
2593
+ }
2594
+ const octocodeConfig = config.mcpServers?.octocode;
2595
+ console.log();
2596
+ console.log(c("blue", "━".repeat(66)));
2597
+ console.log(` 📄 ${bold("Current Octocode Configuration")}`);
2598
+ console.log(c("blue", "━".repeat(66)));
2599
+ console.log();
2600
+ console.log(` ${dim("Client:")} ${c("cyan", clientInfo.name)}`);
2601
+ console.log(` ${dim("Config file:")} ${c("cyan", configPath)}`);
2602
+ console.log();
2603
+ console.log(` ${bold("JSON Configuration:")}`);
2604
+ console.log();
2605
+ const jsonString = JSON.stringify({ octocode: octocodeConfig }, null, 2);
2606
+ const lines = jsonString.split("\n");
2607
+ for (const line of lines) {
2608
+ const highlighted = line.replace(/"([^"]+)":/g, `${c("cyan", '"$1"')}:`).replace(/: "([^"]+)"/g, `: ${c("green", '"$1"')}`).replace(/: (\d+)/g, `: ${c("yellow", "$1")}`).replace(/: (true|false)/g, `: ${c("magenta", "$1")}`);
2609
+ console.log(` ${highlighted}`);
2610
+ }
2611
+ console.log();
2612
+ console.log(c("blue", "━".repeat(66)));
2613
+ await promptOpenConfigFile(configPath);
2614
+ }
2615
+ function getExampleValue(option) {
2616
+ switch (option.id) {
2617
+ case "enableLocal":
2618
+ return "ENABLE_LOCAL=1";
2619
+ case "githubApiUrl":
2620
+ return "GITHUB_API_URL=https://github.mycompany.com/api/v3";
2621
+ case "toolsToRun":
2622
+ return "TOOLS_TO_RUN=githubSearchCode,githubGetFileContent";
2623
+ case "enableTools":
2624
+ return "ENABLE_TOOLS=localSearchCode,localFindFiles";
2625
+ case "disableTools":
2626
+ return "DISABLE_TOOLS=githubSearchPullRequests";
2627
+ case "requestTimeout":
2628
+ return "REQUEST_TIMEOUT=60000";
2629
+ case "maxRetries":
2630
+ return "MAX_RETRIES=5";
2631
+ default:
2632
+ return `${option.envVar}=${option.defaultValue}`;
2633
+ }
2634
+ }
2635
+ function getDisplayDefault(option) {
2636
+ if (option.type === "array") {
2637
+ return option.id === "toolsToRun" ? "(all tools)" : "(none)";
2638
+ }
2639
+ return option.defaultValue;
2640
+ }
2641
+ function showConfigInfo() {
2642
+ console.log();
2643
+ console.log(` ${bold("All Available Configuration Options")}`);
2644
+ console.log();
2645
+ console.log(
2646
+ ` ${dim("These options can be set as environment variables in your MCP config.")}`
2647
+ );
2648
+ console.log(
2649
+ ` ${dim('Add them to the "env" object in your octocode server configuration.')}`
2650
+ );
2651
+ console.log();
2652
+ console.log(` ${dim("Example config:")}`);
2653
+ console.log(` ${dim("{")}
2654
+ ${dim(' "mcpServers": {')}
2655
+ ${dim(' "octocode": {')}
2656
+ ${dim(' "command": "npx",')}
2657
+ ${dim(' "args": ["octocode-mcp@latest"],')}
2658
+ ${c("green", ' "env": { "ENABLE_LOCAL": "1" }')}
2659
+ ${dim(" }")}
2660
+ ${dim(" }")}
2661
+ ${dim("}")}`);
2662
+ console.log();
2663
+ console.log(c("blue", "━".repeat(66)));
2664
+ console.log();
2665
+ for (const option of ALL_CONFIG_OPTIONS) {
2666
+ const typeColor = option.type === "boolean" ? "green" : option.type === "number" ? "yellow" : option.type === "array" ? "magenta" : "cyan";
2667
+ console.log(` ${c("cyan", option.envVar)} ${dim(`(${option.type})`)}`);
2668
+ console.log(` ${option.description}`);
2669
+ console.log(` ${dim("Default:")} ${getDisplayDefault(option)}`);
2670
+ console.log(
2671
+ ` ${dim("Example:")} ${c(typeColor, getExampleValue(option))}`
2672
+ );
2673
+ console.log();
2674
+ }
2675
+ }
2676
+ function getSkillsState() {
2677
+ const srcDir = getSkillsSourceDir$1();
2678
+ const destDir = getSkillsDestDir$1();
2679
+ if (!dirExists(srcDir)) {
2680
+ return {
2681
+ sourceExists: false,
2682
+ destDir,
2683
+ skills: [],
2684
+ installedCount: 0,
2685
+ notInstalledCount: 0,
2686
+ allInstalled: false,
2687
+ hasSkills: false
2688
+ };
2689
+ }
2690
+ const availableSkills = listSubdirectories(srcDir).filter(
2691
+ (name) => !name.startsWith(".")
2692
+ );
2693
+ const skills = availableSkills.map((skill) => ({
2694
+ name: skill,
2695
+ installed: dirExists(path.join(destDir, skill)),
2696
+ srcPath: path.join(srcDir, skill),
2697
+ destPath: path.join(destDir, skill)
2698
+ }));
2699
+ const installedCount = skills.filter((s) => s.installed).length;
2700
+ const notInstalledCount = skills.filter((s) => !s.installed).length;
2701
+ return {
2702
+ sourceExists: true,
2703
+ destDir,
2704
+ skills,
2705
+ installedCount,
2706
+ notInstalledCount,
2707
+ allInstalled: notInstalledCount === 0 && skills.length > 0,
2708
+ hasSkills: skills.length > 0
2709
+ };
2710
+ }
2711
+ function getOctocodeState() {
2712
+ const allClients = getAllClientInstallStatus();
2713
+ const installedClients = allClients.filter((c2) => c2.octocodeInstalled);
2714
+ const availableClients = allClients.filter(
2715
+ (c2) => c2.configExists && !c2.octocodeInstalled
2716
+ );
2717
+ return {
2718
+ installedClients,
2719
+ availableClients,
2720
+ isInstalled: installedClients.length > 0,
2721
+ hasMoreToInstall: availableClients.length > 0
2722
+ };
2723
+ }
2724
+ function getAppState() {
2725
+ return {
2726
+ octocode: getOctocodeState(),
2727
+ skills: getSkillsState(),
2728
+ currentClient: detectCurrentClient(),
2729
+ githubAuth: checkGitHubAuth()
2730
+ };
2731
+ }
2732
+ function getClientNames(clients) {
2733
+ return clients.map((c2) => MCP_CLIENTS[c2.client]?.name || c2.client).join(", ");
2734
+ }
2735
+ function formatPath(p) {
2736
+ if (p.startsWith(HOME)) {
2737
+ return "~" + p.slice(HOME.length);
2738
+ }
2739
+ return p;
2740
+ }
2741
+ function buildSkillsMenuItem(skills) {
2742
+ if (!skills.sourceExists || !skills.hasSkills) {
2743
+ return {
2744
+ name: "📚 Skills",
2745
+ value: "skills",
2746
+ description: "No skills available"
2747
+ };
2748
+ }
2749
+ if (skills.allInstalled) {
2750
+ return {
2751
+ name: `📚 Skills ${c("green", "✓")}`,
2752
+ value: "skills",
2753
+ description: formatPath(skills.destDir)
2754
+ };
2755
+ }
2756
+ if (skills.installedCount > 0) {
2757
+ return {
2758
+ name: "📚 Skills",
2759
+ value: "skills",
2760
+ description: `${skills.installedCount} installed, ${skills.notInstalledCount} available`
2761
+ };
2762
+ }
2763
+ return {
2764
+ name: "📚 Install Skills",
2765
+ value: "skills",
2766
+ description: "Install Octocode skills for Claude Code"
2767
+ };
2768
+ }
2769
+ async function showMainMenu(state) {
2770
+ if (state.octocode.isInstalled) {
2771
+ const names = getClientNames(state.octocode.installedClients);
2772
+ console.log(` ${c("green", "✓")} Installed in: ${c("cyan", names)}`);
2773
+ }
2774
+ if (state.githubAuth.authenticated) {
2775
+ console.log(
2776
+ ` ${c("green", "✓")} GitHub: ${c("cyan", state.githubAuth.username || "authenticated")}`
2777
+ );
2778
+ } else if (state.githubAuth.installed) {
2779
+ console.log(
2780
+ ` ${c("yellow", "⚠")} GitHub: ${c("yellow", "not authenticated")}`
2781
+ );
2782
+ } else {
2783
+ console.log(
2784
+ ` ${c("yellow", "⚠")} GitHub CLI: ${c("yellow", "not installed")}`
2785
+ );
2786
+ }
2787
+ const choices = [];
2788
+ if (state.octocode.isInstalled) {
2789
+ choices.push({
2790
+ name: "⚙️ Configure Options",
2791
+ value: "conf"
2792
+ });
2793
+ if (state.octocode.hasMoreToInstall) {
2794
+ const availableNames = getClientNames(state.octocode.availableClients);
2795
+ choices.push({
2796
+ name: "📦 Install to more clients",
2797
+ value: "install",
2798
+ description: `Available: ${availableNames}`
2799
+ });
2800
+ }
2801
+ } else {
2802
+ choices.push({
2803
+ name: "📦 Install octocode-mcp",
2804
+ value: "install",
2805
+ description: "Install MCP server for Cursor, Claude Desktop, and more"
2806
+ });
2807
+ }
2808
+ choices.push(buildSkillsMenuItem(state.skills));
2809
+ choices.push(
2810
+ new Separator()
2811
+ );
2812
+ choices.push({
2813
+ name: "🚪 Exit",
2814
+ value: "exit",
2815
+ description: "Quit the application"
2816
+ });
2817
+ choices.push(
2818
+ new Separator(" ")
2819
+ );
2820
+ choices.push(
2821
+ new Separator(
2822
+ ` ${c("yellow", "For checking node status in your system use")} ${c("cyan", "npx node-doctor")}`
2823
+ )
2824
+ );
2825
+ choices.push(
2826
+ new Separator(
2827
+ c("magenta", ` ─── 🔍🐙 ${bold("https://octocode.ai")} ───`)
2828
+ )
2829
+ );
2830
+ console.log();
2831
+ const choice = await select({
2832
+ message: "What would you like to do?",
2833
+ choices,
2834
+ pageSize: 10,
2835
+ loop: false,
2836
+ theme: {
2837
+ prefix: " ",
2838
+ style: {
2839
+ highlight: (text) => c("magenta", text),
2840
+ message: (text) => bold(text)
2841
+ }
2842
+ }
2843
+ });
2844
+ return choice;
2845
+ }
2846
+ function getSkillsSourceDir$1() {
2847
+ const __filename = fileURLToPath(import.meta.url);
2848
+ const __dirname = path.dirname(__filename);
2849
+ return path.resolve(__dirname, "..", "skills");
2850
+ }
2851
+ function getSkillsDestDir$1() {
2852
+ return path.join(HOME, ".claude", "skills");
2853
+ }
2854
+ async function pressEnterToContinue() {
2855
+ console.log();
2856
+ await input({
2857
+ message: dim("Press Enter to continue..."),
2858
+ default: ""
2859
+ });
2860
+ }
2861
+ async function showSkillsMenu(hasUninstalled) {
2862
+ const choices = [];
2863
+ if (hasUninstalled) {
2864
+ choices.push({
2865
+ name: "📥 Install skills",
2866
+ value: "install",
2867
+ description: "Install Octocode skills to Claude Code"
2868
+ });
2869
+ }
2870
+ choices.push({
2871
+ name: "📋 View skills status",
2872
+ value: "view",
2873
+ description: "Show installed and available skills"
2874
+ });
2875
+ choices.push(
2876
+ new Separator()
2877
+ );
2878
+ choices.push({
2879
+ name: `${c("dim", "← Back to main menu")}`,
2880
+ value: "back"
2881
+ });
2882
+ const choice = await select({
2883
+ message: "Skills Options:",
2884
+ choices,
2885
+ pageSize: 10,
2886
+ loop: false,
2887
+ theme: {
2888
+ prefix: " ",
2889
+ style: {
2890
+ highlight: (text) => c("magenta", text),
2891
+ message: (text) => bold(text)
2892
+ }
2893
+ }
2894
+ });
2895
+ return choice;
2896
+ }
2897
+ function getSkillsInfo() {
2898
+ const srcDir = getSkillsSourceDir$1();
2899
+ const destDir = getSkillsDestDir$1();
2900
+ if (!dirExists(srcDir)) {
2901
+ return {
2902
+ srcDir,
2903
+ destDir,
2904
+ skillsStatus: [],
2905
+ notInstalled: [],
2906
+ sourceExists: false
2907
+ };
2908
+ }
2909
+ const availableSkills = listSubdirectories(srcDir).filter(
2910
+ (name) => !name.startsWith(".")
2911
+ );
2912
+ const skillsStatus = availableSkills.map((skill) => ({
2913
+ name: skill,
2914
+ installed: dirExists(path.join(destDir, skill)),
2915
+ srcPath: path.join(srcDir, skill),
2916
+ destPath: path.join(destDir, skill)
2917
+ }));
2918
+ const notInstalled = skillsStatus.filter((s) => !s.installed);
2919
+ return { srcDir, destDir, skillsStatus, notInstalled, sourceExists: true };
2920
+ }
2921
+ function showSkillsStatus(info) {
2922
+ const { destDir, skillsStatus, notInstalled } = info;
2923
+ if (skillsStatus.length === 0) {
2924
+ console.log(` ${dim("No skills available.")}`);
2925
+ console.log();
2926
+ return;
2927
+ }
2928
+ console.log(` ${bold("Skills:")}`);
2929
+ console.log();
2930
+ for (const skill of skillsStatus) {
2931
+ if (skill.installed) {
2932
+ console.log(
2933
+ ` ${c("green", "✓")} ${skill.name} - ${c("green", "installed")}`
2934
+ );
2935
+ } else {
2936
+ console.log(
2937
+ ` ${c("yellow", "○")} ${skill.name} - ${dim("not installed")}`
2938
+ );
2939
+ }
2940
+ }
2941
+ console.log();
2942
+ console.log(` ${bold("Installation path:")}`);
2943
+ console.log(` ${c("cyan", destDir)}`);
2944
+ console.log();
2945
+ if (notInstalled.length === 0) {
2946
+ console.log(` ${c("green", "✓")} All skills are installed!`);
2947
+ } else {
2948
+ console.log(
2949
+ ` ${c("yellow", "ℹ")} ${notInstalled.length} skill(s) not installed`
2950
+ );
2951
+ }
2952
+ console.log();
2953
+ }
2954
+ async function installSkills(info) {
2955
+ const { destDir, notInstalled } = info;
2956
+ if (notInstalled.length === 0) {
2957
+ console.log(` ${c("green", "✓")} All skills are already installed!`);
2958
+ console.log();
2959
+ console.log(` ${bold("Installation path:")}`);
2960
+ console.log(` ${c("cyan", destDir)}`);
2961
+ console.log();
2962
+ await pressEnterToContinue();
2963
+ return true;
2964
+ }
2965
+ console.log(` ${bold("Skills to install:")}`);
2966
+ console.log();
2967
+ for (const skill of notInstalled) {
2968
+ console.log(` ${c("yellow", "○")} ${skill.name}`);
2969
+ }
2970
+ console.log();
2971
+ console.log(` ${bold("Installation path:")}`);
2972
+ console.log(` ${c("cyan", destDir)}`);
2973
+ console.log();
2974
+ const choice = await select({
2975
+ message: `Install ${notInstalled.length} skill(s)?`,
2976
+ choices: [
2977
+ {
2978
+ name: `${c("green", "✓")} Yes, install skills`,
2979
+ value: "install"
2980
+ },
2981
+ new Separator(),
2982
+ {
2983
+ name: `${c("dim", "← Back to skills menu")}`,
2984
+ value: "back"
2985
+ }
2986
+ ],
2987
+ loop: false
2988
+ });
2989
+ if (choice === "back") {
2990
+ return false;
2991
+ }
2992
+ console.log();
2993
+ const spinner = new Spinner("Installing skills...").start();
2994
+ let installedCount = 0;
2995
+ const failed = [];
2996
+ for (const skill of notInstalled) {
2997
+ if (copyDirectory(skill.srcPath, skill.destPath)) {
2998
+ installedCount++;
2999
+ } else {
3000
+ failed.push(skill.name);
3001
+ }
3002
+ }
3003
+ if (failed.length === 0) {
3004
+ spinner.succeed("Skills installed!");
3005
+ } else {
3006
+ spinner.warn("Some skills failed to install");
3007
+ }
3008
+ console.log();
3009
+ if (installedCount > 0) {
3010
+ console.log(` ${c("green", "✓")} Installed ${installedCount} skill(s)`);
3011
+ console.log(` ${dim("Location:")} ${c("cyan", destDir)}`);
3012
+ }
3013
+ if (failed.length > 0) {
3014
+ console.log(` ${c("red", "✗")} Failed: ${failed.join(", ")}`);
3015
+ }
3016
+ console.log();
3017
+ if (installedCount > 0) {
3018
+ console.log(` ${bold("Skills are now available in Claude Code!")}`);
3019
+ console.log();
3020
+ }
3021
+ await pressEnterToContinue();
3022
+ return true;
3023
+ }
3024
+ async function runSkillsFlow() {
3025
+ await loadInquirer();
3026
+ console.log();
3027
+ console.log(c("blue", "━".repeat(66)));
3028
+ console.log(` 📚 ${bold("Octocode Skills for Claude Code")}`);
3029
+ console.log(c("blue", "━".repeat(66)));
3030
+ console.log();
3031
+ let info = getSkillsInfo();
3032
+ if (!info.sourceExists) {
3033
+ console.log(` ${c("yellow", "⚠")} Skills source directory not found.`);
3034
+ console.log(` ${dim("This may happen if running from source.")}`);
3035
+ console.log();
3036
+ await pressEnterToContinue();
3037
+ return;
3038
+ }
3039
+ if (info.skillsStatus.length === 0) {
3040
+ console.log(` ${dim("No skills available.")}`);
3041
+ console.log();
3042
+ await pressEnterToContinue();
3043
+ return;
3044
+ }
3045
+ let inSkillsMenu = true;
3046
+ while (inSkillsMenu) {
3047
+ info = getSkillsInfo();
3048
+ const choice = await showSkillsMenu(info.notInstalled.length > 0);
3049
+ switch (choice) {
3050
+ case "install": {
3051
+ const installed = await installSkills(info);
3052
+ if (installed) {
3053
+ continue;
3054
+ }
3055
+ break;
3056
+ }
3057
+ case "view":
3058
+ showSkillsStatus(info);
3059
+ await pressEnterToContinue();
3060
+ break;
3061
+ case "back":
3062
+ default:
3063
+ inSkillsMenu = false;
3064
+ break;
3065
+ }
3066
+ }
3067
+ }
3068
+ async function handleMenuChoice(choice) {
3069
+ switch (choice) {
3070
+ case "install":
3071
+ await runInstallFlow();
3072
+ return true;
3073
+ case "skills":
3074
+ await runSkillsFlow();
3075
+ return true;
3076
+ case "conf":
3077
+ await runConfigOptionsFlow();
3078
+ return true;
3079
+ case "exit":
3080
+ printGoodbye();
3081
+ return false;
3082
+ default:
3083
+ return true;
3084
+ }
3085
+ }
3086
+ async function runMenuLoop() {
3087
+ let firstRun = true;
3088
+ let running = true;
3089
+ while (running) {
3090
+ if (!firstRun) {
3091
+ clearScreen();
3092
+ printWelcome();
3093
+ }
3094
+ firstRun = false;
3095
+ const state = getAppState();
3096
+ const choice = await showMainMenu(state);
3097
+ running = await handleMenuChoice(choice);
3098
+ }
3099
+ }
3100
+ const OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["ide", "method", "output", "o"]);
3101
+ function parseArgs(argv = process.argv.slice(2)) {
3102
+ const result = {
3103
+ command: null,
3104
+ args: [],
3105
+ options: {}
3106
+ };
3107
+ let i = 0;
3108
+ while (i < argv.length) {
3109
+ const arg = argv[i];
3110
+ if (arg.startsWith("--")) {
3111
+ const [key, value] = arg.slice(2).split("=");
3112
+ if (value !== void 0) {
3113
+ result.options[key] = value;
3114
+ } else if (OPTIONS_WITH_VALUES.has(key) && i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
3115
+ result.options[key] = argv[i + 1];
3116
+ i++;
3117
+ } else {
3118
+ result.options[key] = true;
3119
+ }
3120
+ } else if (arg.startsWith("-") && arg.length > 1) {
3121
+ const flags = arg.slice(1);
3122
+ const lastFlag = flags[flags.length - 1];
3123
+ if (flags.length === 1 && OPTIONS_WITH_VALUES.has(lastFlag) && i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
3124
+ result.options[lastFlag] = argv[i + 1];
3125
+ i++;
3126
+ } else {
3127
+ for (const flag of flags) {
3128
+ result.options[flag] = true;
3129
+ }
3130
+ }
3131
+ } else if (!result.command) {
3132
+ result.command = arg;
3133
+ } else {
3134
+ result.args.push(arg);
3135
+ }
3136
+ i++;
3137
+ }
3138
+ return result;
3139
+ }
3140
+ function hasHelpFlag(args) {
3141
+ return Boolean(args.options["help"] || args.options["h"]);
3142
+ }
3143
+ function hasVersionFlag(args) {
3144
+ return Boolean(args.options["version"] || args.options["v"]);
3145
+ }
3146
+ function printNodeDoctorHintCLI() {
3147
+ console.log(
3148
+ ` ${dim("For deeper diagnostics:")} ${c("cyan", "npx node-doctor")}`
3149
+ );
3150
+ console.log();
3151
+ }
3152
+ const installCommand = {
3153
+ name: "install",
3154
+ aliases: ["i"],
3155
+ description: "Install octocode-mcp for an IDE",
3156
+ usage: "octocode install --ide <cursor|claude> --method <npx|direct>",
3157
+ options: [
3158
+ {
3159
+ name: "ide",
3160
+ description: "IDE to configure (cursor or claude)",
3161
+ hasValue: true
3162
+ },
3163
+ {
3164
+ name: "method",
3165
+ short: "m",
3166
+ description: "Installation method (npx or direct)",
3167
+ hasValue: true,
3168
+ default: "npx"
3169
+ },
3170
+ {
3171
+ name: "force",
3172
+ short: "f",
3173
+ description: "Overwrite existing configuration"
3174
+ }
3175
+ ],
3176
+ handler: async (args) => {
3177
+ const ide = args.options["ide"];
3178
+ const method = args.options["method"] || "npx";
3179
+ const force = Boolean(args.options["force"] || args.options["f"]);
3180
+ if (method === "npx") {
3181
+ const nodeCheck = checkNodeInPath();
3182
+ const npmCheck = checkNpmInPath();
3183
+ if (!nodeCheck.installed) {
3184
+ console.log();
3185
+ console.log(
3186
+ ` ${c("red", "✗")} Node.js is ${c("red", "not found in PATH")}`
3187
+ );
3188
+ console.log(
3189
+ ` ${dim("Node.js is required for npx installation method.")}`
3190
+ );
3191
+ console.log();
3192
+ printNodeDoctorHintCLI();
3193
+ process.exitCode = 1;
3194
+ return;
3195
+ }
3196
+ if (!npmCheck.installed) {
3197
+ console.log();
3198
+ console.log(
3199
+ ` ${c("yellow", "⚠")} npm is ${c("yellow", "not found in PATH")}`
3200
+ );
3201
+ console.log(` ${dim("npm is required for npx installation method.")}`);
3202
+ console.log();
3203
+ printNodeDoctorHintCLI();
3204
+ process.exitCode = 1;
3205
+ return;
3206
+ }
3207
+ }
3208
+ if (!ide) {
3209
+ const available = detectAvailableIDEs();
3210
+ console.log();
3211
+ console.log(
3212
+ ` ${c("red", "✗")} Missing required option: ${c("cyan", "--ide")}`
3213
+ );
3214
+ console.log();
3215
+ if (available.length > 0) {
3216
+ console.log(` ${bold("Available IDEs:")}`);
3217
+ for (const availableIde of available) {
3218
+ console.log(` ${c("cyan", "•")} ${availableIde}`);
3219
+ }
3220
+ } else {
3221
+ console.log(` ${c("yellow", "⚠")} No supported IDEs detected.`);
3222
+ console.log(` ${dim("Install Cursor or Claude Desktop first.")}`);
3223
+ }
3224
+ console.log();
3225
+ console.log(
3226
+ ` ${dim("Usage:")} octocode install --ide cursor --method npx`
3227
+ );
3228
+ console.log();
3229
+ process.exitCode = 1;
3230
+ return;
3231
+ }
3232
+ if (!["cursor", "claude"].includes(ide)) {
3233
+ console.log();
3234
+ console.log(` ${c("red", "✗")} Invalid IDE: ${ide}`);
3235
+ console.log(` ${dim("Supported:")} cursor, claude`);
3236
+ console.log();
3237
+ process.exitCode = 1;
3238
+ return;
3239
+ }
3240
+ if (!["npx", "direct"].includes(method)) {
3241
+ console.log();
3242
+ console.log(` ${c("red", "✗")} Invalid method: ${method}`);
3243
+ console.log(` ${dim("Supported:")} npx, direct`);
3244
+ console.log();
3245
+ process.exitCode = 1;
3246
+ return;
3247
+ }
3248
+ const preview = getInstallPreview(ide, method);
3249
+ if (preview.action === "override" && !force) {
3250
+ console.log();
3251
+ console.log(` ${c("yellow", "⚠")} Octocode is already configured.`);
3252
+ console.log(
3253
+ ` ${dim("Use")} ${c("cyan", "--force")} ${dim("to overwrite.")}`
3254
+ );
3255
+ console.log();
3256
+ process.exitCode = 1;
3257
+ return;
3258
+ }
3259
+ console.log();
3260
+ console.log(` ${bold("Installing octocode-mcp")}`);
3261
+ console.log(` ${dim("IDE:")} ${IDE_INFO[ide].name}`);
3262
+ console.log(` ${dim("Method:")} ${INSTALL_METHOD_INFO[method].name}`);
3263
+ console.log(` ${dim("Action:")} ${preview.action.toUpperCase()}`);
3264
+ console.log();
3265
+ const spinner = new Spinner("Writing configuration...").start();
3266
+ const result = installOctocode({ ide, method, force });
3267
+ if (result.success) {
3268
+ spinner.succeed("Installation complete!");
3269
+ console.log();
3270
+ console.log(
3271
+ ` ${c("green", "✓")} Config saved to: ${preview.configPath}`
3272
+ );
3273
+ if (result.backupPath) {
3274
+ console.log(` ${dim("Backup:")} ${result.backupPath}`);
3275
+ }
3276
+ console.log();
3277
+ console.log(
3278
+ ` ${bold("Next:")} Restart ${IDE_INFO[ide].name} to activate.`
3279
+ );
3280
+ console.log();
3281
+ } else {
3282
+ spinner.fail("Installation failed");
3283
+ console.log();
3284
+ if (result.error) {
3285
+ console.log(` ${c("red", "✗")} ${result.error}`);
3286
+ }
3287
+ console.log();
3288
+ process.exitCode = 1;
3289
+ }
3290
+ }
3291
+ };
3292
+ const authCommand = {
3293
+ name: "auth",
3294
+ aliases: ["a", "gh"],
3295
+ description: "Check GitHub CLI authentication status",
3296
+ usage: "octocode auth",
3297
+ handler: async () => {
3298
+ console.log();
3299
+ console.log(` ${bold("🔐 GitHub CLI Authentication")}`);
3300
+ console.log();
3301
+ const status = checkGitHubAuth();
3302
+ if (!status.installed) {
3303
+ console.log(
3304
+ ` ${c("red", "✗")} GitHub CLI is ${c("red", "not installed")}`
3305
+ );
3306
+ console.log();
3307
+ console.log(` ${bold("To install:")}`);
3308
+ console.log(` ${c("cyan", "→")} ${c("underscore", GH_CLI_URL)}`);
3309
+ console.log();
3310
+ process.exitCode = 1;
3311
+ return;
3312
+ }
3313
+ const version = getGitHubCLIVersion();
3314
+ console.log(
3315
+ ` ${c("green", "✓")} GitHub CLI installed` + (version ? dim(` (v${version})`) : "")
3316
+ );
3317
+ if (status.authenticated) {
3318
+ console.log(
3319
+ ` ${c("green", "✓")} Authenticated as ${c("cyan", status.username || "unknown")}`
3320
+ );
3321
+ console.log();
3322
+ } else {
3323
+ console.log(` ${c("yellow", "⚠")} ${c("yellow", "Not authenticated")}`);
3324
+ console.log();
3325
+ console.log(` ${bold("To authenticate:")}`);
3326
+ console.log(
3327
+ ` ${c("cyan", "→")} ${c("yellow", getAuthLoginCommand())}`
3328
+ );
3329
+ console.log();
3330
+ process.exitCode = 1;
3331
+ }
3332
+ }
3333
+ };
3334
+ function getSkillsSourceDir() {
3335
+ const __filename = fileURLToPath(import.meta.url);
3336
+ const __dirname = path.dirname(__filename);
3337
+ return path.resolve(__dirname, "..", "..", "skills");
3338
+ }
3339
+ function getSkillsDestDir() {
3340
+ return path.join(HOME, ".claude", "skills");
3341
+ }
3342
+ const skillsCommand = {
3343
+ name: "skills",
3344
+ aliases: ["sk"],
3345
+ description: "Install Octocode skills for Claude Code",
3346
+ usage: "octocode skills [install|list]",
3347
+ options: [
3348
+ {
3349
+ name: "force",
3350
+ short: "f",
3351
+ description: "Overwrite existing skills"
3352
+ }
3353
+ ],
3354
+ handler: async (args) => {
3355
+ const subcommand = args.args[0] || "list";
3356
+ const force = Boolean(args.options["force"] || args.options["f"]);
3357
+ const srcDir = getSkillsSourceDir();
3358
+ const destDir = getSkillsDestDir();
3359
+ if (!dirExists(srcDir)) {
3360
+ console.log();
3361
+ console.log(` ${c("red", "✗")} Skills directory not found`);
3362
+ console.log(` ${dim("Expected:")} ${srcDir}`);
3363
+ console.log();
3364
+ process.exitCode = 1;
3365
+ return;
3366
+ }
3367
+ const availableSkills = listSubdirectories(srcDir).filter(
3368
+ (name) => !name.startsWith(".")
3369
+ );
3370
+ if (subcommand === "list") {
3371
+ console.log();
3372
+ console.log(` ${bold("📚 Available Octocode Skills")}`);
3373
+ console.log();
3374
+ if (availableSkills.length === 0) {
3375
+ console.log(` ${dim("No skills available.")}`);
3376
+ } else {
3377
+ for (const skill of availableSkills) {
3378
+ const installed = dirExists(path.join(destDir, skill));
3379
+ const status = installed ? c("green", "✓ installed") : dim("not installed");
3380
+ console.log(` ${c("cyan", "•")} ${skill} ${status}`);
3381
+ }
3382
+ }
3383
+ console.log();
3384
+ console.log(` ${dim("To install:")} octocode skills install`);
3385
+ console.log(` ${dim("Destination:")} ${destDir}`);
3386
+ console.log();
3387
+ return;
3388
+ }
3389
+ if (subcommand === "install") {
3390
+ console.log();
3391
+ console.log(` ${bold("📦 Installing Octocode Skills")}`);
3392
+ console.log();
3393
+ if (availableSkills.length === 0) {
3394
+ console.log(` ${c("yellow", "⚠")} No skills to install.`);
3395
+ console.log();
3396
+ return;
3397
+ }
3398
+ const spinner = new Spinner("Installing skills...").start();
3399
+ let installed = 0;
3400
+ let skipped = 0;
3401
+ for (const skill of availableSkills) {
3402
+ const skillSrc = path.join(srcDir, skill);
3403
+ const skillDest = path.join(destDir, skill);
3404
+ if (dirExists(skillDest) && !force) {
3405
+ skipped++;
3406
+ continue;
3407
+ }
3408
+ if (copyDirectory(skillSrc, skillDest)) {
3409
+ installed++;
3410
+ }
3411
+ }
3412
+ spinner.succeed("Skills installation complete!");
3413
+ console.log();
3414
+ if (installed > 0) {
3415
+ console.log(
3416
+ ` ${c("green", "✓")} Installed ${installed} skill(s) to ${destDir}`
3417
+ );
3418
+ }
3419
+ if (skipped > 0) {
3420
+ console.log(
3421
+ ` ${c("yellow", "⚠")} Skipped ${skipped} existing skill(s)`
3422
+ );
3423
+ console.log(
3424
+ ` ${dim("Use")} ${c("cyan", "--force")} ${dim("to overwrite.")}`
3425
+ );
3426
+ }
3427
+ console.log();
3428
+ console.log(` ${bold("Skills are now available in Claude Code!")}`);
3429
+ console.log();
3430
+ return;
3431
+ }
3432
+ console.log();
3433
+ console.log(` ${c("red", "✗")} Unknown subcommand: ${subcommand}`);
3434
+ console.log(` ${dim("Usage:")} octocode skills [install|list]`);
3435
+ console.log();
3436
+ process.exitCode = 1;
3437
+ }
3438
+ };
3439
+ const commands = [
3440
+ installCommand,
3441
+ authCommand,
3442
+ skillsCommand
3443
+ ];
3444
+ function findCommand(name) {
3445
+ return commands.find((cmd) => cmd.name === name || cmd.aliases?.includes(name));
3446
+ }
3447
+ function showHelp() {
3448
+ console.log();
3449
+ console.log(
3450
+ ` ${c("magenta", bold("🔍🐙 Octocode CLI"))} - Install and configure octocode-mcp`
3451
+ );
3452
+ console.log();
3453
+ console.log(` ${bold("USAGE")}`);
3454
+ console.log(` ${c("magenta", "octocode")} [command] [options]`);
3455
+ console.log();
3456
+ console.log(` ${bold("COMMANDS")}`);
3457
+ console.log(
3458
+ ` ${c("magenta", "install")} Configure octocode-mcp for an IDE`
3459
+ );
3460
+ console.log(
3461
+ ` ${c("magenta", "auth")} Check GitHub CLI authentication status`
3462
+ );
3463
+ console.log();
3464
+ console.log(` ${bold("OPTIONS")}`);
3465
+ console.log(` ${c("cyan", "-h, --help")} Show this help message`);
3466
+ console.log(` ${c("cyan", "-v, --version")} Show version number`);
3467
+ console.log();
3468
+ console.log(` ${bold("EXAMPLES")}`);
3469
+ console.log(` ${dim("# Interactive mode")}`);
3470
+ console.log(` ${c("yellow", "octocode")}`);
3471
+ console.log();
3472
+ console.log(` ${dim("# Install for Cursor using npx")}`);
3473
+ console.log(
3474
+ ` ${c("yellow", "octocode install --ide cursor --method npx")}`
3475
+ );
3476
+ console.log();
3477
+ console.log(` ${dim("# Install for Claude Desktop using direct method")}`);
3478
+ console.log(
3479
+ ` ${c("yellow", "octocode install --ide claude --method direct")}`
3480
+ );
3481
+ console.log();
3482
+ console.log(` ${dim("# Check GitHub authentication")}`);
3483
+ console.log(` ${c("yellow", "octocode auth")}`);
3484
+ console.log();
3485
+ console.log(c("magenta", ` ─── 🔍🐙 ${bold("https://octocode.ai")} ───`));
3486
+ console.log();
3487
+ }
3488
+ function showCommandHelp(command) {
3489
+ console.log();
3490
+ console.log(` ${c("magenta", bold("🔍🐙 octocode " + command.name))}`);
3491
+ console.log();
3492
+ console.log(` ${command.description}`);
3493
+ console.log();
3494
+ if (command.usage) {
3495
+ console.log(` ${bold("USAGE")}`);
3496
+ console.log(` ${command.usage}`);
3497
+ console.log();
3498
+ }
3499
+ if (command.options && command.options.length > 0) {
3500
+ console.log(` ${bold("OPTIONS")}`);
3501
+ for (const opt of command.options) {
3502
+ const shortFlag = opt.short ? `-${opt.short}, ` : " ";
3503
+ const longFlag = `--${opt.name}`;
3504
+ const valueHint = opt.hasValue ? ` <value>` : "";
3505
+ const defaultHint = opt.default !== void 0 ? dim(` (default: ${opt.default})`) : "";
3506
+ console.log(
3507
+ ` ${c("cyan", shortFlag + longFlag + valueHint)}${defaultHint}`
3508
+ );
3509
+ console.log(` ${opt.description}`);
3510
+ }
3511
+ console.log();
3512
+ }
3513
+ }
3514
+ function showVersion() {
3515
+ const version = "1.0.0";
3516
+ console.log(`octocode-cli v${version}`);
3517
+ }
3518
+ async function runCLI(argv) {
3519
+ const args = parseArgs(argv);
3520
+ if (hasHelpFlag(args)) {
3521
+ if (args.command) {
3522
+ const cmd = findCommand(args.command);
3523
+ if (cmd) {
3524
+ showCommandHelp(cmd);
3525
+ return true;
3526
+ }
3527
+ }
3528
+ showHelp();
3529
+ return true;
3530
+ }
3531
+ if (hasVersionFlag(args)) {
3532
+ showVersion();
3533
+ return true;
3534
+ }
3535
+ if (!args.command) {
3536
+ return false;
3537
+ }
3538
+ const command = findCommand(args.command);
3539
+ if (!command) {
3540
+ console.log();
3541
+ console.log(` Unknown command: ${args.command}`);
3542
+ console.log(` Run 'octocode --help' to see available commands.`);
3543
+ console.log();
3544
+ process.exitCode = 1;
3545
+ return true;
3546
+ }
3547
+ if (hasHelpFlag(args)) {
3548
+ showCommandHelp(command);
3549
+ return true;
3550
+ }
3551
+ await command.handler(args);
3552
+ return true;
3553
+ }
3554
+ async function runInteractiveMode() {
3555
+ await loadInquirer();
3556
+ clearScreen();
3557
+ printWelcome();
3558
+ console.log(c("blue", "━".repeat(66)));
3559
+ console.log(` 🔍 ${bold("Environment Check")}`);
3560
+ console.log(c("blue", "━".repeat(66)));
3561
+ const envStatus = await checkNodeEnvironment();
3562
+ printNodeEnvironmentStatus(envStatus);
3563
+ printGitHubAuthStatus();
3564
+ if (hasEnvironmentIssues(envStatus)) {
3565
+ printNodeDoctorHint();
3566
+ }
3567
+ if (!envStatus.nodeInstalled) {
3568
+ console.log(
3569
+ ` ${c("red", "✗")} ${bold("Node.js is required to run octocode-mcp")}`
3570
+ );
3571
+ printNodeDoctorHint();
3572
+ printGoodbye();
3573
+ return;
3574
+ }
3575
+ await runMenuLoop();
3576
+ }
3577
+ async function main() {
3578
+ const handled = await runCLI();
3579
+ if (handled) {
3580
+ return;
3581
+ }
3582
+ await runInteractiveMode();
3583
+ }
3584
+ function handleTermination() {
3585
+ process.stdout.write("\x1B[?25h");
3586
+ console.log();
3587
+ console.log(dim(" Goodbye! 👋"));
3588
+ process.exit(0);
3589
+ }
3590
+ process.on("SIGINT", handleTermination);
3591
+ process.on("SIGTERM", handleTermination);
3592
+ main().catch((err) => {
3593
+ if (err?.name === "ExitPromptError") {
3594
+ console.log();
3595
+ console.log(dim(" Goodbye! 👋"));
3596
+ process.exit(0);
3597
+ }
3598
+ console.error("Error:", err);
3599
+ process.exit(1);
3600
+ });