@corbat-tech/coco 2.25.7 → 2.25.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,23 +1,23 @@
1
- import * as fs16 from 'fs/promises';
2
- import fs16__default, { readFile, access, readdir } from 'fs/promises';
3
- import * as path17 from 'path';
4
- import path17__default, { dirname, join, basename, resolve } from 'path';
5
- import { randomUUID } from 'crypto';
6
- import 'http';
7
- import { fileURLToPath } from 'url';
1
+ import { Logger } from 'tslog';
8
2
  import * as fs4 from 'fs';
9
3
  import fs4__default, { readFileSync, constants } from 'fs';
4
+ import * as path17 from 'path';
5
+ import path17__default, { dirname, join, basename, resolve } from 'path';
6
+ import * as fs16 from 'fs/promises';
7
+ import fs16__default, { access, readFile, writeFile, mkdir, readdir } from 'fs/promises';
8
+ import { randomUUID, randomBytes, createHash } from 'crypto';
9
+ import * as http from 'http';
10
+ import { fileURLToPath, URL as URL$1 } from 'url';
10
11
  import { z } from 'zod';
11
12
  import * as p4 from '@clack/prompts';
12
13
  import chalk5 from 'chalk';
13
- import { exec, execSync, execFile } from 'child_process';
14
+ import { exec, execFile, execSync, spawn } from 'child_process';
14
15
  import { promisify } from 'util';
15
16
  import { homedir } from 'os';
16
17
  import JSON5 from 'json5';
17
18
  import { execa } from 'execa';
18
19
  import { parse } from '@typescript-eslint/typescript-estree';
19
20
  import { glob } from 'glob';
20
- import { Logger } from 'tslog';
21
21
  import Anthropic from '@anthropic-ai/sdk';
22
22
  import { jsonrepair } from 'jsonrepair';
23
23
  import OpenAI from 'openai';
@@ -198,6 +198,60 @@ var init_errors = __esm({
198
198
  };
199
199
  }
200
200
  });
201
+ function levelToNumber(level) {
202
+ const levels = {
203
+ silly: 0,
204
+ trace: 1,
205
+ debug: 2,
206
+ info: 3,
207
+ warn: 4,
208
+ error: 5,
209
+ fatal: 6
210
+ };
211
+ return levels[level];
212
+ }
213
+ function createLogger(config = {}) {
214
+ const finalConfig = { ...DEFAULT_CONFIG, ...config };
215
+ const logger2 = new Logger({
216
+ name: finalConfig.name,
217
+ minLevel: levelToNumber(finalConfig.level),
218
+ prettyLogTemplate: finalConfig.prettyPrint ? "{{yyyy}}-{{mm}}-{{dd}} {{hh}}:{{MM}}:{{ss}} {{logLevelName}} [{{name}}] " : void 0,
219
+ prettyLogTimeZone: "local",
220
+ stylePrettyLogs: finalConfig.prettyPrint
221
+ });
222
+ if (finalConfig.logToFile && finalConfig.logDir) {
223
+ setupFileLogging(logger2, finalConfig.logDir, finalConfig.name);
224
+ }
225
+ return logger2;
226
+ }
227
+ function setupFileLogging(logger2, logDir, name) {
228
+ if (!fs4__default.existsSync(logDir)) {
229
+ fs4__default.mkdirSync(logDir, { recursive: true });
230
+ }
231
+ const logFile = path17__default.join(logDir, `${name}.log`);
232
+ logger2.attachTransport((logObj) => {
233
+ const line = JSON.stringify(logObj) + "\n";
234
+ fs4__default.appendFileSync(logFile, line);
235
+ });
236
+ }
237
+ function getLogger() {
238
+ if (!globalLogger) {
239
+ globalLogger = createLogger();
240
+ }
241
+ return globalLogger;
242
+ }
243
+ var DEFAULT_CONFIG, globalLogger;
244
+ var init_logger = __esm({
245
+ "src/utils/logger.ts"() {
246
+ DEFAULT_CONFIG = {
247
+ name: "coco",
248
+ level: "info",
249
+ prettyPrint: true,
250
+ logToFile: false
251
+ };
252
+ globalLogger = null;
253
+ }
254
+ });
201
255
  async function refreshAccessToken(provider, refreshToken) {
202
256
  const config = OAUTH_CONFIGS[provider];
203
257
  if (!config) {
@@ -309,8 +363,276 @@ var init_pkce = __esm({
309
363
  "src/auth/pkce.ts"() {
310
364
  }
311
365
  });
366
+ function escapeHtml(unsafe) {
367
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
368
+ }
369
+ async function createCallbackServer(expectedState, timeout = 5 * 60 * 1e3, port = OAUTH_CALLBACK_PORT) {
370
+ let resolveResult;
371
+ let rejectResult;
372
+ const resultPromise = new Promise((resolve3, reject) => {
373
+ resolveResult = resolve3;
374
+ rejectResult = reject;
375
+ });
376
+ const server = http.createServer((req, res) => {
377
+ console.log(` [OAuth] ${req.method} ${req.url?.split("?")[0]}`);
378
+ res.setHeader("Access-Control-Allow-Origin", "*");
379
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
380
+ res.setHeader("Access-Control-Allow-Headers", "*");
381
+ if (req.method === "OPTIONS") {
382
+ res.writeHead(204);
383
+ res.end();
384
+ return;
385
+ }
386
+ if (!req.url?.startsWith("/auth/callback")) {
387
+ res.writeHead(404);
388
+ res.end("Not Found");
389
+ return;
390
+ }
391
+ try {
392
+ const url = new URL$1(req.url, `http://localhost`);
393
+ const code = url.searchParams.get("code");
394
+ const state = url.searchParams.get("state");
395
+ const error = url.searchParams.get("error");
396
+ const errorDescription = url.searchParams.get("error_description");
397
+ if (error) {
398
+ res.writeHead(200, { "Content-Type": "text/html" });
399
+ res.end(ERROR_HTML(errorDescription || error));
400
+ server.close();
401
+ rejectResult(new Error(errorDescription || error));
402
+ return;
403
+ }
404
+ if (!code || !state) {
405
+ res.writeHead(200, { "Content-Type": "text/html" });
406
+ res.end(ERROR_HTML("Missing authorization code or state"));
407
+ server.close();
408
+ rejectResult(new Error("Missing authorization code or state"));
409
+ return;
410
+ }
411
+ if (state !== expectedState) {
412
+ res.writeHead(200, { "Content-Type": "text/html" });
413
+ res.end(ERROR_HTML("State mismatch - possible CSRF attack"));
414
+ server.close();
415
+ rejectResult(new Error("State mismatch - possible CSRF attack"));
416
+ return;
417
+ }
418
+ res.writeHead(200, { "Content-Type": "text/html" });
419
+ res.end(SUCCESS_HTML);
420
+ server.close();
421
+ resolveResult({ code, state });
422
+ } catch (err) {
423
+ res.writeHead(500, { "Content-Type": "text/html" });
424
+ res.end(ERROR_HTML(String(err)));
425
+ server.close();
426
+ rejectResult(err instanceof Error ? err : new Error(String(err)));
427
+ }
428
+ });
429
+ const actualPort = await new Promise((resolve3, reject) => {
430
+ const errorHandler = (err) => {
431
+ if (err.code === "EADDRINUSE") {
432
+ console.log(` Port ${port} is in use, trying alternative port...`);
433
+ server.removeListener("error", errorHandler);
434
+ server.listen(0, () => {
435
+ const address = server.address();
436
+ if (typeof address === "object" && address) {
437
+ resolve3(address.port);
438
+ } else {
439
+ reject(new Error("Failed to get server port"));
440
+ }
441
+ });
442
+ } else {
443
+ reject(err);
444
+ }
445
+ };
446
+ server.on("error", errorHandler);
447
+ server.listen(port, () => {
448
+ server.removeListener("error", errorHandler);
449
+ const address = server.address();
450
+ if (typeof address === "object" && address) {
451
+ resolve3(address.port);
452
+ } else {
453
+ reject(new Error("Failed to get server port"));
454
+ }
455
+ });
456
+ });
457
+ const timeoutId = setTimeout(() => {
458
+ server.close();
459
+ rejectResult(new Error("Authentication timed out. Please try again."));
460
+ }, timeout);
461
+ server.on("close", () => {
462
+ clearTimeout(timeoutId);
463
+ });
464
+ return { port: actualPort, resultPromise };
465
+ }
466
+ var OAUTH_CALLBACK_PORT, SUCCESS_HTML, ERROR_HTML;
312
467
  var init_callback_server = __esm({
313
468
  "src/auth/callback-server.ts"() {
469
+ OAUTH_CALLBACK_PORT = 1455;
470
+ SUCCESS_HTML = `
471
+ <!DOCTYPE html>
472
+ <html>
473
+ <head>
474
+ <meta charset="utf-8">
475
+ <meta name="viewport" content="width=device-width, initial-scale=1">
476
+ <title>Authentication Successful</title>
477
+ <style>
478
+ * { margin: 0; padding: 0; box-sizing: border-box; }
479
+ body {
480
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
481
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
482
+ min-height: 100vh;
483
+ display: flex;
484
+ align-items: center;
485
+ justify-content: center;
486
+ }
487
+ .container {
488
+ background: white;
489
+ border-radius: 16px;
490
+ padding: 48px;
491
+ text-align: center;
492
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
493
+ max-width: 400px;
494
+ }
495
+ .checkmark {
496
+ width: 80px;
497
+ height: 80px;
498
+ margin: 0 auto 24px;
499
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
500
+ border-radius: 50%;
501
+ display: flex;
502
+ align-items: center;
503
+ justify-content: center;
504
+ }
505
+ .checkmark svg {
506
+ width: 40px;
507
+ height: 40px;
508
+ stroke: white;
509
+ stroke-width: 3;
510
+ fill: none;
511
+ }
512
+ h1 {
513
+ font-size: 24px;
514
+ font-weight: 600;
515
+ color: #1f2937;
516
+ margin-bottom: 12px;
517
+ }
518
+ p {
519
+ color: #6b7280;
520
+ font-size: 16px;
521
+ line-height: 1.5;
522
+ }
523
+ .brand {
524
+ margin-top: 24px;
525
+ padding-top: 24px;
526
+ border-top: 1px solid #e5e7eb;
527
+ color: #9ca3af;
528
+ font-size: 14px;
529
+ }
530
+ .brand strong {
531
+ color: #667eea;
532
+ }
533
+ </style>
534
+ </head>
535
+ <body>
536
+ <div class="container">
537
+ <div class="checkmark">
538
+ <svg viewBox="0 0 24 24">
539
+ <path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round"/>
540
+ </svg>
541
+ </div>
542
+ <h1>Authentication Successful!</h1>
543
+ <p>You can close this window and return to your terminal.</p>
544
+ <div class="brand">
545
+ Powered by <strong>Corbat-Coco</strong>
546
+ </div>
547
+ </div>
548
+ <script>
549
+ // Auto-close after 3 seconds
550
+ setTimeout(() => window.close(), 3000);
551
+ </script>
552
+ </body>
553
+ </html>
554
+ `;
555
+ ERROR_HTML = (error) => {
556
+ const safeError = escapeHtml(error);
557
+ return `
558
+ <!DOCTYPE html>
559
+ <html>
560
+ <head>
561
+ <meta charset="utf-8">
562
+ <meta name="viewport" content="width=device-width, initial-scale=1">
563
+ <title>Authentication Failed</title>
564
+ <style>
565
+ * { margin: 0; padding: 0; box-sizing: border-box; }
566
+ body {
567
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
568
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
569
+ min-height: 100vh;
570
+ display: flex;
571
+ align-items: center;
572
+ justify-content: center;
573
+ }
574
+ .container {
575
+ background: white;
576
+ border-radius: 16px;
577
+ padding: 48px;
578
+ text-align: center;
579
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
580
+ max-width: 400px;
581
+ }
582
+ .icon {
583
+ width: 80px;
584
+ height: 80px;
585
+ margin: 0 auto 24px;
586
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
587
+ border-radius: 50%;
588
+ display: flex;
589
+ align-items: center;
590
+ justify-content: center;
591
+ }
592
+ .icon svg {
593
+ width: 40px;
594
+ height: 40px;
595
+ stroke: white;
596
+ stroke-width: 3;
597
+ fill: none;
598
+ }
599
+ h1 {
600
+ font-size: 24px;
601
+ font-weight: 600;
602
+ color: #1f2937;
603
+ margin-bottom: 12px;
604
+ }
605
+ p {
606
+ color: #6b7280;
607
+ font-size: 16px;
608
+ line-height: 1.5;
609
+ }
610
+ .error {
611
+ margin-top: 16px;
612
+ padding: 12px;
613
+ background: #fef2f2;
614
+ border-radius: 8px;
615
+ color: #dc2626;
616
+ font-family: monospace;
617
+ font-size: 14px;
618
+ }
619
+ </style>
620
+ </head>
621
+ <body>
622
+ <div class="container">
623
+ <div class="icon">
624
+ <svg viewBox="0 0 24 24">
625
+ <path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
626
+ </svg>
627
+ </div>
628
+ <h1>Authentication Failed</h1>
629
+ <p>Something went wrong. Please try again.</p>
630
+ <div class="error">${safeError}</div>
631
+ </div>
632
+ </body>
633
+ </html>
634
+ `;
635
+ };
314
636
  }
315
637
  });
316
638
  function detectWSL() {
@@ -506,6 +828,33 @@ var init_auth = __esm({
506
828
  init_gcloud();
507
829
  }
508
830
  });
831
+
832
+ // src/config/schema.ts
833
+ var schema_exports = {};
834
+ __export(schema_exports, {
835
+ CocoConfigSchema: () => CocoConfigSchema,
836
+ GitHubConfigSchema: () => GitHubConfigSchema,
837
+ IntegrationsConfigSchema: () => IntegrationsConfigSchema,
838
+ MCPConfigSchema: () => MCPConfigSchema,
839
+ MCPServerConfigEntrySchema: () => MCPServerConfigEntrySchema,
840
+ PersistenceConfigSchema: () => PersistenceConfigSchema,
841
+ ProjectConfigSchema: () => ProjectConfigSchema2,
842
+ ProviderConfigSchema: () => ProviderConfigSchema,
843
+ QualityConfigSchema: () => QualityConfigSchema,
844
+ ShipConfigSchema: () => ShipConfigSchema,
845
+ SkillsConfigSchema: () => SkillsConfigSchema,
846
+ StackConfigSchema: () => StackConfigSchema,
847
+ ToolsConfigSchema: () => ToolsConfigSchema,
848
+ createDefaultConfigObject: () => createDefaultConfigObject,
849
+ validateConfig: () => validateConfig
850
+ });
851
+ function validateConfig(config) {
852
+ const result = CocoConfigSchema.safeParse(config);
853
+ if (result.success) {
854
+ return { success: true, data: result.data };
855
+ }
856
+ return { success: false, error: result.error };
857
+ }
509
858
  function createDefaultConfigObject(projectName, language = "typescript") {
510
859
  return {
511
860
  project: {
@@ -721,7 +1070,7 @@ var init_schema = __esm({
721
1070
  });
722
1071
  }
723
1072
  });
724
- var COCO_HOME, CONFIG_PATHS;
1073
+ var COCO_HOME, CONFIG_PATHS, LEGACY_PATHS;
725
1074
  var init_paths = __esm({
726
1075
  "src/config/paths.ts"() {
727
1076
  COCO_HOME = join(homedir(), ".coco");
@@ -755,14 +1104,28 @@ var init_paths = __esm({
755
1104
  /** MCP server registry: ~/.coco/mcp.json */
756
1105
  mcpRegistry: join(COCO_HOME, "mcp.json")
757
1106
  };
758
- ({
1107
+ LEGACY_PATHS = {
759
1108
  /** Old config location */
760
1109
  oldConfig: join(homedir(), ".config", "corbat-coco"),
761
1110
  /** Old MCP config directory (pre-unification) */
762
1111
  oldMcpDir: join(homedir(), ".config", "coco", "mcp")
763
- });
1112
+ };
764
1113
  }
765
1114
  });
1115
+
1116
+ // src/config/loader.ts
1117
+ var loader_exports = {};
1118
+ __export(loader_exports, {
1119
+ configExists: () => configExists,
1120
+ createDefaultConfig: () => createDefaultConfig,
1121
+ findAllConfigPaths: () => findAllConfigPaths,
1122
+ findConfigPath: () => findConfigPath,
1123
+ getConfigValue: () => getConfigValue,
1124
+ loadConfig: () => loadConfig,
1125
+ mergeWithDefaults: () => mergeWithDefaults,
1126
+ saveConfig: () => saveConfig,
1127
+ setConfigValue: () => setConfigValue
1128
+ });
766
1129
  async function loadConfig(configPath) {
767
1130
  let config = createDefaultConfig("my-project");
768
1131
  const globalConfig = await loadConfigFile(CONFIG_PATHS.config, { strict: false });
@@ -858,6 +1221,45 @@ async function saveConfig(config, configPath, global = false) {
858
1221
  function createDefaultConfig(projectName, language = "typescript") {
859
1222
  return createDefaultConfigObject(projectName, language);
860
1223
  }
1224
+ async function findConfigPath(cwd) {
1225
+ const envPath = process.env["COCO_CONFIG_PATH"];
1226
+ if (envPath) {
1227
+ try {
1228
+ await fs16__default.access(envPath);
1229
+ return envPath;
1230
+ } catch {
1231
+ }
1232
+ }
1233
+ const basePath = cwd || process.cwd();
1234
+ const projectConfigPath = path17__default.join(basePath, ".coco", "config.json");
1235
+ try {
1236
+ await fs16__default.access(projectConfigPath);
1237
+ return projectConfigPath;
1238
+ } catch {
1239
+ }
1240
+ try {
1241
+ await fs16__default.access(CONFIG_PATHS.config);
1242
+ return CONFIG_PATHS.config;
1243
+ } catch {
1244
+ return void 0;
1245
+ }
1246
+ }
1247
+ async function findAllConfigPaths(cwd) {
1248
+ const result = {};
1249
+ try {
1250
+ await fs16__default.access(CONFIG_PATHS.config);
1251
+ result.global = CONFIG_PATHS.config;
1252
+ } catch {
1253
+ }
1254
+ const basePath = cwd || process.cwd();
1255
+ const projectConfigPath = path17__default.join(basePath, ".coco", "config.json");
1256
+ try {
1257
+ await fs16__default.access(projectConfigPath);
1258
+ result.project = projectConfigPath;
1259
+ } catch {
1260
+ }
1261
+ return result;
1262
+ }
861
1263
  async function configExists(configPath, scope = "any") {
862
1264
  if (configPath) {
863
1265
  try {
@@ -885,6 +1287,45 @@ async function configExists(configPath, scope = "any") {
885
1287
  }
886
1288
  return false;
887
1289
  }
1290
+ function getConfigValue(config, path42) {
1291
+ const keys = path42.split(".");
1292
+ let current = config;
1293
+ for (const key of keys) {
1294
+ if (current === null || current === void 0 || typeof current !== "object") {
1295
+ return void 0;
1296
+ }
1297
+ current = current[key];
1298
+ }
1299
+ return current;
1300
+ }
1301
+ function setConfigValue(config, configPath, value) {
1302
+ const keys = configPath.split(".");
1303
+ const result = structuredClone(config);
1304
+ let current = result;
1305
+ for (let i = 0; i < keys.length - 1; i++) {
1306
+ const key = keys[i];
1307
+ if (!key) continue;
1308
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
1309
+ throw new Error(`Invalid config path: cannot set ${key}`);
1310
+ }
1311
+ if (!(key in current) || typeof current[key] !== "object") {
1312
+ current[key] = {};
1313
+ }
1314
+ current = current[key];
1315
+ }
1316
+ const lastKey = keys[keys.length - 1];
1317
+ if (lastKey) {
1318
+ if (lastKey === "__proto__" || lastKey === "constructor" || lastKey === "prototype") {
1319
+ throw new Error(`Invalid config path: cannot set ${lastKey}`);
1320
+ }
1321
+ current[lastKey] = value;
1322
+ }
1323
+ return result;
1324
+ }
1325
+ function mergeWithDefaults(partial, projectName) {
1326
+ const defaults = createDefaultConfig(projectName);
1327
+ return deepMergeConfig(defaults, partial);
1328
+ }
888
1329
  var init_loader = __esm({
889
1330
  "src/config/loader.ts"() {
890
1331
  init_schema();
@@ -1007,7 +1448,7 @@ function getDefaultModel(provider) {
1007
1448
  case "codex":
1008
1449
  return process.env["CODEX_MODEL"] ?? "codex-mini-latest";
1009
1450
  case "copilot":
1010
- return process.env["COPILOT_MODEL"] ?? "gpt-4o-copilot";
1451
+ return process.env["COPILOT_MODEL"] ?? "claude-sonnet-4.6";
1011
1452
  case "groq":
1012
1453
  return process.env["GROQ_MODEL"] ?? "llama-3.3-70b-versatile";
1013
1454
  case "openrouter":
@@ -1203,123 +1644,553 @@ var init_heartbeat = __esm({
1203
1644
  }
1204
1645
  });
1205
1646
 
1206
- // src/cli/repl/allow-path-prompt.ts
1207
- var allow_path_prompt_exports = {};
1208
- __export(allow_path_prompt_exports, {
1209
- promptAllowPath: () => promptAllowPath
1210
- });
1211
- async function promptAllowPath(dirPath) {
1212
- const absolute = path17__default.resolve(dirPath);
1213
- console.log();
1214
- console.log(chalk5.yellow(" \u26A0 Access denied \u2014 path is outside the project directory"));
1215
- console.log(chalk5.dim(` \u{1F4C1} ${absolute}`));
1216
- console.log();
1217
- const action = await p4.select({
1218
- message: "Grant access to this directory?",
1219
- options: [
1220
- { value: "session-write", label: "\u2713 Allow write (this session)" },
1221
- { value: "session-read", label: "\u25D0 Allow read-only (this session)" },
1222
- { value: "persist-write", label: "\u26A1 Allow write (remember for this project)" },
1223
- { value: "persist-read", label: "\u{1F4BE} Allow read-only (remember for this project)" },
1224
- { value: "no", label: "\u2717 Deny" }
1225
- ]
1226
- });
1227
- if (p4.isCancel(action) || action === "no") {
1228
- return false;
1647
+ // src/mcp/types.ts
1648
+ var init_types = __esm({
1649
+ "src/mcp/types.ts"() {
1229
1650
  }
1230
- const level = action.includes("read") ? "read" : "write";
1231
- const persist = action.startsWith("persist");
1232
- addAllowedPathToSession(absolute, level);
1233
- if (persist) {
1234
- await persistAllowedPath(absolute, level);
1651
+ });
1652
+
1653
+ // src/mcp/errors.ts
1654
+ var MCPError, MCPTransportError, MCPConnectionError, MCPTimeoutError;
1655
+ var init_errors2 = __esm({
1656
+ "src/mcp/errors.ts"() {
1657
+ init_types();
1658
+ MCPError = class extends Error {
1659
+ code;
1660
+ constructor(code, message) {
1661
+ super(message);
1662
+ this.name = "MCPError";
1663
+ this.code = code;
1664
+ }
1665
+ };
1666
+ MCPTransportError = class extends MCPError {
1667
+ constructor(message) {
1668
+ super(-32001 /* TRANSPORT_ERROR */, message);
1669
+ this.name = "MCPTransportError";
1670
+ }
1671
+ };
1672
+ MCPConnectionError = class extends MCPError {
1673
+ constructor(message) {
1674
+ super(-32003 /* CONNECTION_ERROR */, message);
1675
+ this.name = "MCPConnectionError";
1676
+ }
1677
+ };
1678
+ MCPTimeoutError = class extends MCPError {
1679
+ constructor(message = "Request timed out") {
1680
+ super(-32002 /* TIMEOUT_ERROR */, message);
1681
+ this.name = "MCPTimeoutError";
1682
+ }
1683
+ };
1235
1684
  }
1236
- const levelLabel = level === "write" ? "write" : "read-only";
1237
- const persistLabel = persist ? " (remembered)" : "";
1238
- console.log(chalk5.green(` \u2713 Access granted: ${levelLabel}${persistLabel}`));
1239
- return true;
1685
+ });
1686
+ function getDefaultRegistryPath() {
1687
+ return CONFIG_PATHS.mcpRegistry;
1240
1688
  }
1241
- var init_allow_path_prompt = __esm({
1242
- "src/cli/repl/allow-path-prompt.ts"() {
1243
- init_allowed_paths();
1689
+ function validateServerConfig(config) {
1690
+ if (!config || typeof config !== "object") {
1691
+ throw new MCPError(-32602 /* INVALID_PARAMS */, "Server config must be an object");
1244
1692
  }
1245
- });
1246
- function findPackageJson() {
1247
- let dir = dirname(fileURLToPath(import.meta.url));
1248
- for (let i = 0; i < 10; i++) {
1693
+ const cfg = config;
1694
+ if (!cfg.name || typeof cfg.name !== "string") {
1695
+ throw new MCPError(-32602 /* INVALID_PARAMS */, "Server name is required and must be a string");
1696
+ }
1697
+ if (cfg.name.length < 1 || cfg.name.length > 64) {
1698
+ throw new MCPError(
1699
+ -32602 /* INVALID_PARAMS */,
1700
+ "Server name must be between 1 and 64 characters"
1701
+ );
1702
+ }
1703
+ if (!/^[a-zA-Z0-9_-]+$/.test(cfg.name)) {
1704
+ throw new MCPError(
1705
+ -32602 /* INVALID_PARAMS */,
1706
+ "Server name must contain only letters, numbers, underscores, and hyphens"
1707
+ );
1708
+ }
1709
+ if (!cfg.transport || cfg.transport !== "stdio" && cfg.transport !== "http") {
1710
+ throw new MCPError(-32602 /* INVALID_PARAMS */, 'Transport must be "stdio" or "http"');
1711
+ }
1712
+ if (cfg.transport === "stdio") {
1713
+ if (!cfg.stdio || typeof cfg.stdio !== "object") {
1714
+ throw new MCPError(
1715
+ -32602 /* INVALID_PARAMS */,
1716
+ "stdio transport requires stdio configuration"
1717
+ );
1718
+ }
1719
+ const stdio = cfg.stdio;
1720
+ if (!stdio.command || typeof stdio.command !== "string") {
1721
+ throw new MCPError(-32602 /* INVALID_PARAMS */, "stdio.command is required");
1722
+ }
1723
+ }
1724
+ if (cfg.transport === "http") {
1725
+ if (!cfg.http || typeof cfg.http !== "object") {
1726
+ throw new MCPError(-32602 /* INVALID_PARAMS */, "http transport requires http configuration");
1727
+ }
1728
+ const http2 = cfg.http;
1729
+ if (!http2.url || typeof http2.url !== "string") {
1730
+ throw new MCPError(-32602 /* INVALID_PARAMS */, "http.url is required");
1731
+ }
1249
1732
  try {
1250
- const content = readFileSync(join(dir, "package.json"), "utf-8");
1251
- const pkg = JSON.parse(content);
1252
- if (pkg.name === "@corbat-tech/coco") return pkg;
1733
+ new URL(http2.url);
1253
1734
  } catch {
1735
+ throw new MCPError(-32602 /* INVALID_PARAMS */, "http.url must be a valid URL");
1254
1736
  }
1255
- dir = dirname(dir);
1256
1737
  }
1257
- return { version: "0.0.0" };
1258
1738
  }
1259
- var VERSION = findPackageJson().version;
1260
-
1261
- // src/phases/converge/prompts.ts
1262
- var DISCOVERY_SYSTEM_PROMPT = `You are a senior software architect and requirements analyst. Your role is to help gather and clarify requirements for software projects.
1263
-
1264
- Your goals:
1265
- 1. Understand what the user wants to build
1266
- 2. Extract clear, actionable requirements
1267
- 3. Identify ambiguities and ask clarifying questions
1268
- 4. Make reasonable assumptions when appropriate
1269
- 5. Recommend technology choices when needed
1270
-
1271
- Guidelines:
1272
- - Be thorough but not overwhelming
1273
- - Ask focused, specific questions
1274
- - Group related questions together
1275
- - Prioritize questions by importance
1276
- - Make assumptions for minor details
1277
- - Always explain your reasoning
1278
-
1279
- You communicate in a professional but friendly manner. You use concrete examples to clarify abstract requirements.`;
1280
- var INITIAL_ANALYSIS_PROMPT = `Analyze the following project description and extract:
1281
-
1282
- 1. **Project Type**: What kind of software is this? (CLI, API, web app, library, service, etc.)
1283
- 2. **Complexity**: How complex is this project? (simple, moderate, complex, enterprise)
1284
- 3. **Completeness**: How complete is the description? (0-100%)
1285
- 4. **Functional Requirements**: What should the system do?
1286
- 5. **Non-Functional Requirements**: Performance, security, scalability needs
1287
- 6. **Technical Constraints**: Any specified technologies or limitations
1288
- 7. **Assumptions**: What must we assume to proceed?
1289
- 8. **Critical Questions**: What must be clarified before proceeding?
1290
- 9. **Technology Recommendations**: What tech stack would you recommend?
1291
-
1292
- User's project description:
1293
- ---
1294
- {{userInput}}
1295
- ---
1296
-
1297
- Respond in JSON format:
1298
- {
1299
- "projectType": "string",
1300
- "complexity": "simple|moderate|complex|enterprise",
1301
- "completeness": number,
1302
- "requirements": [
1303
- {
1304
- "category": "functional|non_functional|technical|constraint",
1305
- "priority": "must_have|should_have|could_have|wont_have",
1306
- "title": "string",
1307
- "description": "string",
1308
- "explicit": boolean,
1309
- "acceptanceCriteria": ["string"]
1310
- }
1311
- ],
1312
- "assumptions": [
1313
- {
1314
- "category": "string",
1315
- "statement": "string",
1316
- "confidence": "high|medium|low",
1317
- "impactIfWrong": "string"
1739
+ function parseRegistry(json2) {
1740
+ try {
1741
+ const parsed = JSON.parse(json2);
1742
+ if (!parsed.servers || !Array.isArray(parsed.servers)) {
1743
+ return [];
1318
1744
  }
1319
- ],
1320
- "questions": [
1321
- {
1322
- "category": "clarification|expansion|decision|confirmation|scope|priority",
1745
+ return parsed.servers;
1746
+ } catch {
1747
+ return [];
1748
+ }
1749
+ }
1750
+ function serializeRegistry(servers) {
1751
+ return JSON.stringify({ servers, version: "1.0" }, null, 2);
1752
+ }
1753
+ async function migrateMCPData(opts) {
1754
+ const oldDir = LEGACY_PATHS.oldMcpDir;
1755
+ const newRegistry = CONFIG_PATHS.mcpRegistry;
1756
+ const newConfig = CONFIG_PATHS.config;
1757
+ try {
1758
+ await migrateRegistry(oldDir, newRegistry);
1759
+ await migrateGlobalConfig(oldDir, newConfig);
1760
+ } catch (error) {
1761
+ getLogger().warn(
1762
+ `[MCP] Migration failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`
1763
+ );
1764
+ }
1765
+ }
1766
+ async function migrateRegistry(oldDir, newRegistry) {
1767
+ const oldFile = join(oldDir, "registry.json");
1768
+ if (await fileExists3(newRegistry)) return;
1769
+ if (!await fileExists3(oldFile)) return;
1770
+ try {
1771
+ const content = await readFile(oldFile, "utf-8");
1772
+ const servers = parseRegistry(content);
1773
+ await mkdir(dirname(newRegistry), { recursive: true });
1774
+ await writeFile(newRegistry, serializeRegistry(servers), "utf-8");
1775
+ getLogger().info(
1776
+ `[MCP] Migrated registry from ${oldFile} to ${newRegistry}. The old file can be safely deleted.`
1777
+ );
1778
+ } catch (error) {
1779
+ getLogger().warn(
1780
+ `[MCP] Could not migrate registry: ${error instanceof Error ? error.message : String(error)}`
1781
+ );
1782
+ }
1783
+ }
1784
+ async function migrateGlobalConfig(oldDir, newConfigPath) {
1785
+ const oldFile = join(oldDir, "config.json");
1786
+ if (!await fileExists3(oldFile)) return;
1787
+ try {
1788
+ const oldContent = await readFile(oldFile, "utf-8");
1789
+ const oldMcpConfig = JSON.parse(oldContent);
1790
+ let cocoConfig = {};
1791
+ if (await fileExists3(newConfigPath)) {
1792
+ const existing = await readFile(newConfigPath, "utf-8");
1793
+ cocoConfig = JSON.parse(existing);
1794
+ }
1795
+ const existingMcp = cocoConfig.mcp ?? {};
1796
+ const fieldsToMigrate = ["defaultTimeout", "autoDiscover", "logLevel", "customServersPath"];
1797
+ let didMerge = false;
1798
+ for (const field of fieldsToMigrate) {
1799
+ if (oldMcpConfig[field] !== void 0 && existingMcp[field] === void 0) {
1800
+ existingMcp[field] = oldMcpConfig[field];
1801
+ didMerge = true;
1802
+ }
1803
+ }
1804
+ if (!didMerge) return;
1805
+ cocoConfig.mcp = existingMcp;
1806
+ await mkdir(dirname(newConfigPath), { recursive: true });
1807
+ await writeFile(newConfigPath, JSON.stringify(cocoConfig, null, 2), "utf-8");
1808
+ getLogger().info(`[MCP] Migrated global MCP settings from ${oldFile} into ${newConfigPath}.`);
1809
+ } catch (error) {
1810
+ getLogger().warn(
1811
+ `[MCP] Could not migrate global MCP config: ${error instanceof Error ? error.message : String(error)}`
1812
+ );
1813
+ }
1814
+ }
1815
+ async function fileExists3(path42) {
1816
+ try {
1817
+ await access(path42);
1818
+ return true;
1819
+ } catch {
1820
+ return false;
1821
+ }
1822
+ }
1823
+ var init_config = __esm({
1824
+ "src/mcp/config.ts"() {
1825
+ init_paths();
1826
+ init_types();
1827
+ init_errors2();
1828
+ init_logger();
1829
+ }
1830
+ });
1831
+
1832
+ // src/mcp/config-loader.ts
1833
+ var config_loader_exports = {};
1834
+ __export(config_loader_exports, {
1835
+ loadMCPConfigFile: () => loadMCPConfigFile,
1836
+ loadMCPServersFromCOCOConfig: () => loadMCPServersFromCOCOConfig,
1837
+ loadProjectMCPFile: () => loadProjectMCPFile,
1838
+ mergeMCPConfigs: () => mergeMCPConfigs
1839
+ });
1840
+ function expandEnvVar(value) {
1841
+ return value.replace(/\$\{([^}]+)\}/g, (match, name) => process.env[name] ?? match);
1842
+ }
1843
+ function expandEnvObject(env2) {
1844
+ const result = {};
1845
+ for (const [k, v] of Object.entries(env2)) {
1846
+ result[k] = expandEnvVar(v);
1847
+ }
1848
+ return result;
1849
+ }
1850
+ function expandHeaders(headers) {
1851
+ const result = {};
1852
+ for (const [k, v] of Object.entries(headers)) {
1853
+ result[k] = expandEnvVar(v);
1854
+ }
1855
+ return result;
1856
+ }
1857
+ function convertStandardEntry(name, entry) {
1858
+ if (entry.command) {
1859
+ return {
1860
+ name,
1861
+ transport: "stdio",
1862
+ enabled: entry.enabled ?? true,
1863
+ stdio: {
1864
+ command: entry.command,
1865
+ args: entry.args,
1866
+ env: entry.env ? expandEnvObject(entry.env) : void 0
1867
+ }
1868
+ };
1869
+ }
1870
+ if (entry.url) {
1871
+ const headers = entry.headers ? expandHeaders(entry.headers) : void 0;
1872
+ const authHeader = headers?.["Authorization"] ?? headers?.["authorization"];
1873
+ let auth;
1874
+ if (authHeader) {
1875
+ if (authHeader.startsWith("Bearer ")) {
1876
+ auth = { type: "bearer", token: authHeader.slice(7) };
1877
+ } else {
1878
+ auth = { type: "apikey", token: authHeader };
1879
+ }
1880
+ }
1881
+ return {
1882
+ name,
1883
+ transport: "http",
1884
+ enabled: entry.enabled ?? true,
1885
+ http: {
1886
+ url: entry.url,
1887
+ ...headers && Object.keys(headers).length > 0 ? { headers } : {},
1888
+ ...auth ? { auth } : {}
1889
+ }
1890
+ };
1891
+ }
1892
+ throw new Error(`Server "${name}" must have either "command" (stdio) or "url" (http) defined`);
1893
+ }
1894
+ async function loadMCPConfigFile(configPath) {
1895
+ try {
1896
+ await access(configPath);
1897
+ } catch {
1898
+ throw new MCPError(-32003 /* CONNECTION_ERROR */, `Config file not found: ${configPath}`);
1899
+ }
1900
+ let content;
1901
+ try {
1902
+ content = await readFile(configPath, "utf-8");
1903
+ } catch (error) {
1904
+ throw new MCPError(
1905
+ -32003 /* CONNECTION_ERROR */,
1906
+ `Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`
1907
+ );
1908
+ }
1909
+ let parsed;
1910
+ try {
1911
+ parsed = JSON.parse(content);
1912
+ } catch {
1913
+ throw new MCPError(-32700 /* PARSE_ERROR */, "Invalid JSON in config file");
1914
+ }
1915
+ const obj = parsed;
1916
+ if (obj.mcpServers && typeof obj.mcpServers === "object" && !Array.isArray(obj.mcpServers)) {
1917
+ return loadStandardFormat(obj, configPath);
1918
+ }
1919
+ if (obj.servers && Array.isArray(obj.servers)) {
1920
+ return loadCocoFormat(obj, configPath);
1921
+ }
1922
+ throw new MCPError(
1923
+ -32602 /* INVALID_PARAMS */,
1924
+ 'Config file must have either a "mcpServers" object (standard) or a "servers" array (Coco format)'
1925
+ );
1926
+ }
1927
+ function loadStandardFormat(config, configPath) {
1928
+ const validServers = [];
1929
+ const errors = [];
1930
+ for (const [name, entry] of Object.entries(config.mcpServers)) {
1931
+ if (name.startsWith("_")) continue;
1932
+ try {
1933
+ const converted = convertStandardEntry(name, entry);
1934
+ validateServerConfig(converted);
1935
+ validServers.push(converted);
1936
+ } catch (error) {
1937
+ const message = error instanceof Error ? error.message : "Unknown error";
1938
+ errors.push(`Server '${name}': ${message}`);
1939
+ }
1940
+ }
1941
+ if (errors.length > 0) {
1942
+ getLogger().warn(`[MCP] Some servers in ${configPath} failed to load: ${errors.join("; ")}`);
1943
+ }
1944
+ return validServers;
1945
+ }
1946
+ async function loadProjectMCPFile(projectPath) {
1947
+ const mcpJsonPath = path17__default.join(projectPath, ".mcp.json");
1948
+ try {
1949
+ await access(mcpJsonPath);
1950
+ } catch {
1951
+ return [];
1952
+ }
1953
+ try {
1954
+ return await loadMCPConfigFile(mcpJsonPath);
1955
+ } catch (error) {
1956
+ getLogger().warn(
1957
+ `[MCP] Failed to load .mcp.json: ${error instanceof Error ? error.message : String(error)}`
1958
+ );
1959
+ return [];
1960
+ }
1961
+ }
1962
+ function loadCocoFormat(config, configPath) {
1963
+ const validServers = [];
1964
+ const errors = [];
1965
+ for (const server of config.servers) {
1966
+ try {
1967
+ const converted = convertCocoServerEntry(server);
1968
+ validateServerConfig(converted);
1969
+ validServers.push(converted);
1970
+ } catch (error) {
1971
+ const message = error instanceof Error ? error.message : "Unknown error";
1972
+ errors.push(`Server '${server.name || "unknown"}': ${message}`);
1973
+ }
1974
+ }
1975
+ if (errors.length > 0) {
1976
+ getLogger().warn(`[MCP] Some servers in ${configPath} failed to load: ${errors.join("; ")}`);
1977
+ }
1978
+ return validServers;
1979
+ }
1980
+ function convertCocoServerEntry(server) {
1981
+ const base = {
1982
+ name: server.name,
1983
+ description: server.description,
1984
+ transport: server.transport,
1985
+ enabled: server.enabled ?? true,
1986
+ metadata: server.metadata
1987
+ };
1988
+ if (server.transport === "stdio" && server.stdio) {
1989
+ return {
1990
+ ...base,
1991
+ stdio: {
1992
+ command: server.stdio.command,
1993
+ args: server.stdio.args,
1994
+ env: server.stdio.env ? expandEnvObject(server.stdio.env) : void 0,
1995
+ cwd: server.stdio.cwd
1996
+ }
1997
+ };
1998
+ }
1999
+ if (server.transport === "http" && server.http) {
2000
+ return {
2001
+ ...base,
2002
+ http: {
2003
+ url: server.http.url,
2004
+ ...server.http.headers ? { headers: expandHeaders(server.http.headers) } : {},
2005
+ ...server.http.auth ? { auth: server.http.auth } : {},
2006
+ ...server.http.timeout !== void 0 ? { timeout: server.http.timeout } : {}
2007
+ }
2008
+ };
2009
+ }
2010
+ throw new Error(`Missing configuration for transport: ${server.transport}`);
2011
+ }
2012
+ function mergeMCPConfigs(base, ...overrides) {
2013
+ const merged = /* @__PURE__ */ new Map();
2014
+ for (const server of base) {
2015
+ merged.set(server.name, server);
2016
+ }
2017
+ for (const override of overrides) {
2018
+ for (const server of override) {
2019
+ const existing = merged.get(server.name);
2020
+ if (existing) {
2021
+ merged.set(server.name, { ...existing, ...server });
2022
+ } else {
2023
+ merged.set(server.name, server);
2024
+ }
2025
+ }
2026
+ }
2027
+ return Array.from(merged.values());
2028
+ }
2029
+ async function loadMCPServersFromCOCOConfig(configPath) {
2030
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_loader(), loader_exports));
2031
+ const { MCPServerConfigEntrySchema: MCPServerConfigEntrySchema2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
2032
+ const config = await loadConfig2(configPath);
2033
+ if (!config.mcp?.servers || config.mcp.servers.length === 0) {
2034
+ return [];
2035
+ }
2036
+ const servers = [];
2037
+ for (const entry of config.mcp.servers) {
2038
+ try {
2039
+ const parsed = MCPServerConfigEntrySchema2.parse(entry);
2040
+ const serverConfig = {
2041
+ name: parsed.name,
2042
+ description: parsed.description,
2043
+ transport: parsed.transport,
2044
+ enabled: parsed.enabled,
2045
+ ...parsed.transport === "stdio" && parsed.command && {
2046
+ stdio: {
2047
+ command: parsed.command,
2048
+ args: parsed.args,
2049
+ env: parsed.env ? expandEnvObject(parsed.env) : void 0
2050
+ }
2051
+ },
2052
+ ...parsed.transport === "http" && parsed.url && {
2053
+ http: {
2054
+ url: parsed.url,
2055
+ auth: parsed.auth
2056
+ }
2057
+ }
2058
+ };
2059
+ validateServerConfig(serverConfig);
2060
+ servers.push(serverConfig);
2061
+ } catch (error) {
2062
+ const message = error instanceof Error ? error.message : "Unknown error";
2063
+ getLogger().warn(`[MCP] Failed to load server '${entry.name}': ${message}`);
2064
+ }
2065
+ }
2066
+ return servers;
2067
+ }
2068
+ var init_config_loader = __esm({
2069
+ "src/mcp/config-loader.ts"() {
2070
+ init_config();
2071
+ init_types();
2072
+ init_errors2();
2073
+ init_logger();
2074
+ }
2075
+ });
2076
+
2077
+ // src/cli/repl/allow-path-prompt.ts
2078
+ var allow_path_prompt_exports = {};
2079
+ __export(allow_path_prompt_exports, {
2080
+ promptAllowPath: () => promptAllowPath
2081
+ });
2082
+ async function promptAllowPath(dirPath) {
2083
+ const absolute = path17__default.resolve(dirPath);
2084
+ console.log();
2085
+ console.log(chalk5.yellow(" \u26A0 Access denied \u2014 path is outside the project directory"));
2086
+ console.log(chalk5.dim(` \u{1F4C1} ${absolute}`));
2087
+ console.log();
2088
+ const action = await p4.select({
2089
+ message: "Grant access to this directory?",
2090
+ options: [
2091
+ { value: "session-write", label: "\u2713 Allow write (this session)" },
2092
+ { value: "session-read", label: "\u25D0 Allow read-only (this session)" },
2093
+ { value: "persist-write", label: "\u26A1 Allow write (remember for this project)" },
2094
+ { value: "persist-read", label: "\u{1F4BE} Allow read-only (remember for this project)" },
2095
+ { value: "no", label: "\u2717 Deny" }
2096
+ ]
2097
+ });
2098
+ if (p4.isCancel(action) || action === "no") {
2099
+ return false;
2100
+ }
2101
+ const level = action.includes("read") ? "read" : "write";
2102
+ const persist = action.startsWith("persist");
2103
+ addAllowedPathToSession(absolute, level);
2104
+ if (persist) {
2105
+ await persistAllowedPath(absolute, level);
2106
+ }
2107
+ const levelLabel = level === "write" ? "write" : "read-only";
2108
+ const persistLabel = persist ? " (remembered)" : "";
2109
+ console.log(chalk5.green(` \u2713 Access granted: ${levelLabel}${persistLabel}`));
2110
+ return true;
2111
+ }
2112
+ var init_allow_path_prompt = __esm({
2113
+ "src/cli/repl/allow-path-prompt.ts"() {
2114
+ init_allowed_paths();
2115
+ }
2116
+ });
2117
+ function findPackageJson() {
2118
+ let dir = dirname(fileURLToPath(import.meta.url));
2119
+ for (let i = 0; i < 10; i++) {
2120
+ try {
2121
+ const content = readFileSync(join(dir, "package.json"), "utf-8");
2122
+ const pkg = JSON.parse(content);
2123
+ if (pkg.name === "@corbat-tech/coco") return pkg;
2124
+ } catch {
2125
+ }
2126
+ dir = dirname(dir);
2127
+ }
2128
+ return { version: "0.0.0" };
2129
+ }
2130
+ var VERSION = findPackageJson().version;
2131
+
2132
+ // src/phases/converge/prompts.ts
2133
+ var DISCOVERY_SYSTEM_PROMPT = `You are a senior software architect and requirements analyst. Your role is to help gather and clarify requirements for software projects.
2134
+
2135
+ Your goals:
2136
+ 1. Understand what the user wants to build
2137
+ 2. Extract clear, actionable requirements
2138
+ 3. Identify ambiguities and ask clarifying questions
2139
+ 4. Make reasonable assumptions when appropriate
2140
+ 5. Recommend technology choices when needed
2141
+
2142
+ Guidelines:
2143
+ - Be thorough but not overwhelming
2144
+ - Ask focused, specific questions
2145
+ - Group related questions together
2146
+ - Prioritize questions by importance
2147
+ - Make assumptions for minor details
2148
+ - Always explain your reasoning
2149
+
2150
+ You communicate in a professional but friendly manner. You use concrete examples to clarify abstract requirements.`;
2151
+ var INITIAL_ANALYSIS_PROMPT = `Analyze the following project description and extract:
2152
+
2153
+ 1. **Project Type**: What kind of software is this? (CLI, API, web app, library, service, etc.)
2154
+ 2. **Complexity**: How complex is this project? (simple, moderate, complex, enterprise)
2155
+ 3. **Completeness**: How complete is the description? (0-100%)
2156
+ 4. **Functional Requirements**: What should the system do?
2157
+ 5. **Non-Functional Requirements**: Performance, security, scalability needs
2158
+ 6. **Technical Constraints**: Any specified technologies or limitations
2159
+ 7. **Assumptions**: What must we assume to proceed?
2160
+ 8. **Critical Questions**: What must be clarified before proceeding?
2161
+ 9. **Technology Recommendations**: What tech stack would you recommend?
2162
+
2163
+ User's project description:
2164
+ ---
2165
+ {{userInput}}
2166
+ ---
2167
+
2168
+ Respond in JSON format:
2169
+ {
2170
+ "projectType": "string",
2171
+ "complexity": "simple|moderate|complex|enterprise",
2172
+ "completeness": number,
2173
+ "requirements": [
2174
+ {
2175
+ "category": "functional|non_functional|technical|constraint",
2176
+ "priority": "must_have|should_have|could_have|wont_have",
2177
+ "title": "string",
2178
+ "description": "string",
2179
+ "explicit": boolean,
2180
+ "acceptanceCriteria": ["string"]
2181
+ }
2182
+ ],
2183
+ "assumptions": [
2184
+ {
2185
+ "category": "string",
2186
+ "statement": "string",
2187
+ "confidence": "high|medium|low",
2188
+ "impactIfWrong": "string"
2189
+ }
2190
+ ],
2191
+ "questions": [
2192
+ {
2193
+ "category": "clarification|expansion|decision|confirmation|scope|priority",
1323
2194
  "question": "string",
1324
2195
  "context": "string",
1325
2196
  "importance": "critical|important|helpful",
@@ -8666,16 +9537,16 @@ var QualityEvaluator = class {
8666
9537
  * Find source files in project, adapting to the detected language stack.
8667
9538
  */
8668
9539
  async findSourceFiles() {
8669
- const { access: access10 } = await import('fs/promises');
8670
- const { join: join18 } = await import('path');
9540
+ const { access: access13 } = await import('fs/promises');
9541
+ const { join: join19 } = await import('path');
8671
9542
  let isJava = false;
8672
9543
  try {
8673
- await access10(join18(this.projectPath, "pom.xml"));
9544
+ await access13(join19(this.projectPath, "pom.xml"));
8674
9545
  isJava = true;
8675
9546
  } catch {
8676
9547
  for (const f of ["build.gradle", "build.gradle.kts"]) {
8677
9548
  try {
8678
- await access10(join18(this.projectPath, f));
9549
+ await access13(join19(this.projectPath, f));
8679
9550
  isJava = true;
8680
9551
  break;
8681
9552
  } catch {
@@ -8976,55 +9847,9 @@ function buildFeedbackSection(feedback, issues) {
8976
9847
 
8977
9848
  // src/phases/complete/generator.ts
8978
9849
  init_errors();
8979
- var DEFAULT_CONFIG = {
8980
- name: "coco",
8981
- level: "info",
8982
- prettyPrint: true,
8983
- logToFile: false
8984
- };
8985
- function levelToNumber(level) {
8986
- const levels = {
8987
- silly: 0,
8988
- trace: 1,
8989
- debug: 2,
8990
- info: 3,
8991
- warn: 4,
8992
- error: 5,
8993
- fatal: 6
8994
- };
8995
- return levels[level];
8996
- }
8997
- function createLogger(config = {}) {
8998
- const finalConfig = { ...DEFAULT_CONFIG, ...config };
8999
- const logger = new Logger({
9000
- name: finalConfig.name,
9001
- minLevel: levelToNumber(finalConfig.level),
9002
- prettyLogTemplate: finalConfig.prettyPrint ? "{{yyyy}}-{{mm}}-{{dd}} {{hh}}:{{MM}}:{{ss}} {{logLevelName}} [{{name}}] " : void 0,
9003
- prettyLogTimeZone: "local",
9004
- stylePrettyLogs: finalConfig.prettyPrint
9005
- });
9006
- if (finalConfig.logToFile && finalConfig.logDir) {
9007
- setupFileLogging(logger, finalConfig.logDir, finalConfig.name);
9008
- }
9009
- return logger;
9010
- }
9011
- function setupFileLogging(logger, logDir, name) {
9012
- if (!fs4__default.existsSync(logDir)) {
9013
- fs4__default.mkdirSync(logDir, { recursive: true });
9014
- }
9015
- const logFile = path17__default.join(logDir, `${name}.log`);
9016
- logger.attachTransport((logObj) => {
9017
- const line = JSON.stringify(logObj) + "\n";
9018
- fs4__default.appendFileSync(logFile, line);
9019
- });
9020
- }
9021
- var globalLogger = null;
9022
- function getLogger() {
9023
- if (!globalLogger) {
9024
- globalLogger = createLogger();
9025
- }
9026
- return globalLogger;
9027
- }
9850
+
9851
+ // src/tools/registry.ts
9852
+ init_logger();
9028
9853
 
9029
9854
  // src/utils/error-humanizer.ts
9030
9855
  function extractQuotedPath(msg) {
@@ -9075,12 +9900,12 @@ function humanizeError(message, toolName) {
9075
9900
  return msg;
9076
9901
  }
9077
9902
  if (/ENOENT/i.test(msg)) {
9078
- const path40 = extractQuotedPath(msg);
9079
- return path40 ? `File or directory not found: ${path40}` : "File or directory not found";
9903
+ const path42 = extractQuotedPath(msg);
9904
+ return path42 ? `File or directory not found: ${path42}` : "File or directory not found";
9080
9905
  }
9081
9906
  if (/EACCES/i.test(msg)) {
9082
- const path40 = extractQuotedPath(msg);
9083
- return path40 ? `Permission denied: ${path40}` : "Permission denied \u2014 check file permissions";
9907
+ const path42 = extractQuotedPath(msg);
9908
+ return path42 ? `Permission denied: ${path42}` : "Permission denied \u2014 check file permissions";
9084
9909
  }
9085
9910
  if (/EISDIR/i.test(msg)) {
9086
9911
  return "Expected a file but found a directory at the specified path";
@@ -12536,6 +13361,7 @@ async function withRetry(fn, config = {}) {
12536
13361
  }
12537
13362
 
12538
13363
  // src/providers/anthropic.ts
13364
+ init_logger();
12539
13365
  var DEFAULT_MODEL = "claude-opus-4-6";
12540
13366
  var CONTEXT_WINDOWS = {
12541
13367
  // Kimi Code model (Anthropic-compatible endpoint)
@@ -15068,6 +15894,11 @@ var CONTEXT_WINDOWS4 = {
15068
15894
  "gemini-2.5-pro": 1048576
15069
15895
  };
15070
15896
  var DEFAULT_MODEL4 = "claude-sonnet-4.6";
15897
+ function normalizeModel(model) {
15898
+ if (typeof model !== "string") return void 0;
15899
+ const trimmed = model.trim();
15900
+ return trimmed.length > 0 ? trimmed : void 0;
15901
+ }
15071
15902
  var COPILOT_HEADERS = {
15072
15903
  "Copilot-Integration-Id": "vscode-chat",
15073
15904
  "Editor-Version": "vscode/1.99.0",
@@ -15091,7 +15922,7 @@ var CopilotProvider = class extends OpenAIProvider {
15091
15922
  async initialize(config) {
15092
15923
  this.config = {
15093
15924
  ...config,
15094
- model: config.model ?? DEFAULT_MODEL4
15925
+ model: normalizeModel(config.model) ?? DEFAULT_MODEL4
15095
15926
  };
15096
15927
  const tokenResult = await getValidCopilotToken();
15097
15928
  if (tokenResult) {
@@ -15977,12 +16808,17 @@ function createResilientProvider(provider, config) {
15977
16808
  init_copilot();
15978
16809
  init_errors();
15979
16810
  init_env();
16811
+ function normalizeProviderModel(model) {
16812
+ if (typeof model !== "string") return void 0;
16813
+ const trimmed = model.trim();
16814
+ return trimmed.length > 0 ? trimmed : void 0;
16815
+ }
15980
16816
  async function createProvider(type, config = {}) {
15981
16817
  let provider;
15982
16818
  const mergedConfig = {
15983
16819
  apiKey: config.apiKey ?? getApiKey(type),
15984
16820
  baseUrl: config.baseUrl ?? getBaseUrl(type),
15985
- model: config.model ?? getDefaultModel(type),
16821
+ model: normalizeProviderModel(config.model) ?? getDefaultModel(type),
15986
16822
  maxTokens: config.maxTokens,
15987
16823
  temperature: config.temperature,
15988
16824
  timeout: config.timeout
@@ -16171,9 +17007,9 @@ function createInitialState(config) {
16171
17007
  }
16172
17008
  async function loadExistingState(projectPath) {
16173
17009
  try {
16174
- const fs38 = await import('fs/promises');
17010
+ const fs39 = await import('fs/promises');
16175
17011
  const statePath = `${projectPath}/.coco/state/project.json`;
16176
- const content = await fs38.readFile(statePath, "utf-8");
17012
+ const content = await fs39.readFile(statePath, "utf-8");
16177
17013
  const data = JSON.parse(content);
16178
17014
  data.createdAt = new Date(data.createdAt);
16179
17015
  data.updatedAt = new Date(data.updatedAt);
@@ -16183,13 +17019,13 @@ async function loadExistingState(projectPath) {
16183
17019
  }
16184
17020
  }
16185
17021
  async function saveState(state) {
16186
- const fs38 = await import('fs/promises');
17022
+ const fs39 = await import('fs/promises');
16187
17023
  const statePath = `${state.path}/.coco/state`;
16188
- await fs38.mkdir(statePath, { recursive: true });
17024
+ await fs39.mkdir(statePath, { recursive: true });
16189
17025
  const filePath = `${statePath}/project.json`;
16190
17026
  const tmpPath = `${filePath}.tmp.${Date.now()}`;
16191
- await fs38.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
16192
- await fs38.rename(tmpPath, filePath);
17027
+ await fs39.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
17028
+ await fs39.rename(tmpPath, filePath);
16193
17029
  }
16194
17030
  function getPhaseExecutor(phase) {
16195
17031
  switch (phase) {
@@ -16248,20 +17084,20 @@ async function createPhaseContext(config, state) {
16248
17084
  };
16249
17085
  const tools = {
16250
17086
  file: {
16251
- async read(path40) {
16252
- const fs38 = await import('fs/promises');
16253
- return fs38.readFile(path40, "utf-8");
17087
+ async read(path42) {
17088
+ const fs39 = await import('fs/promises');
17089
+ return fs39.readFile(path42, "utf-8");
16254
17090
  },
16255
- async write(path40, content) {
16256
- const fs38 = await import('fs/promises');
17091
+ async write(path42, content) {
17092
+ const fs39 = await import('fs/promises');
16257
17093
  const nodePath = await import('path');
16258
- await fs38.mkdir(nodePath.dirname(path40), { recursive: true });
16259
- await fs38.writeFile(path40, content, "utf-8");
17094
+ await fs39.mkdir(nodePath.dirname(path42), { recursive: true });
17095
+ await fs39.writeFile(path42, content, "utf-8");
16260
17096
  },
16261
- async exists(path40) {
16262
- const fs38 = await import('fs/promises');
17097
+ async exists(path42) {
17098
+ const fs39 = await import('fs/promises');
16263
17099
  try {
16264
- await fs38.access(path40);
17100
+ await fs39.access(path42);
16265
17101
  return true;
16266
17102
  } catch {
16267
17103
  return false;
@@ -16410,9 +17246,9 @@ async function createSnapshot(state) {
16410
17246
  var MAX_CHECKPOINT_VERSIONS = 5;
16411
17247
  async function getCheckpointFiles(state, phase) {
16412
17248
  try {
16413
- const fs38 = await import('fs/promises');
17249
+ const fs39 = await import('fs/promises');
16414
17250
  const checkpointDir = `${state.path}/.coco/checkpoints`;
16415
- const files = await fs38.readdir(checkpointDir);
17251
+ const files = await fs39.readdir(checkpointDir);
16416
17252
  const phaseFiles = files.filter((f) => f.startsWith(`snapshot-pre-${phase}-`) && f.endsWith(".json")).sort((a, b) => {
16417
17253
  const tsA = parseInt(a.split("-").pop()?.replace(".json", "") ?? "0", 10);
16418
17254
  const tsB = parseInt(b.split("-").pop()?.replace(".json", "") ?? "0", 10);
@@ -16425,11 +17261,11 @@ async function getCheckpointFiles(state, phase) {
16425
17261
  }
16426
17262
  async function cleanupOldCheckpoints(state, phase) {
16427
17263
  try {
16428
- const fs38 = await import('fs/promises');
17264
+ const fs39 = await import('fs/promises');
16429
17265
  const files = await getCheckpointFiles(state, phase);
16430
17266
  if (files.length > MAX_CHECKPOINT_VERSIONS) {
16431
17267
  const filesToDelete = files.slice(MAX_CHECKPOINT_VERSIONS);
16432
- await Promise.all(filesToDelete.map((f) => fs38.unlink(f).catch(() => {
17268
+ await Promise.all(filesToDelete.map((f) => fs39.unlink(f).catch(() => {
16433
17269
  })));
16434
17270
  }
16435
17271
  } catch {
@@ -16437,13 +17273,13 @@ async function cleanupOldCheckpoints(state, phase) {
16437
17273
  }
16438
17274
  async function saveSnapshot(state, snapshotId) {
16439
17275
  try {
16440
- const fs38 = await import('fs/promises');
17276
+ const fs39 = await import('fs/promises');
16441
17277
  const snapshotPath = `${state.path}/.coco/checkpoints/snapshot-${snapshotId}.json`;
16442
17278
  const snapshotDir = `${state.path}/.coco/checkpoints`;
16443
- await fs38.mkdir(snapshotDir, { recursive: true });
17279
+ await fs39.mkdir(snapshotDir, { recursive: true });
16444
17280
  const createdAt = state.createdAt instanceof Date ? state.createdAt.toISOString() : String(state.createdAt);
16445
17281
  const updatedAt = state.updatedAt instanceof Date ? state.updatedAt.toISOString() : String(state.updatedAt);
16446
- await fs38.writeFile(
17282
+ await fs39.writeFile(
16447
17283
  snapshotPath,
16448
17284
  JSON.stringify(
16449
17285
  {
@@ -16735,20 +17571,69 @@ var SENSITIVE_PATTERNS = [
16735
17571
  // PyPI auth
16736
17572
  ];
16737
17573
  var BLOCKED_PATHS = ["/etc", "/var", "/usr", "/root", "/sys", "/proc", "/boot"];
17574
+ var SAFE_COCO_HOME_READ_FILES = /* @__PURE__ */ new Set([
17575
+ "mcp.json",
17576
+ "config.json",
17577
+ "COCO.md",
17578
+ "AGENTS.md",
17579
+ "CLAUDE.md",
17580
+ "projects.json",
17581
+ "trusted-tools.json",
17582
+ "allowed-paths.json"
17583
+ ]);
17584
+ var SAFE_COCO_HOME_READ_DIR_PREFIXES = ["skills", "memories", "logs", "checkpoints", "sessions"];
16738
17585
  function hasNullByte(str) {
16739
17586
  return str.includes("\0");
16740
17587
  }
16741
17588
  function normalizePath(filePath) {
16742
17589
  let normalized = filePath.replace(/\0/g, "");
17590
+ const home = process.env.HOME || process.env.USERPROFILE;
17591
+ if (home && normalized.startsWith("~")) {
17592
+ if (normalized === "~") {
17593
+ normalized = home;
17594
+ } else if (normalized.startsWith("~/") || normalized.startsWith(`~${path17__default.sep}`)) {
17595
+ normalized = path17__default.join(home, normalized.slice(2));
17596
+ }
17597
+ }
16743
17598
  normalized = path17__default.normalize(normalized);
16744
17599
  return normalized;
16745
17600
  }
17601
+ function resolveUserPath(filePath) {
17602
+ return path17__default.resolve(normalizePath(filePath));
17603
+ }
17604
+ function isWithinDirectory(targetPath, baseDir) {
17605
+ const normalizedTarget = path17__default.normalize(targetPath);
17606
+ const normalizedBase = path17__default.normalize(baseDir);
17607
+ return normalizedTarget === normalizedBase || normalizedTarget.startsWith(normalizedBase + path17__default.sep);
17608
+ }
17609
+ function isSafeCocoHomeReadPath(absolutePath, homeDir) {
17610
+ const cocoHome = path17__default.join(homeDir, ".coco");
17611
+ if (!isWithinDirectory(absolutePath, cocoHome)) {
17612
+ return false;
17613
+ }
17614
+ const relativePath = path17__default.relative(cocoHome, absolutePath);
17615
+ if (!relativePath || relativePath.startsWith("..")) {
17616
+ return false;
17617
+ }
17618
+ const segments = relativePath.split(path17__default.sep).filter(Boolean);
17619
+ const firstSegment = segments[0];
17620
+ if (!firstSegment) {
17621
+ return false;
17622
+ }
17623
+ if (firstSegment === "tokens" || firstSegment === ".env") {
17624
+ return false;
17625
+ }
17626
+ if (segments.length === 1 && SAFE_COCO_HOME_READ_FILES.has(firstSegment)) {
17627
+ return true;
17628
+ }
17629
+ return SAFE_COCO_HOME_READ_DIR_PREFIXES.includes(firstSegment);
17630
+ }
16746
17631
  function isPathAllowed(filePath, operation) {
16747
17632
  if (hasNullByte(filePath)) {
16748
17633
  return { allowed: false, reason: "Path contains invalid characters" };
16749
17634
  }
16750
17635
  const normalized = normalizePath(filePath);
16751
- const absolute = path17__default.resolve(normalized);
17636
+ const absolute = resolveUserPath(normalized);
16752
17637
  const cwd = process.cwd();
16753
17638
  for (const blocked of BLOCKED_PATHS) {
16754
17639
  const normalizedBlocked = path17__default.normalize(blocked);
@@ -16762,6 +17647,9 @@ function isPathAllowed(filePath, operation) {
16762
17647
  const normalizedCwd = path17__default.normalize(cwd);
16763
17648
  if (absolute.startsWith(normalizedHome) && !absolute.startsWith(normalizedCwd)) {
16764
17649
  if (isWithinAllowedPath(absolute, operation)) ; else if (operation === "read") {
17650
+ if (isSafeCocoHomeReadPath(absolute, normalizedHome)) {
17651
+ return { allowed: true };
17652
+ }
16765
17653
  const allowedHomeReads = [".gitconfig", ".zshrc", ".bashrc"];
16766
17654
  const basename5 = path17__default.basename(absolute);
16767
17655
  if (!allowedHomeReads.includes(basename5)) {
@@ -16804,7 +17692,7 @@ function isENOENT(error) {
16804
17692
  return error.code === "ENOENT";
16805
17693
  }
16806
17694
  async function enrichENOENT(filePath, operation) {
16807
- const absPath = path17__default.resolve(filePath);
17695
+ const absPath = resolveUserPath(filePath);
16808
17696
  const suggestions = await suggestSimilarFilesDeep(absPath, process.cwd());
16809
17697
  const hint = formatSuggestions(suggestions, path17__default.dirname(absPath));
16810
17698
  const action = operation === "read" ? "Use glob or list_dir to find the correct path." : "Check that the parent directory exists.";
@@ -16812,7 +17700,7 @@ async function enrichENOENT(filePath, operation) {
16812
17700
  ${action}`;
16813
17701
  }
16814
17702
  async function enrichDirENOENT(dirPath) {
16815
- const absPath = path17__default.resolve(dirPath);
17703
+ const absPath = resolveUserPath(dirPath);
16816
17704
  const suggestions = await suggestSimilarDirsDeep(absPath, process.cwd());
16817
17705
  const hint = formatSuggestions(suggestions, path17__default.dirname(absPath));
16818
17706
  return `Directory not found: ${dirPath}${hint}
@@ -16835,7 +17723,7 @@ Examples:
16835
17723
  async execute({ path: filePath, encoding, maxSize }) {
16836
17724
  validatePath(filePath, "read");
16837
17725
  try {
16838
- const absolutePath = path17__default.resolve(filePath);
17726
+ const absolutePath = resolveUserPath(filePath);
16839
17727
  const stats = await fs16__default.stat(absolutePath);
16840
17728
  const maxBytes = maxSize ?? DEFAULT_MAX_FILE_SIZE;
16841
17729
  let truncated = false;
@@ -16894,7 +17782,7 @@ Examples:
16894
17782
  async execute({ path: filePath, content, createDirs, dryRun }) {
16895
17783
  validatePath(filePath, "write");
16896
17784
  try {
16897
- const absolutePath = path17__default.resolve(filePath);
17785
+ const absolutePath = resolveUserPath(filePath);
16898
17786
  let wouldCreate = false;
16899
17787
  try {
16900
17788
  await fs16__default.access(absolutePath);
@@ -16956,7 +17844,7 @@ Examples:
16956
17844
  async execute({ path: filePath, oldText, newText, all, dryRun }) {
16957
17845
  validatePath(filePath, "write");
16958
17846
  try {
16959
- const absolutePath = path17__default.resolve(filePath);
17847
+ const absolutePath = resolveUserPath(filePath);
16960
17848
  let content = await fs16__default.readFile(absolutePath, "utf-8");
16961
17849
  let replacements = 0;
16962
17850
  if (all) {
@@ -17077,7 +17965,7 @@ Examples:
17077
17965
  }),
17078
17966
  async execute({ path: filePath }) {
17079
17967
  try {
17080
- const absolutePath = path17__default.resolve(filePath);
17968
+ const absolutePath = resolveUserPath(filePath);
17081
17969
  const stats = await fs16__default.stat(absolutePath);
17082
17970
  return {
17083
17971
  exists: true,
@@ -17108,7 +17996,7 @@ Examples:
17108
17996
  }),
17109
17997
  async execute({ path: dirPath, recursive }) {
17110
17998
  try {
17111
- const absolutePath = path17__default.resolve(dirPath);
17999
+ const absolutePath = resolveUserPath(dirPath);
17112
18000
  const entries = [];
17113
18001
  async function listDir(dir, prefix = "") {
17114
18002
  const items = await fs16__default.readdir(dir, { withFileTypes: true });
@@ -17168,7 +18056,7 @@ Examples:
17168
18056
  }
17169
18057
  validatePath(filePath, "delete");
17170
18058
  try {
17171
- const absolutePath = path17__default.resolve(filePath);
18059
+ const absolutePath = resolveUserPath(filePath);
17172
18060
  const stats = await fs16__default.stat(absolutePath);
17173
18061
  if (stats.isDirectory()) {
17174
18062
  if (!recursive) {
@@ -17184,7 +18072,7 @@ Examples:
17184
18072
  } catch (error) {
17185
18073
  if (error instanceof ToolError) throw error;
17186
18074
  if (error.code === "ENOENT") {
17187
- return { deleted: false, path: path17__default.resolve(filePath) };
18075
+ return { deleted: false, path: resolveUserPath(filePath) };
17188
18076
  }
17189
18077
  throw new FileSystemError(`Failed to delete: ${filePath}`, {
17190
18078
  path: filePath,
@@ -17212,8 +18100,8 @@ Examples:
17212
18100
  validatePath(source, "read");
17213
18101
  validatePath(destination, "write");
17214
18102
  try {
17215
- const srcPath = path17__default.resolve(source);
17216
- const destPath = path17__default.resolve(destination);
18103
+ const srcPath = resolveUserPath(source);
18104
+ const destPath = resolveUserPath(destination);
17217
18105
  if (!overwrite) {
17218
18106
  try {
17219
18107
  await fs16__default.access(destPath);
@@ -17273,8 +18161,8 @@ Examples:
17273
18161
  validatePath(source, "delete");
17274
18162
  validatePath(destination, "write");
17275
18163
  try {
17276
- const srcPath = path17__default.resolve(source);
17277
- const destPath = path17__default.resolve(destination);
18164
+ const srcPath = resolveUserPath(source);
18165
+ const destPath = resolveUserPath(destination);
17278
18166
  if (!overwrite) {
17279
18167
  try {
17280
18168
  await fs16__default.access(destPath);
@@ -17360,7 +18248,7 @@ Examples:
17360
18248
  }),
17361
18249
  async execute({ path: dirPath, depth, showHidden, dirsOnly }) {
17362
18250
  try {
17363
- const absolutePath = path17__default.resolve(dirPath ?? ".");
18251
+ const absolutePath = resolveUserPath(dirPath ?? ".");
17364
18252
  let totalFiles = 0;
17365
18253
  let totalDirs = 0;
17366
18254
  const lines = [path17__default.basename(absolutePath) + "/"];
@@ -18271,6 +19159,9 @@ var simpleAutoCommitTool = defineTool({
18271
19159
  }
18272
19160
  });
18273
19161
  var gitSimpleTools = [checkProtectedBranchTool, simpleAutoCommitTool];
19162
+
19163
+ // src/cli/repl/agents/manager.ts
19164
+ init_logger();
18274
19165
  var AGENT_NAMES = {
18275
19166
  explore: "Explorer",
18276
19167
  plan: "Planner",
@@ -21763,9 +22654,9 @@ async function fileExists(filePath) {
21763
22654
  return false;
21764
22655
  }
21765
22656
  }
21766
- async function fileExists2(path40) {
22657
+ async function fileExists2(path42) {
21767
22658
  try {
21768
- await access(path40);
22659
+ await access(path42);
21769
22660
  return true;
21770
22661
  } catch {
21771
22662
  return false;
@@ -21855,7 +22746,7 @@ async function detectMaturity(cwd) {
21855
22746
  if (!hasLintConfig && hasPackageJson) {
21856
22747
  try {
21857
22748
  const pkgRaw = await import('fs/promises').then(
21858
- (fs38) => fs38.readFile(join(cwd, "package.json"), "utf-8")
22749
+ (fs39) => fs39.readFile(join(cwd, "package.json"), "utf-8")
21859
22750
  );
21860
22751
  const pkg = JSON.parse(pkgRaw);
21861
22752
  if (pkg.scripts?.lint || pkg.scripts?.["lint:fix"]) {
@@ -25782,6 +26673,1624 @@ Examples:
25782
26673
  }
25783
26674
  });
25784
26675
  var openTools = [openFileTool];
26676
+
26677
+ // src/mcp/registry.ts
26678
+ init_types();
26679
+ init_config();
26680
+ init_errors2();
26681
+ var MCPRegistryImpl = class {
26682
+ servers = /* @__PURE__ */ new Map();
26683
+ registryPath;
26684
+ constructor(registryPath) {
26685
+ this.registryPath = registryPath || getDefaultRegistryPath();
26686
+ }
26687
+ /**
26688
+ * Add or update a server configuration
26689
+ */
26690
+ async addServer(config) {
26691
+ validateServerConfig(config);
26692
+ const existing = this.servers.get(config.name);
26693
+ if (existing) {
26694
+ this.servers.set(config.name, { ...existing, ...config });
26695
+ } else {
26696
+ this.servers.set(config.name, config);
26697
+ }
26698
+ await this.save();
26699
+ }
26700
+ /**
26701
+ * Remove a server by name
26702
+ */
26703
+ async removeServer(name) {
26704
+ const existed = this.servers.has(name);
26705
+ if (existed) {
26706
+ this.servers.delete(name);
26707
+ await this.save();
26708
+ }
26709
+ return existed;
26710
+ }
26711
+ /**
26712
+ * Get a server configuration by name
26713
+ */
26714
+ getServer(name) {
26715
+ return this.servers.get(name);
26716
+ }
26717
+ /**
26718
+ * List all registered servers
26719
+ */
26720
+ listServers() {
26721
+ return Array.from(this.servers.values());
26722
+ }
26723
+ /**
26724
+ * List enabled servers only
26725
+ */
26726
+ listEnabledServers() {
26727
+ return this.listServers().filter((s) => s.enabled !== false);
26728
+ }
26729
+ /**
26730
+ * Check if a server exists
26731
+ */
26732
+ hasServer(name) {
26733
+ return this.servers.has(name);
26734
+ }
26735
+ /**
26736
+ * Get registry file path
26737
+ */
26738
+ getRegistryPath() {
26739
+ return this.registryPath;
26740
+ }
26741
+ /**
26742
+ * Save registry to disk
26743
+ */
26744
+ async save() {
26745
+ try {
26746
+ await this.ensureDir(this.registryPath);
26747
+ const data = serializeRegistry(this.listServers());
26748
+ await writeFile(this.registryPath, data, "utf-8");
26749
+ } catch (error) {
26750
+ throw new MCPError(
26751
+ -32001 /* TRANSPORT_ERROR */,
26752
+ `Failed to save registry: ${error instanceof Error ? error.message : "Unknown error"}`
26753
+ );
26754
+ }
26755
+ }
26756
+ /**
26757
+ * Load registry from disk
26758
+ */
26759
+ async load() {
26760
+ if (this.registryPath === getDefaultRegistryPath()) {
26761
+ await migrateMCPData();
26762
+ }
26763
+ try {
26764
+ await access(this.registryPath);
26765
+ const content = await readFile(this.registryPath, "utf-8");
26766
+ let servers = parseRegistry(content);
26767
+ if (servers.length === 0) {
26768
+ try {
26769
+ const { loadMCPConfigFile: loadMCPConfigFile2 } = await Promise.resolve().then(() => (init_config_loader(), config_loader_exports));
26770
+ servers = await loadMCPConfigFile2(this.registryPath);
26771
+ } catch {
26772
+ }
26773
+ }
26774
+ this.servers.clear();
26775
+ for (const server of servers) {
26776
+ try {
26777
+ validateServerConfig(server);
26778
+ this.servers.set(server.name, server);
26779
+ } catch {
26780
+ }
26781
+ }
26782
+ } catch {
26783
+ this.servers.clear();
26784
+ }
26785
+ }
26786
+ /**
26787
+ * Ensure directory exists
26788
+ */
26789
+ async ensureDir(path42) {
26790
+ await mkdir(dirname(path42), { recursive: true });
26791
+ }
26792
+ };
26793
+
26794
+ // src/tools/mcp.ts
26795
+ init_config_loader();
26796
+
26797
+ // src/mcp/client.ts
26798
+ init_errors2();
26799
+ var DEFAULT_REQUEST_TIMEOUT = 6e4;
26800
+ var MCPClientImpl = class {
26801
+ constructor(transport, requestTimeout = DEFAULT_REQUEST_TIMEOUT) {
26802
+ this.transport = transport;
26803
+ this.requestTimeout = requestTimeout;
26804
+ this.setupTransportHandlers();
26805
+ }
26806
+ requestId = 0;
26807
+ pendingRequests = /* @__PURE__ */ new Map();
26808
+ initialized = false;
26809
+ serverCapabilities = null;
26810
+ /**
26811
+ * Setup transport message handlers
26812
+ */
26813
+ setupTransportHandlers() {
26814
+ this.transport.onMessage((message) => {
26815
+ this.handleMessage(message);
26816
+ });
26817
+ this.transport.onError((error) => {
26818
+ this.rejectAllPending(error);
26819
+ });
26820
+ this.transport.onClose(() => {
26821
+ this.initialized = false;
26822
+ this.rejectAllPending(new MCPConnectionError("Connection closed"));
26823
+ });
26824
+ }
26825
+ /**
26826
+ * Handle incoming messages from transport
26827
+ */
26828
+ handleMessage(message) {
26829
+ const pending = this.pendingRequests.get(message.id);
26830
+ if (!pending) return;
26831
+ clearTimeout(pending.timeout);
26832
+ this.pendingRequests.delete(message.id);
26833
+ if (message.error) {
26834
+ pending.reject(new Error(message.error.message));
26835
+ } else {
26836
+ pending.resolve(message.result);
26837
+ }
26838
+ }
26839
+ /**
26840
+ * Reject all pending requests
26841
+ */
26842
+ rejectAllPending(error) {
26843
+ for (const [, pending] of this.pendingRequests) {
26844
+ clearTimeout(pending.timeout);
26845
+ pending.reject(error);
26846
+ }
26847
+ this.pendingRequests.clear();
26848
+ }
26849
+ /**
26850
+ * Send a request and wait for response
26851
+ */
26852
+ async sendRequest(method, params) {
26853
+ if (!this.transport.isConnected()) {
26854
+ throw new MCPConnectionError("Transport not connected");
26855
+ }
26856
+ const id = ++this.requestId;
26857
+ const request = {
26858
+ jsonrpc: "2.0",
26859
+ id,
26860
+ method,
26861
+ params
26862
+ };
26863
+ return new Promise((resolve3, reject) => {
26864
+ const timeout = setTimeout(() => {
26865
+ this.pendingRequests.delete(id);
26866
+ reject(new MCPTimeoutError(`Request '${method}' timed out after ${this.requestTimeout}ms`));
26867
+ }, this.requestTimeout);
26868
+ this.pendingRequests.set(id, {
26869
+ resolve: resolve3,
26870
+ reject,
26871
+ timeout
26872
+ });
26873
+ this.transport.send(request).catch((error) => {
26874
+ clearTimeout(timeout);
26875
+ this.pendingRequests.delete(id);
26876
+ reject(error);
26877
+ });
26878
+ });
26879
+ }
26880
+ /**
26881
+ * Initialize connection to MCP server
26882
+ */
26883
+ async initialize(params) {
26884
+ if (!this.transport.isConnected()) {
26885
+ await this.transport.connect();
26886
+ }
26887
+ const result = await this.sendRequest("initialize", params);
26888
+ this.serverCapabilities = result.capabilities;
26889
+ this.initialized = true;
26890
+ await this.transport.send({
26891
+ jsonrpc: "2.0",
26892
+ id: ++this.requestId,
26893
+ method: "notifications/initialized"
26894
+ });
26895
+ return result;
26896
+ }
26897
+ /**
26898
+ * List available tools
26899
+ */
26900
+ async listTools() {
26901
+ this.ensureInitialized();
26902
+ return this.sendRequest("tools/list");
26903
+ }
26904
+ /**
26905
+ * Call a tool on the MCP server
26906
+ */
26907
+ async callTool(params) {
26908
+ this.ensureInitialized();
26909
+ return this.sendRequest("tools/call", params);
26910
+ }
26911
+ /**
26912
+ * List available resources
26913
+ */
26914
+ async listResources() {
26915
+ this.ensureInitialized();
26916
+ return this.sendRequest("resources/list");
26917
+ }
26918
+ /**
26919
+ * Read a specific resource by URI
26920
+ */
26921
+ async readResource(uri) {
26922
+ this.ensureInitialized();
26923
+ return this.sendRequest("resources/read", { uri });
26924
+ }
26925
+ /**
26926
+ * List available prompts
26927
+ */
26928
+ async listPrompts() {
26929
+ this.ensureInitialized();
26930
+ return this.sendRequest("prompts/list");
26931
+ }
26932
+ /**
26933
+ * Get a specific prompt with arguments
26934
+ */
26935
+ async getPrompt(name, args) {
26936
+ this.ensureInitialized();
26937
+ return this.sendRequest("prompts/get", {
26938
+ name,
26939
+ arguments: args
26940
+ });
26941
+ }
26942
+ /**
26943
+ * Ensure client is initialized
26944
+ */
26945
+ ensureInitialized() {
26946
+ if (!this.initialized) {
26947
+ throw new MCPConnectionError("Client not initialized. Call initialize() first.");
26948
+ }
26949
+ }
26950
+ /**
26951
+ * Close the client connection
26952
+ */
26953
+ async close() {
26954
+ this.initialized = false;
26955
+ await this.transport.disconnect();
26956
+ }
26957
+ /**
26958
+ * Check if client is connected
26959
+ */
26960
+ isConnected() {
26961
+ return this.transport.isConnected() && this.initialized;
26962
+ }
26963
+ /**
26964
+ * Get server capabilities
26965
+ */
26966
+ getServerCapabilities() {
26967
+ return this.serverCapabilities;
26968
+ }
26969
+ };
26970
+
26971
+ // src/mcp/transport/stdio.ts
26972
+ init_errors2();
26973
+ var StdioTransport = class {
26974
+ constructor(config) {
26975
+ this.config = config;
26976
+ }
26977
+ process = null;
26978
+ messageCallback = null;
26979
+ errorCallback = null;
26980
+ closeCallback = null;
26981
+ buffer = "";
26982
+ connected = false;
26983
+ /**
26984
+ * Connect to the stdio transport by spawning the process
26985
+ */
26986
+ async connect() {
26987
+ if (this.connected) {
26988
+ throw new MCPConnectionError("Transport already connected");
26989
+ }
26990
+ return new Promise((resolve3, reject) => {
26991
+ const { command, args = [], env: env2, cwd } = this.config;
26992
+ this.process = spawn(command, args, {
26993
+ stdio: ["pipe", "pipe", "pipe"],
26994
+ env: { ...process.env, ...env2 },
26995
+ cwd
26996
+ });
26997
+ this.process.on("error", (error) => {
26998
+ reject(new MCPConnectionError(`Failed to spawn process: ${error.message}`));
26999
+ });
27000
+ this.process.on("spawn", () => {
27001
+ this.connected = true;
27002
+ this.setupHandlers();
27003
+ resolve3();
27004
+ });
27005
+ this.process.stderr?.on("data", (data) => {
27006
+ console.debug(`[MCP Server stderr]: ${data.toString()}`);
27007
+ });
27008
+ });
27009
+ }
27010
+ /**
27011
+ * Setup data handlers for the process
27012
+ */
27013
+ setupHandlers() {
27014
+ if (!this.process?.stdout) return;
27015
+ this.process.stdout.on("data", (data) => {
27016
+ this.handleData(data);
27017
+ });
27018
+ this.process.on("exit", (code) => {
27019
+ this.connected = false;
27020
+ if (code !== 0 && code !== null) {
27021
+ this.errorCallback?.(new MCPTransportError(`Process exited with code ${code}`));
27022
+ }
27023
+ this.closeCallback?.();
27024
+ });
27025
+ this.process.on("close", () => {
27026
+ this.connected = false;
27027
+ this.closeCallback?.();
27028
+ });
27029
+ }
27030
+ /**
27031
+ * Handle incoming data from stdout
27032
+ */
27033
+ handleData(data) {
27034
+ this.buffer += data.toString();
27035
+ const lines = this.buffer.split("\n");
27036
+ this.buffer = lines.pop() ?? "";
27037
+ for (const line of lines) {
27038
+ const trimmed = line.trim();
27039
+ if (!trimmed) continue;
27040
+ try {
27041
+ const message = JSON.parse(trimmed);
27042
+ this.messageCallback?.(message);
27043
+ } catch {
27044
+ this.errorCallback?.(new MCPTransportError(`Invalid JSON: ${trimmed}`));
27045
+ }
27046
+ }
27047
+ }
27048
+ /**
27049
+ * Send a message through the transport
27050
+ */
27051
+ async send(message) {
27052
+ if (!this.connected || !this.process?.stdin) {
27053
+ throw new MCPTransportError("Transport not connected");
27054
+ }
27055
+ const line = JSON.stringify(message) + "\n";
27056
+ return new Promise((resolve3, reject) => {
27057
+ if (!this.process?.stdin) {
27058
+ reject(new MCPTransportError("stdin not available"));
27059
+ return;
27060
+ }
27061
+ const stdin = this.process.stdin;
27062
+ const canWrite = stdin.write(line, (error) => {
27063
+ if (error) {
27064
+ reject(new MCPTransportError(`Write error: ${error.message}`));
27065
+ } else {
27066
+ resolve3();
27067
+ }
27068
+ });
27069
+ if (!canWrite) {
27070
+ stdin.once("drain", () => resolve3());
27071
+ }
27072
+ });
27073
+ }
27074
+ /**
27075
+ * Disconnect from the transport
27076
+ */
27077
+ async disconnect() {
27078
+ if (!this.process) return;
27079
+ return new Promise((resolve3) => {
27080
+ if (!this.process) {
27081
+ resolve3();
27082
+ return;
27083
+ }
27084
+ this.process.stdin?.end();
27085
+ const timeout = setTimeout(() => {
27086
+ this.process?.kill("SIGTERM");
27087
+ }, 5e3);
27088
+ this.process.on("close", () => {
27089
+ clearTimeout(timeout);
27090
+ this.connected = false;
27091
+ this.process = null;
27092
+ resolve3();
27093
+ });
27094
+ if (this.process.killed || !this.connected) {
27095
+ clearTimeout(timeout);
27096
+ this.process = null;
27097
+ resolve3();
27098
+ }
27099
+ });
27100
+ }
27101
+ /**
27102
+ * Set callback for received messages
27103
+ */
27104
+ onMessage(callback) {
27105
+ this.messageCallback = callback;
27106
+ }
27107
+ /**
27108
+ * Set callback for errors
27109
+ */
27110
+ onError(callback) {
27111
+ this.errorCallback = callback;
27112
+ }
27113
+ /**
27114
+ * Set callback for connection close
27115
+ */
27116
+ onClose(callback) {
27117
+ this.closeCallback = callback;
27118
+ }
27119
+ /**
27120
+ * Check if transport is connected
27121
+ */
27122
+ isConnected() {
27123
+ return this.connected;
27124
+ }
27125
+ };
27126
+
27127
+ // src/mcp/transport/http.ts
27128
+ init_errors2();
27129
+
27130
+ // src/mcp/oauth.ts
27131
+ init_callback_server();
27132
+ init_paths();
27133
+ init_logger();
27134
+ var execFileAsync2 = promisify(execFile);
27135
+ var TOKEN_STORE_PATH = path17__default.join(CONFIG_PATHS.tokens, "mcp-oauth.json");
27136
+ var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
27137
+ var logger = getLogger();
27138
+ function getResourceKey(resourceUrl) {
27139
+ const resource = canonicalizeResourceUrl(resourceUrl);
27140
+ return resource.toLowerCase();
27141
+ }
27142
+ function canonicalizeResourceUrl(resourceUrl) {
27143
+ const parsed = new URL(resourceUrl);
27144
+ parsed.search = "";
27145
+ parsed.hash = "";
27146
+ if (parsed.pathname === "/") {
27147
+ return `${parsed.protocol}//${parsed.host}`;
27148
+ }
27149
+ parsed.pathname = parsed.pathname.replace(/\/+$/, "");
27150
+ return parsed.toString();
27151
+ }
27152
+ async function loadStore2() {
27153
+ try {
27154
+ const content = await fs16__default.readFile(TOKEN_STORE_PATH, "utf-8");
27155
+ const parsed = JSON.parse(content);
27156
+ return {
27157
+ tokens: parsed.tokens ?? {},
27158
+ clients: parsed.clients ?? {}
27159
+ };
27160
+ } catch {
27161
+ return { tokens: {}, clients: {} };
27162
+ }
27163
+ }
27164
+ async function saveStore2(store) {
27165
+ await fs16__default.mkdir(path17__default.dirname(TOKEN_STORE_PATH), { recursive: true });
27166
+ await fs16__default.writeFile(TOKEN_STORE_PATH, JSON.stringify(store, null, 2), {
27167
+ encoding: "utf-8",
27168
+ mode: 384
27169
+ });
27170
+ }
27171
+ function isTokenExpired2(token) {
27172
+ if (!token.expiresAt) return false;
27173
+ return Date.now() >= token.expiresAt - 3e4;
27174
+ }
27175
+ async function getStoredMcpOAuthToken(resourceUrl) {
27176
+ const store = await loadStore2();
27177
+ const token = store.tokens[getResourceKey(resourceUrl)];
27178
+ if (!token) return void 0;
27179
+ if (isTokenExpired2(token)) return void 0;
27180
+ return token.accessToken;
27181
+ }
27182
+ function createCodeVerifier() {
27183
+ return randomBytes(32).toString("base64url");
27184
+ }
27185
+ function createCodeChallenge(verifier) {
27186
+ return createHash("sha256").update(verifier).digest("base64url");
27187
+ }
27188
+ function createState() {
27189
+ return randomBytes(16).toString("hex");
27190
+ }
27191
+ async function openBrowser(url) {
27192
+ let safeUrl;
27193
+ try {
27194
+ const parsed = new URL(url);
27195
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
27196
+ return false;
27197
+ }
27198
+ safeUrl = parsed.toString();
27199
+ } catch {
27200
+ return false;
27201
+ }
27202
+ const isWSL2 = process.platform === "linux" && (process.env["WSL_DISTRO_NAME"] !== void 0 || process.env["WSL_INTEROP"] !== void 0 || process.env["TERM_PROGRAM"]?.toLowerCase().includes("wsl") === true);
27203
+ const commands = [];
27204
+ if (process.platform === "darwin") {
27205
+ commands.push(
27206
+ { cmd: "open", args: [safeUrl] },
27207
+ { cmd: "open", args: ["-a", "Safari", safeUrl] },
27208
+ { cmd: "open", args: ["-a", "Google Chrome", safeUrl] }
27209
+ );
27210
+ } else if (process.platform === "win32") {
27211
+ commands.push({ cmd: "rundll32", args: ["url.dll,FileProtocolHandler", safeUrl] });
27212
+ } else if (isWSL2) {
27213
+ commands.push(
27214
+ { cmd: "cmd.exe", args: ["/c", "start", "", safeUrl] },
27215
+ { cmd: "powershell.exe", args: ["-Command", `Start-Process '${safeUrl}'`] },
27216
+ { cmd: "wslview", args: [safeUrl] }
27217
+ );
27218
+ } else {
27219
+ commands.push(
27220
+ { cmd: "xdg-open", args: [safeUrl] },
27221
+ { cmd: "sensible-browser", args: [safeUrl] },
27222
+ { cmd: "x-www-browser", args: [safeUrl] },
27223
+ { cmd: "gnome-open", args: [safeUrl] },
27224
+ { cmd: "firefox", args: [safeUrl] },
27225
+ { cmd: "chromium-browser", args: [safeUrl] },
27226
+ { cmd: "google-chrome", args: [safeUrl] }
27227
+ );
27228
+ }
27229
+ for (const { cmd, args } of commands) {
27230
+ try {
27231
+ await execFileAsync2(cmd, args);
27232
+ return true;
27233
+ } catch {
27234
+ continue;
27235
+ }
27236
+ }
27237
+ return false;
27238
+ }
27239
+ function maskUrlForLogs(rawUrl) {
27240
+ try {
27241
+ const url = new URL(rawUrl);
27242
+ url.search = "";
27243
+ url.hash = "";
27244
+ return url.toString();
27245
+ } catch {
27246
+ return "[invalid-url]";
27247
+ }
27248
+ }
27249
+ function parseResourceMetadataUrl(wwwAuthenticateHeader) {
27250
+ if (!wwwAuthenticateHeader) return void 0;
27251
+ const match = wwwAuthenticateHeader.match(/resource_metadata="([^"]+)"/i);
27252
+ return match?.[1];
27253
+ }
27254
+ function createProtectedMetadataCandidates(resourceUrl, headerUrl) {
27255
+ const candidates = [];
27256
+ if (headerUrl) {
27257
+ candidates.push(headerUrl);
27258
+ }
27259
+ const resource = new URL(resourceUrl);
27260
+ const origin = `${resource.protocol}//${resource.host}`;
27261
+ const pathPart = resource.pathname.replace(/\/+$/, "");
27262
+ candidates.push(`${origin}/.well-known/oauth-protected-resource`);
27263
+ if (pathPart && pathPart !== "/") {
27264
+ candidates.push(`${origin}/.well-known/oauth-protected-resource${pathPart}`);
27265
+ candidates.push(`${origin}/.well-known/oauth-protected-resource/${pathPart.replace(/^\//, "")}`);
27266
+ }
27267
+ return Array.from(new Set(candidates));
27268
+ }
27269
+ async function fetchJson(url) {
27270
+ const res = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
27271
+ if (!res.ok) {
27272
+ throw new Error(`HTTP ${res.status} while fetching ${url}`);
27273
+ }
27274
+ return await res.json();
27275
+ }
27276
+ function buildAuthorizationMetadataCandidates(issuer) {
27277
+ const parsed = new URL(issuer);
27278
+ const base = `${parsed.protocol}//${parsed.host}`;
27279
+ const issuerPath = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
27280
+ const candidates = [
27281
+ `${base}/.well-known/oauth-authorization-server${issuerPath}`,
27282
+ `${base}/.well-known/oauth-authorization-server`,
27283
+ `${base}/.well-known/openid-configuration${issuerPath}`,
27284
+ `${base}/.well-known/openid-configuration`
27285
+ ];
27286
+ return Array.from(new Set(candidates));
27287
+ }
27288
+ async function discoverProtectedResourceMetadata(resourceUrl, wwwAuthenticateHeader) {
27289
+ const headerUrl = parseResourceMetadataUrl(wwwAuthenticateHeader);
27290
+ const candidates = createProtectedMetadataCandidates(resourceUrl, headerUrl);
27291
+ for (const candidate of candidates) {
27292
+ try {
27293
+ const metadata = await fetchJson(candidate);
27294
+ if (Array.isArray(metadata.authorization_servers) && metadata.authorization_servers.length > 0) {
27295
+ return metadata;
27296
+ }
27297
+ } catch {
27298
+ }
27299
+ }
27300
+ throw new Error("Could not discover OAuth protected resource metadata for MCP server");
27301
+ }
27302
+ async function discoverAuthorizationServerMetadata(authorizationServer) {
27303
+ const candidates = buildAuthorizationMetadataCandidates(authorizationServer);
27304
+ for (const candidate of candidates) {
27305
+ try {
27306
+ const metadata = await fetchJson(candidate);
27307
+ if (metadata.authorization_endpoint && metadata.token_endpoint) {
27308
+ return metadata;
27309
+ }
27310
+ } catch {
27311
+ }
27312
+ }
27313
+ throw new Error("Could not discover OAuth authorization server metadata");
27314
+ }
27315
+ async function ensureClientId(authorizationMetadata, authorizationServer, redirectUri) {
27316
+ const store = await loadStore2();
27317
+ const clientKey = `${authorizationServer}|${redirectUri}`;
27318
+ const existing = store.clients[clientKey]?.clientId;
27319
+ if (existing) return existing;
27320
+ const registrationEndpoint = authorizationMetadata.registration_endpoint;
27321
+ if (!registrationEndpoint) {
27322
+ throw new Error(
27323
+ "Authorization server does not expose dynamic client registration; configure a static OAuth client ID for this MCP server."
27324
+ );
27325
+ }
27326
+ const registrationPayload = {
27327
+ client_name: "corbat-coco-mcp",
27328
+ redirect_uris: [redirectUri],
27329
+ grant_types: ["authorization_code", "refresh_token"],
27330
+ response_types: ["code"],
27331
+ token_endpoint_auth_method: "none"
27332
+ };
27333
+ const response = await fetch(registrationEndpoint, {
27334
+ method: "POST",
27335
+ headers: {
27336
+ "Content-Type": "application/json",
27337
+ Accept: "application/json"
27338
+ },
27339
+ body: JSON.stringify(registrationPayload)
27340
+ });
27341
+ if (!response.ok) {
27342
+ throw new Error(`Dynamic client registration failed: HTTP ${response.status}`);
27343
+ }
27344
+ const data = await response.json();
27345
+ const clientId = data.client_id;
27346
+ if (!clientId) {
27347
+ throw new Error("Dynamic client registration did not return client_id");
27348
+ }
27349
+ store.clients[clientKey] = { clientId };
27350
+ await saveStore2(store);
27351
+ return clientId;
27352
+ }
27353
+ async function refreshAccessToken2(params) {
27354
+ const body = new URLSearchParams({
27355
+ grant_type: "refresh_token",
27356
+ client_id: params.clientId,
27357
+ refresh_token: params.refreshToken,
27358
+ resource: params.resource
27359
+ });
27360
+ const response = await fetch(params.tokenEndpoint, {
27361
+ method: "POST",
27362
+ headers: {
27363
+ "Content-Type": "application/x-www-form-urlencoded",
27364
+ Accept: "application/json"
27365
+ },
27366
+ body: body.toString()
27367
+ });
27368
+ if (!response.ok) {
27369
+ throw new Error(`Refresh token exchange failed: HTTP ${response.status}`);
27370
+ }
27371
+ const tokenResponse = await response.json();
27372
+ if (!tokenResponse.access_token) {
27373
+ throw new Error("Refresh token response missing access_token");
27374
+ }
27375
+ return tokenResponse;
27376
+ }
27377
+ async function exchangeCodeForToken(tokenEndpoint, clientId, code, codeVerifier, redirectUri, resource) {
27378
+ const body = new URLSearchParams({
27379
+ grant_type: "authorization_code",
27380
+ code,
27381
+ client_id: clientId,
27382
+ redirect_uri: redirectUri,
27383
+ code_verifier: codeVerifier,
27384
+ resource
27385
+ });
27386
+ const response = await fetch(tokenEndpoint, {
27387
+ method: "POST",
27388
+ headers: {
27389
+ "Content-Type": "application/x-www-form-urlencoded",
27390
+ Accept: "application/json"
27391
+ },
27392
+ body: body.toString()
27393
+ });
27394
+ if (!response.ok) {
27395
+ throw new Error(`Token exchange failed: HTTP ${response.status}`);
27396
+ }
27397
+ const tokenResponse = await response.json();
27398
+ if (!tokenResponse.access_token) {
27399
+ throw new Error("Token exchange response missing access_token");
27400
+ }
27401
+ return tokenResponse;
27402
+ }
27403
+ async function persistToken(resourceUrl, token, metadata) {
27404
+ const store = await loadStore2();
27405
+ const expiresAt = typeof token.expires_in === "number" ? Date.now() + Math.max(0, token.expires_in) * 1e3 : void 0;
27406
+ store.tokens[getResourceKey(resourceUrl)] = {
27407
+ accessToken: token.access_token,
27408
+ tokenType: token.token_type,
27409
+ refreshToken: token.refresh_token,
27410
+ authorizationServer: metadata?.authorizationServer,
27411
+ clientId: metadata?.clientId,
27412
+ resource: canonicalizeResourceUrl(resourceUrl),
27413
+ ...expiresAt ? { expiresAt } : {}
27414
+ };
27415
+ await saveStore2(store);
27416
+ }
27417
+ async function authenticateMcpOAuth(params) {
27418
+ const resource = canonicalizeResourceUrl(params.resourceUrl);
27419
+ const store = await loadStore2();
27420
+ const stored = store.tokens[getResourceKey(resource)];
27421
+ if (stored && !params.forceRefresh && !isTokenExpired2(stored)) {
27422
+ return stored.accessToken;
27423
+ }
27424
+ if (!process.stdout.isTTY) {
27425
+ throw new Error(
27426
+ `MCP server '${params.serverName}' requires interactive OAuth in a TTY session. Run Coco in a terminal, or use mcp-remote (e.g. npx -y mcp-remote@latest ${resource}) for IDE bridge workflows.`
27427
+ );
27428
+ }
27429
+ let authorizationServer;
27430
+ let authorizationMetadata;
27431
+ try {
27432
+ const protectedMetadata = await discoverProtectedResourceMetadata(
27433
+ resource,
27434
+ params.wwwAuthenticateHeader
27435
+ );
27436
+ authorizationServer = protectedMetadata.authorization_servers?.[0];
27437
+ if (authorizationServer) {
27438
+ authorizationMetadata = await discoverAuthorizationServerMetadata(authorizationServer);
27439
+ }
27440
+ } catch {
27441
+ }
27442
+ if (!authorizationMetadata) {
27443
+ authorizationMetadata = await discoverAuthorizationServerMetadata(resource);
27444
+ }
27445
+ authorizationServer = authorizationServer ?? authorizationMetadata.issuer ?? new URL(resource).origin;
27446
+ if (stored && stored.refreshToken && stored.clientId && (params.forceRefresh || isTokenExpired2(stored))) {
27447
+ try {
27448
+ const refreshed = await refreshAccessToken2({
27449
+ tokenEndpoint: authorizationMetadata.token_endpoint,
27450
+ clientId: stored.clientId,
27451
+ refreshToken: stored.refreshToken,
27452
+ resource
27453
+ });
27454
+ await persistToken(resource, refreshed, {
27455
+ authorizationServer,
27456
+ clientId: stored.clientId
27457
+ });
27458
+ return refreshed.access_token;
27459
+ } catch {
27460
+ }
27461
+ }
27462
+ const codeVerifier = createCodeVerifier();
27463
+ const codeChallenge = createCodeChallenge(codeVerifier);
27464
+ const state = createState();
27465
+ const { port, resultPromise } = await createCallbackServer(
27466
+ state,
27467
+ OAUTH_TIMEOUT_MS,
27468
+ OAUTH_CALLBACK_PORT
27469
+ );
27470
+ const redirectUri = `http://localhost:${port}/auth/callback`;
27471
+ const clientId = await ensureClientId(authorizationMetadata, authorizationServer, redirectUri);
27472
+ const authUrl = new URL(authorizationMetadata.authorization_endpoint);
27473
+ authUrl.searchParams.set("response_type", "code");
27474
+ authUrl.searchParams.set("client_id", clientId);
27475
+ authUrl.searchParams.set("redirect_uri", redirectUri);
27476
+ authUrl.searchParams.set("state", state);
27477
+ authUrl.searchParams.set("code_challenge", codeChallenge);
27478
+ authUrl.searchParams.set("code_challenge_method", "S256");
27479
+ authUrl.searchParams.set("resource", resource);
27480
+ if (authorizationMetadata.scopes_supported?.includes("offline_access")) {
27481
+ authUrl.searchParams.set("scope", "offline_access");
27482
+ }
27483
+ const opened = await openBrowser(authUrl.toString());
27484
+ if (!opened) {
27485
+ logger.warn(`[MCP OAuth] Could not open browser automatically for '${params.serverName}'`);
27486
+ logger.warn(`[MCP OAuth] Manual auth URL base: ${maskUrlForLogs(authUrl.toString())}`);
27487
+ console.log(`[MCP OAuth] Open this URL manually: ${authUrl.toString()}`);
27488
+ } else {
27489
+ logger.info(
27490
+ `[MCP OAuth] Opened browser for '${params.serverName}'. Complete login to continue.`
27491
+ );
27492
+ }
27493
+ const callback = await resultPromise;
27494
+ const token = await exchangeCodeForToken(
27495
+ authorizationMetadata.token_endpoint,
27496
+ clientId,
27497
+ callback.code,
27498
+ codeVerifier,
27499
+ redirectUri,
27500
+ resource
27501
+ );
27502
+ await persistToken(resource, token, { authorizationServer, clientId });
27503
+ return token.access_token;
27504
+ }
27505
+
27506
+ // src/mcp/transport/http.ts
27507
+ init_errors2();
27508
+ var HTTPTransport = class {
27509
+ constructor(config) {
27510
+ this.config = config;
27511
+ this.config.timeout = config.timeout ?? 6e4;
27512
+ this.config.retries = config.retries ?? 3;
27513
+ }
27514
+ messageCallback = null;
27515
+ errorCallback = null;
27516
+ // Used to report transport errors to the client
27517
+ reportError(error) {
27518
+ this.errorCallback?.(error);
27519
+ }
27520
+ closeCallback = null;
27521
+ connected = false;
27522
+ abortController = null;
27523
+ pendingRequests = /* @__PURE__ */ new Map();
27524
+ oauthToken;
27525
+ oauthInFlight = null;
27526
+ /**
27527
+ * Get authentication token
27528
+ */
27529
+ getAuthToken() {
27530
+ if (this.oauthToken) {
27531
+ return this.oauthToken;
27532
+ }
27533
+ if (!this.config.auth) return void 0;
27534
+ if (this.config.auth.token) {
27535
+ return this.config.auth.token;
27536
+ }
27537
+ if (this.config.auth.tokenEnv) {
27538
+ return process.env[this.config.auth.tokenEnv];
27539
+ }
27540
+ return void 0;
27541
+ }
27542
+ /**
27543
+ * Build request headers
27544
+ */
27545
+ buildHeaders() {
27546
+ const headers = {
27547
+ "Content-Type": "application/json",
27548
+ Accept: "application/json",
27549
+ ...this.config.headers
27550
+ };
27551
+ if (this.oauthToken) {
27552
+ headers["Authorization"] = `Bearer ${this.oauthToken}`;
27553
+ return headers;
27554
+ }
27555
+ const token = this.getAuthToken();
27556
+ if (token && this.config.auth) {
27557
+ if (this.config.auth.type === "apikey") {
27558
+ headers[this.config.auth.headerName || "X-API-Key"] = token;
27559
+ } else {
27560
+ headers["Authorization"] = `Bearer ${token}`;
27561
+ }
27562
+ }
27563
+ return headers;
27564
+ }
27565
+ shouldAttemptOAuth() {
27566
+ if (this.config.auth?.type === "apikey") {
27567
+ return false;
27568
+ }
27569
+ if (this.config.auth?.type === "bearer") {
27570
+ return !this.getAuthToken();
27571
+ }
27572
+ return true;
27573
+ }
27574
+ async ensureOAuthToken(wwwAuthenticateHeader, options) {
27575
+ if (this.oauthToken && !options?.forceRefresh) {
27576
+ return this.oauthToken;
27577
+ }
27578
+ if (this.oauthInFlight) {
27579
+ return this.oauthInFlight;
27580
+ }
27581
+ const serverName = this.config.name ?? this.config.url;
27582
+ if (options?.forceRefresh) {
27583
+ this.oauthToken = void 0;
27584
+ }
27585
+ this.oauthInFlight = authenticateMcpOAuth({
27586
+ serverName,
27587
+ resourceUrl: this.config.url,
27588
+ wwwAuthenticateHeader,
27589
+ forceRefresh: options?.forceRefresh
27590
+ }).then((token) => {
27591
+ this.oauthToken = token;
27592
+ return token;
27593
+ }).finally(() => {
27594
+ this.oauthInFlight = null;
27595
+ });
27596
+ return this.oauthInFlight;
27597
+ }
27598
+ async sendRequestWithOAuthRetry(method, body, signal) {
27599
+ const doFetch = async () => fetch(this.config.url, {
27600
+ method,
27601
+ headers: this.buildHeaders(),
27602
+ ...body ? { body } : {},
27603
+ signal
27604
+ });
27605
+ let response = await doFetch();
27606
+ if (response.status !== 401 || !this.shouldAttemptOAuth()) {
27607
+ if (this.shouldAttemptOAuth() && !this.oauthToken && response.headers.get("www-authenticate")) {
27608
+ await this.ensureOAuthToken(response.headers.get("www-authenticate"));
27609
+ response = await doFetch();
27610
+ }
27611
+ return response;
27612
+ }
27613
+ await this.ensureOAuthToken(response.headers.get("www-authenticate"), { forceRefresh: true });
27614
+ response = await doFetch();
27615
+ return response;
27616
+ }
27617
+ looksLikeAuthErrorMessage(message) {
27618
+ if (!message) return false;
27619
+ const msg = message.toLowerCase();
27620
+ const hasStrongAuthSignal = msg.includes("unauthorized") || msg.includes("unauthorised") || msg.includes("authentication") || msg.includes("oauth") || msg.includes("access token") || msg.includes("invalid_token") || msg.includes("invalid token") || msg.includes("token expired") || msg.includes("bearer") || msg.includes("not authenticated") || msg.includes("not logged") || msg.includes("login") || msg.includes("generate") && msg.includes("token");
27621
+ const hasVendorHint = msg.includes("gemini cli") || msg.includes("jira") || msg.includes("confluence") || msg.includes("atlassian");
27622
+ const hasWeakAuthSignal = msg.includes("authenticate") || msg.includes("token") || msg.includes("authorization");
27623
+ return hasStrongAuthSignal || // Vendor-specific hints alone are not enough; require an auth-related token too.
27624
+ hasVendorHint && hasWeakAuthSignal;
27625
+ }
27626
+ isJsonRpcAuthError(payload) {
27627
+ if (!payload.error) return false;
27628
+ return this.looksLikeAuthErrorMessage(payload.error.message);
27629
+ }
27630
+ /**
27631
+ * Connect to the HTTP transport
27632
+ */
27633
+ async connect() {
27634
+ if (this.connected) {
27635
+ throw new MCPConnectionError("Transport already connected");
27636
+ }
27637
+ try {
27638
+ new URL(this.config.url);
27639
+ } catch {
27640
+ throw new MCPConnectionError(`Invalid URL: ${this.config.url}`);
27641
+ }
27642
+ try {
27643
+ this.abortController = new AbortController();
27644
+ if (this.shouldAttemptOAuth()) {
27645
+ this.oauthToken = await getStoredMcpOAuthToken(this.config.url);
27646
+ }
27647
+ const response = await this.sendRequestWithOAuthRetry(
27648
+ "GET",
27649
+ void 0,
27650
+ this.abortController.signal
27651
+ );
27652
+ if (!response.ok && response.status !== 404) {
27653
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
27654
+ }
27655
+ this.connected = true;
27656
+ } catch (error) {
27657
+ if (error instanceof MCPError) {
27658
+ this.reportError(error);
27659
+ throw error;
27660
+ }
27661
+ const connError = new MCPConnectionError(
27662
+ `Failed to connect: ${error instanceof Error ? error.message : "Unknown error"}`
27663
+ );
27664
+ this.reportError(connError);
27665
+ throw connError;
27666
+ }
27667
+ }
27668
+ /**
27669
+ * Send a message through the transport
27670
+ */
27671
+ async send(message) {
27672
+ if (!this.connected) {
27673
+ throw new MCPTransportError("Transport not connected");
27674
+ }
27675
+ const abortController = new AbortController();
27676
+ this.pendingRequests.set(message.id, abortController);
27677
+ let lastError;
27678
+ for (let attempt = 0; attempt < this.config.retries; attempt++) {
27679
+ try {
27680
+ const timeoutId = setTimeout(() => {
27681
+ abortController.abort();
27682
+ }, this.config.timeout);
27683
+ const response = await this.sendRequestWithOAuthRetry(
27684
+ "POST",
27685
+ JSON.stringify(message),
27686
+ abortController.signal
27687
+ );
27688
+ clearTimeout(timeoutId);
27689
+ if (!response.ok) {
27690
+ throw new MCPTransportError(`HTTP error ${response.status}: ${response.statusText}`);
27691
+ }
27692
+ const data = await response.json();
27693
+ if (this.shouldAttemptOAuth() && this.isJsonRpcAuthError(data)) {
27694
+ await this.ensureOAuthToken(response.headers.get("www-authenticate"), {
27695
+ forceRefresh: true
27696
+ });
27697
+ const retryResponse = await this.sendRequestWithOAuthRetry(
27698
+ "POST",
27699
+ JSON.stringify(message),
27700
+ abortController.signal
27701
+ );
27702
+ if (!retryResponse.ok) {
27703
+ throw new MCPTransportError(
27704
+ `HTTP error ${retryResponse.status}: ${retryResponse.statusText}`
27705
+ );
27706
+ }
27707
+ const retryData = await retryResponse.json();
27708
+ this.messageCallback?.(retryData);
27709
+ return;
27710
+ }
27711
+ this.messageCallback?.(data);
27712
+ return;
27713
+ } catch (error) {
27714
+ lastError = error instanceof Error ? error : new Error(String(error));
27715
+ if (error instanceof MCPTransportError) {
27716
+ this.reportError(error);
27717
+ throw error;
27718
+ }
27719
+ if (attempt < this.config.retries - 1) {
27720
+ await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1e3));
27721
+ }
27722
+ }
27723
+ }
27724
+ this.pendingRequests.delete(message.id);
27725
+ throw new MCPTransportError(
27726
+ `Request failed after ${this.config.retries} attempts: ${lastError?.message}`
27727
+ );
27728
+ }
27729
+ /**
27730
+ * Disconnect from the transport
27731
+ */
27732
+ async disconnect() {
27733
+ for (const [, controller] of this.pendingRequests) {
27734
+ controller.abort();
27735
+ }
27736
+ this.pendingRequests.clear();
27737
+ this.abortController?.abort();
27738
+ this.connected = false;
27739
+ this.closeCallback?.();
27740
+ }
27741
+ /**
27742
+ * Set callback for received messages
27743
+ */
27744
+ onMessage(callback) {
27745
+ this.messageCallback = callback;
27746
+ }
27747
+ /**
27748
+ * Set callback for errors
27749
+ */
27750
+ onError(callback) {
27751
+ this.errorCallback = callback;
27752
+ }
27753
+ /**
27754
+ * Set callback for connection close
27755
+ */
27756
+ onClose(callback) {
27757
+ this.closeCallback = callback;
27758
+ }
27759
+ /**
27760
+ * Check if transport is connected
27761
+ */
27762
+ isConnected() {
27763
+ return this.connected;
27764
+ }
27765
+ /**
27766
+ * Get transport URL
27767
+ */
27768
+ getURL() {
27769
+ return this.config.url;
27770
+ }
27771
+ /**
27772
+ * Get auth type
27773
+ */
27774
+ getAuthType() {
27775
+ return this.config.auth?.type;
27776
+ }
27777
+ };
27778
+
27779
+ // src/mcp/transport/sse.ts
27780
+ init_errors2();
27781
+ var DEFAULT_CONFIG2 = {
27782
+ initialReconnectDelay: 1e3,
27783
+ maxReconnectDelay: 3e4,
27784
+ maxReconnectAttempts: 10
27785
+ };
27786
+ var SSETransport = class {
27787
+ config;
27788
+ connected = false;
27789
+ abortController = null;
27790
+ reconnectAttempts = 0;
27791
+ lastEventId = null;
27792
+ messageEndpoint = null;
27793
+ messageHandler = null;
27794
+ errorHandler = null;
27795
+ closeHandler = null;
27796
+ constructor(config) {
27797
+ this.config = { ...DEFAULT_CONFIG2, ...config };
27798
+ }
27799
+ /**
27800
+ * Connect to the SSE endpoint
27801
+ */
27802
+ async connect() {
27803
+ if (this.connected) return;
27804
+ this.abortController = new AbortController();
27805
+ this.reconnectAttempts = 0;
27806
+ this.connected = true;
27807
+ try {
27808
+ await this.startListening();
27809
+ } catch (error) {
27810
+ this.connected = false;
27811
+ throw error;
27812
+ }
27813
+ }
27814
+ /**
27815
+ * Disconnect from the SSE endpoint
27816
+ */
27817
+ async disconnect() {
27818
+ this.connected = false;
27819
+ this.abortController?.abort();
27820
+ this.abortController = null;
27821
+ this.messageEndpoint = null;
27822
+ this.closeHandler?.();
27823
+ }
27824
+ /**
27825
+ * Send a JSON-RPC message via HTTP POST
27826
+ */
27827
+ async send(message) {
27828
+ if (!this.connected) {
27829
+ throw new MCPConnectionError("Not connected to SSE endpoint");
27830
+ }
27831
+ const endpoint = this.messageEndpoint ?? `${this.config.url}/message`;
27832
+ try {
27833
+ const response = await fetch(endpoint, {
27834
+ method: "POST",
27835
+ headers: {
27836
+ "Content-Type": "application/json",
27837
+ ...this.config.headers
27838
+ },
27839
+ body: JSON.stringify(message),
27840
+ signal: this.abortController?.signal
27841
+ });
27842
+ if (!response.ok) {
27843
+ throw new MCPTransportError(`HTTP POST failed: ${response.status} ${response.statusText}`);
27844
+ }
27845
+ } catch (error) {
27846
+ if (error.name === "AbortError") return;
27847
+ if (error instanceof MCPTransportError) throw error;
27848
+ throw new MCPTransportError(
27849
+ `Failed to send message: ${error instanceof Error ? error.message : String(error)}`
27850
+ );
27851
+ }
27852
+ }
27853
+ /**
27854
+ * Register message handler
27855
+ */
27856
+ onMessage(handler) {
27857
+ this.messageHandler = handler;
27858
+ }
27859
+ /**
27860
+ * Register error handler
27861
+ */
27862
+ onError(handler) {
27863
+ this.errorHandler = handler;
27864
+ }
27865
+ /**
27866
+ * Register close handler
27867
+ */
27868
+ onClose(handler) {
27869
+ this.closeHandler = handler;
27870
+ }
27871
+ /**
27872
+ * Check if connected
27873
+ */
27874
+ isConnected() {
27875
+ return this.connected;
27876
+ }
27877
+ /**
27878
+ * Start listening to the SSE stream
27879
+ */
27880
+ async startListening() {
27881
+ const headers = {
27882
+ Accept: "text/event-stream",
27883
+ "Cache-Control": "no-cache",
27884
+ ...this.config.headers
27885
+ };
27886
+ if (this.lastEventId) {
27887
+ headers["Last-Event-ID"] = this.lastEventId;
27888
+ }
27889
+ try {
27890
+ const response = await fetch(this.config.url, {
27891
+ method: "GET",
27892
+ headers,
27893
+ signal: this.abortController?.signal
27894
+ });
27895
+ if (!response.ok) {
27896
+ throw new MCPConnectionError(
27897
+ `SSE connection failed: ${response.status} ${response.statusText}`
27898
+ );
27899
+ }
27900
+ if (!response.body) {
27901
+ throw new MCPConnectionError("SSE response has no body");
27902
+ }
27903
+ this.processStream(response.body);
27904
+ } catch (error) {
27905
+ if (error.name === "AbortError") return;
27906
+ if (error instanceof MCPConnectionError) throw error;
27907
+ throw new MCPConnectionError(
27908
+ `Failed to connect to SSE: ${error instanceof Error ? error.message : String(error)}`
27909
+ );
27910
+ }
27911
+ }
27912
+ /**
27913
+ * Process the SSE stream
27914
+ */
27915
+ async processStream(body) {
27916
+ const reader = body.getReader();
27917
+ const decoder = new TextDecoder();
27918
+ let buffer = "";
27919
+ let eventType = "";
27920
+ let eventData = "";
27921
+ let eventId = "";
27922
+ try {
27923
+ while (this.connected) {
27924
+ const { done, value } = await reader.read();
27925
+ if (done) {
27926
+ if (this.connected) {
27927
+ await this.handleReconnect();
27928
+ }
27929
+ return;
27930
+ }
27931
+ buffer += decoder.decode(value, { stream: true });
27932
+ const lines = buffer.split("\n");
27933
+ buffer = lines.pop() ?? "";
27934
+ for (const line of lines) {
27935
+ if (line === "") {
27936
+ if (eventData) {
27937
+ this.handleEvent(eventType, eventData, eventId);
27938
+ eventType = "";
27939
+ eventData = "";
27940
+ eventId = "";
27941
+ }
27942
+ continue;
27943
+ }
27944
+ if (line.startsWith(":")) {
27945
+ continue;
27946
+ }
27947
+ const colonIdx = line.indexOf(":");
27948
+ if (colonIdx === -1) continue;
27949
+ const field = line.slice(0, colonIdx);
27950
+ const value2 = line.slice(colonIdx + 1).trimStart();
27951
+ switch (field) {
27952
+ case "event":
27953
+ eventType = value2;
27954
+ break;
27955
+ case "data":
27956
+ eventData += (eventData ? "\n" : "") + value2;
27957
+ break;
27958
+ case "id":
27959
+ eventId = value2;
27960
+ break;
27961
+ case "retry":
27962
+ const delay = parseInt(value2, 10);
27963
+ if (!isNaN(delay)) {
27964
+ this.config.initialReconnectDelay = delay;
27965
+ }
27966
+ break;
27967
+ }
27968
+ }
27969
+ }
27970
+ } catch (error) {
27971
+ if (error.name === "AbortError") return;
27972
+ this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
27973
+ if (this.connected) {
27974
+ await this.handleReconnect();
27975
+ }
27976
+ } finally {
27977
+ reader.releaseLock();
27978
+ }
27979
+ }
27980
+ /**
27981
+ * Handle a complete SSE event
27982
+ */
27983
+ handleEvent(type, data, id) {
27984
+ if (id) {
27985
+ this.lastEventId = id;
27986
+ }
27987
+ if (type === "endpoint") {
27988
+ this.messageEndpoint = data;
27989
+ return;
27990
+ }
27991
+ try {
27992
+ const message = JSON.parse(data);
27993
+ this.messageHandler?.(message);
27994
+ } catch {
27995
+ this.errorHandler?.(new Error(`Invalid JSON in SSE event: ${data.slice(0, 100)}`));
27996
+ }
27997
+ }
27998
+ /**
27999
+ * Handle reconnection with exponential backoff
28000
+ */
28001
+ async handleReconnect() {
28002
+ if (!this.connected || this.reconnectAttempts >= this.config.maxReconnectAttempts) {
28003
+ this.connected = false;
28004
+ this.closeHandler?.();
28005
+ return;
28006
+ }
28007
+ this.reconnectAttempts++;
28008
+ const delay = Math.min(
28009
+ this.config.initialReconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
28010
+ this.config.maxReconnectDelay
28011
+ );
28012
+ await new Promise((resolve3) => setTimeout(resolve3, delay));
28013
+ if (!this.connected) return;
28014
+ try {
28015
+ await this.startListening();
28016
+ this.reconnectAttempts = 0;
28017
+ } catch {
28018
+ if (this.connected) {
28019
+ await this.handleReconnect();
28020
+ }
28021
+ }
28022
+ }
28023
+ };
28024
+
28025
+ // src/mcp/lifecycle.ts
28026
+ init_errors2();
28027
+ init_logger();
28028
+ var MCPServerManager = class {
28029
+ connections = /* @__PURE__ */ new Map();
28030
+ logger = getLogger();
28031
+ /**
28032
+ * Create transport for a server config
28033
+ */
28034
+ createTransport(config) {
28035
+ switch (config.transport) {
28036
+ case "stdio": {
28037
+ if (!config.stdio?.command) {
28038
+ throw new MCPConnectionError(`Server '${config.name}' requires stdio.command`);
28039
+ }
28040
+ return new StdioTransport({
28041
+ command: config.stdio.command,
28042
+ args: config.stdio.args ?? [],
28043
+ env: config.stdio.env
28044
+ });
28045
+ }
28046
+ case "http": {
28047
+ if (!config.http?.url) {
28048
+ throw new MCPConnectionError(`Server '${config.name}' requires http.url`);
28049
+ }
28050
+ return new HTTPTransport({
28051
+ name: config.name,
28052
+ url: config.http.url,
28053
+ headers: config.http.headers,
28054
+ auth: config.http.auth
28055
+ });
28056
+ }
28057
+ case "sse": {
28058
+ if (!config.http?.url) {
28059
+ throw new MCPConnectionError(`Server '${config.name}' requires http.url for SSE`);
28060
+ }
28061
+ return new SSETransport({
28062
+ url: config.http.url,
28063
+ headers: config.http.headers
28064
+ });
28065
+ }
28066
+ default:
28067
+ throw new MCPConnectionError(`Unsupported transport: ${config.transport}`);
28068
+ }
28069
+ }
28070
+ /**
28071
+ * Start a single server
28072
+ */
28073
+ async startServer(config) {
28074
+ if (this.connections.has(config.name)) {
28075
+ this.logger.warn(`Server '${config.name}' already connected`);
28076
+ return this.connections.get(config.name);
28077
+ }
28078
+ this.logger.info(`Starting MCP server: ${config.name}`);
28079
+ const transport = this.createTransport(config);
28080
+ await transport.connect();
28081
+ const client = new MCPClientImpl(transport);
28082
+ await client.initialize({
28083
+ protocolVersion: "2024-11-05",
28084
+ capabilities: {},
28085
+ clientInfo: { name: "coco-mcp-client", version: VERSION }
28086
+ });
28087
+ let toolCount = 0;
28088
+ try {
28089
+ const { tools } = await client.listTools();
28090
+ toolCount = tools.length;
28091
+ } catch {
28092
+ }
28093
+ const connection = {
28094
+ name: config.name,
28095
+ client,
28096
+ transport,
28097
+ config,
28098
+ connectedAt: /* @__PURE__ */ new Date(),
28099
+ toolCount,
28100
+ healthy: true
28101
+ };
28102
+ this.connections.set(config.name, connection);
28103
+ this.logger.info(`Server '${config.name}' started with ${toolCount} tools`);
28104
+ return connection;
28105
+ }
28106
+ /**
28107
+ * Stop a single server
28108
+ */
28109
+ async stopServer(name) {
28110
+ const connection = this.connections.get(name);
28111
+ if (!connection) {
28112
+ this.logger.warn(`Server '${name}' not found`);
28113
+ return;
28114
+ }
28115
+ this.logger.info(`Stopping MCP server: ${name}`);
28116
+ try {
28117
+ await connection.transport.disconnect();
28118
+ } catch (error) {
28119
+ this.logger.error(
28120
+ `Error disconnecting server '${name}': ${error instanceof Error ? error.message : String(error)}`
28121
+ );
28122
+ }
28123
+ this.connections.delete(name);
28124
+ }
28125
+ /**
28126
+ * Restart a server
28127
+ */
28128
+ async restartServer(name) {
28129
+ const connection = this.connections.get(name);
28130
+ if (!connection) {
28131
+ throw new MCPConnectionError(`Server '${name}' not found`);
28132
+ }
28133
+ const config = connection.config;
28134
+ await this.stopServer(name);
28135
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
28136
+ return this.startServer(config);
28137
+ }
28138
+ /**
28139
+ * Health check for a server
28140
+ */
28141
+ async healthCheck(name) {
28142
+ const connection = this.connections.get(name);
28143
+ if (!connection) {
28144
+ return {
28145
+ name,
28146
+ healthy: false,
28147
+ toolCount: 0,
28148
+ latencyMs: 0,
28149
+ error: "Server not connected"
28150
+ };
28151
+ }
28152
+ const startTime = performance.now();
28153
+ try {
28154
+ const { tools } = await Promise.race([
28155
+ connection.client.listTools(),
28156
+ new Promise(
28157
+ (_, reject) => setTimeout(() => reject(new Error("Health check timeout")), 5e3)
28158
+ )
28159
+ ]);
28160
+ const latencyMs = performance.now() - startTime;
28161
+ connection.healthy = true;
28162
+ connection.toolCount = tools.length;
28163
+ return {
28164
+ name,
28165
+ healthy: true,
28166
+ toolCount: tools.length,
28167
+ latencyMs
28168
+ };
28169
+ } catch (error) {
28170
+ const latencyMs = performance.now() - startTime;
28171
+ connection.healthy = false;
28172
+ return {
28173
+ name,
28174
+ healthy: false,
28175
+ toolCount: 0,
28176
+ latencyMs,
28177
+ error: error instanceof Error ? error.message : String(error)
28178
+ };
28179
+ }
28180
+ }
28181
+ /**
28182
+ * Start all servers from config list
28183
+ */
28184
+ async startAll(configs) {
28185
+ const results = /* @__PURE__ */ new Map();
28186
+ for (const config of configs) {
28187
+ if (config.enabled === false) continue;
28188
+ try {
28189
+ const connection = await this.startServer(config);
28190
+ results.set(config.name, connection);
28191
+ } catch (error) {
28192
+ this.logger.error(
28193
+ `Failed to start server '${config.name}': ${error instanceof Error ? error.message : String(error)}`
28194
+ );
28195
+ }
28196
+ }
28197
+ return results;
28198
+ }
28199
+ /**
28200
+ * Stop all servers
28201
+ */
28202
+ async stopAll() {
28203
+ const names = Array.from(this.connections.keys());
28204
+ for (const name of names) {
28205
+ await this.stopServer(name);
28206
+ }
28207
+ }
28208
+ /**
28209
+ * Get list of connected server names
28210
+ */
28211
+ getConnectedServers() {
28212
+ return Array.from(this.connections.keys());
28213
+ }
28214
+ /**
28215
+ * Get a specific server connection
28216
+ */
28217
+ getConnection(name) {
28218
+ return this.connections.get(name);
28219
+ }
28220
+ /**
28221
+ * Get all connections
28222
+ */
28223
+ getAllConnections() {
28224
+ return Array.from(this.connections.values());
28225
+ }
28226
+ /**
28227
+ * Get the client for a server
28228
+ */
28229
+ getClient(name) {
28230
+ return this.connections.get(name)?.client;
28231
+ }
28232
+ };
28233
+ var globalManager = null;
28234
+ function getMCPServerManager() {
28235
+ if (!globalManager) {
28236
+ globalManager = new MCPServerManager();
28237
+ }
28238
+ return globalManager;
28239
+ }
28240
+
28241
+ // src/tools/mcp.ts
28242
+ var mcpListServersTool = defineTool({
28243
+ name: "mcp_list_servers",
28244
+ description: `Inspect Coco's MCP configuration and current runtime connections.
28245
+
28246
+ Use this instead of bash_exec with "coco mcp ..." and instead of manually reading ~/.coco/mcp.json
28247
+ when you need to know which MCP servers are configured, connected, healthy, or which tools they expose.`,
28248
+ category: "config",
28249
+ parameters: z.object({
28250
+ includeDisabled: z.boolean().optional().default(false).describe("Include disabled MCP servers in the result"),
28251
+ includeTools: z.boolean().optional().default(false).describe("Include the list of exposed tool names for connected servers"),
28252
+ projectPath: z.string().optional().describe("Project path whose .mcp.json should be merged. Defaults to process.cwd()")
28253
+ }),
28254
+ async execute({ includeDisabled, includeTools, projectPath }) {
28255
+ const registry = new MCPRegistryImpl();
28256
+ await registry.load();
28257
+ const resolvedProjectPath = projectPath || process.cwd();
28258
+ const configuredServers = mergeMCPConfigs(
28259
+ registry.listServers(),
28260
+ await loadMCPServersFromCOCOConfig(),
28261
+ await loadProjectMCPFile(resolvedProjectPath)
28262
+ ).filter((server) => includeDisabled || server.enabled !== false);
28263
+ const manager = getMCPServerManager();
28264
+ const servers = [];
28265
+ for (const server of configuredServers) {
28266
+ const connection = manager.getConnection(server.name);
28267
+ let tools;
28268
+ if (includeTools && connection) {
28269
+ try {
28270
+ const listed = await connection.client.listTools();
28271
+ tools = listed.tools.map((tool) => tool.name);
28272
+ } catch {
28273
+ tools = [];
28274
+ }
28275
+ }
28276
+ servers.push({
28277
+ name: server.name,
28278
+ transport: server.transport,
28279
+ enabled: server.enabled !== false,
28280
+ connected: connection !== void 0,
28281
+ healthy: connection?.healthy ?? false,
28282
+ toolCount: connection?.toolCount ?? 0,
28283
+ ...includeTools ? { tools: tools ?? [] } : {}
28284
+ });
28285
+ }
28286
+ return {
28287
+ configuredCount: servers.length,
28288
+ connectedCount: servers.filter((server) => server.connected).length,
28289
+ servers
28290
+ };
28291
+ }
28292
+ });
28293
+ var mcpTools = [mcpListServersTool];
25785
28294
  init_allowed_paths();
25786
28295
  var BLOCKED_SYSTEM_PATHS = [
25787
28296
  "/etc",
@@ -25915,6 +28424,7 @@ function registerAllTools(registry) {
25915
28424
  ...gitEnhancedTools,
25916
28425
  ...githubTools,
25917
28426
  ...openTools,
28427
+ ...mcpTools,
25918
28428
  ...authorizePathTools
25919
28429
  ];
25920
28430
  for (const tool of allTools) {
@@ -25929,6 +28439,7 @@ function createFullToolRegistry() {
25929
28439
 
25930
28440
  // src/index.ts
25931
28441
  init_errors();
28442
+ init_logger();
25932
28443
 
25933
28444
  export { ADRGenerator, AnthropicProvider, ArchitectureGenerator, BacklogGenerator, CICDGenerator, CocoError, CodeGenerator, CodeReviewer, CompleteExecutor, ConfigError, ConvergeExecutor, DiscoveryEngine, DockerGenerator, DocsGenerator, OrchestrateExecutor, OutputExecutor, PhaseError, SessionManager, SpecificationGenerator, TaskError, TaskIterator, ToolRegistry, VERSION, configExists, createADRGenerator, createAnthropicProvider, createArchitectureGenerator, createBacklogGenerator, createCICDGenerator, createCodeGenerator, createCodeReviewer, createCompleteExecutor, createConvergeExecutor, createDefaultConfig, createDiscoveryEngine, createDockerGenerator, createDocsGenerator, createFullToolRegistry, createLogger, createOrchestrateExecutor, createOrchestrator, createOutputExecutor, createProvider, createSessionManager, createSpecificationGenerator, createTaskIterator, createToolRegistry, loadConfig, registerAllTools, saveConfig };
25934
28445
  //# sourceMappingURL=index.js.map