@corbat-tech/coco 1.9.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { homedir } from 'os';
2
- import * as path14 from 'path';
3
- import path14__default, { dirname, join, basename } from 'path';
2
+ import * as path15 from 'path';
3
+ import path15__default, { dirname, join, basename, resolve } from 'path';
4
4
  import * as fs4 from 'fs';
5
5
  import fs4__default, { readFileSync, constants } from 'fs';
6
6
  import * as fs14 from 'fs/promises';
@@ -76,7 +76,9 @@ var init_paths = __esm({
76
76
  /** Checkpoints directory: ~/.coco/checkpoints/ */
77
77
  checkpoints: join(COCO_HOME, "checkpoints"),
78
78
  /** Search index directory: ~/.coco/search-index/ */
79
- searchIndex: join(COCO_HOME, "search-index")
79
+ searchIndex: join(COCO_HOME, "search-index"),
80
+ /** Global skills directory: ~/.coco/skills/ */
81
+ skills: join(COCO_HOME, "skills")
80
82
  };
81
83
  ({
82
84
  /** Old config location */
@@ -88,7 +90,7 @@ function loadGlobalCocoEnv() {
88
90
  try {
89
91
  const home = process.env.HOME || process.env.USERPROFILE || "";
90
92
  if (!home) return;
91
- const globalEnvPath = path14.join(home, ".coco", ".env");
93
+ const globalEnvPath = path15.join(home, ".coco", ".env");
92
94
  const content = fs4.readFileSync(globalEnvPath, "utf-8");
93
95
  for (const line of content.split("\n")) {
94
96
  const trimmed = line.trim();
@@ -116,12 +118,26 @@ function getApiKey(provider) {
116
118
  return process.env["GEMINI_API_KEY"] ?? process.env["GOOGLE_API_KEY"];
117
119
  case "kimi":
118
120
  return process.env["KIMI_API_KEY"] ?? process.env["MOONSHOT_API_KEY"];
121
+ case "kimi-code":
122
+ return process.env["KIMI_CODE_API_KEY"];
119
123
  case "lmstudio":
120
124
  return process.env["LMSTUDIO_API_KEY"] ?? "lm-studio";
121
125
  case "ollama":
122
126
  return process.env["OLLAMA_API_KEY"] ?? "ollama";
123
127
  case "codex":
124
128
  return void 0;
129
+ case "groq":
130
+ return process.env["GROQ_API_KEY"];
131
+ case "openrouter":
132
+ return process.env["OPENROUTER_API_KEY"];
133
+ case "mistral":
134
+ return process.env["MISTRAL_API_KEY"];
135
+ case "deepseek":
136
+ return process.env["DEEPSEEK_API_KEY"];
137
+ case "together":
138
+ return process.env["TOGETHER_API_KEY"];
139
+ case "huggingface":
140
+ return process.env["HF_TOKEN"] ?? process.env["HUGGINGFACE_API_KEY"];
125
141
  default:
126
142
  return void 0;
127
143
  }
@@ -134,12 +150,26 @@ function getBaseUrl(provider) {
134
150
  return process.env["OPENAI_BASE_URL"];
135
151
  case "kimi":
136
152
  return process.env["KIMI_BASE_URL"] ?? "https://api.moonshot.ai/v1";
153
+ case "kimi-code":
154
+ return process.env["KIMI_CODE_BASE_URL"] ?? "https://api.kimi.com/coding/v1";
137
155
  case "lmstudio":
138
156
  return process.env["LMSTUDIO_BASE_URL"] ?? "http://localhost:1234/v1";
139
157
  case "ollama":
140
158
  return process.env["OLLAMA_BASE_URL"] ?? "http://localhost:11434/v1";
141
159
  case "codex":
142
160
  return "https://chatgpt.com/backend-api/codex/responses";
161
+ case "groq":
162
+ return process.env["GROQ_BASE_URL"] ?? "https://api.groq.com/openai/v1";
163
+ case "openrouter":
164
+ return process.env["OPENROUTER_BASE_URL"] ?? "https://openrouter.ai/api/v1";
165
+ case "mistral":
166
+ return process.env["MISTRAL_BASE_URL"] ?? "https://api.mistral.ai/v1";
167
+ case "deepseek":
168
+ return process.env["DEEPSEEK_BASE_URL"] ?? "https://api.deepseek.com/v1";
169
+ case "together":
170
+ return process.env["TOGETHER_BASE_URL"] ?? "https://api.together.xyz/v1";
171
+ case "huggingface":
172
+ return process.env["HF_BASE_URL"] ?? "https://api-inference.huggingface.co/v1";
143
173
  default:
144
174
  return void 0;
145
175
  }
@@ -154,27 +184,58 @@ function getDefaultModel(provider) {
154
184
  return process.env["GEMINI_MODEL"] ?? "gemini-3-flash-preview";
155
185
  case "kimi":
156
186
  return process.env["KIMI_MODEL"] ?? "kimi-k2.5";
187
+ case "kimi-code":
188
+ return process.env["KIMI_CODE_MODEL"] ?? "kimi-for-coding";
157
189
  case "lmstudio":
158
190
  return process.env["LMSTUDIO_MODEL"] ?? "local-model";
159
191
  case "ollama":
160
192
  return process.env["OLLAMA_MODEL"] ?? "llama3.1";
161
193
  case "codex":
162
194
  return process.env["CODEX_MODEL"] ?? "gpt-5.3-codex";
195
+ case "groq":
196
+ return process.env["GROQ_MODEL"] ?? "llama-3.3-70b-versatile";
197
+ case "openrouter":
198
+ return process.env["OPENROUTER_MODEL"] ?? "anthropic/claude-opus-4-6";
199
+ case "mistral":
200
+ return process.env["MISTRAL_MODEL"] ?? "codestral-latest";
201
+ case "deepseek":
202
+ return process.env["DEEPSEEK_MODEL"] ?? "deepseek-coder";
203
+ case "together":
204
+ return process.env["TOGETHER_MODEL"] ?? "Qwen/Qwen2.5-Coder-32B-Instruct";
205
+ case "huggingface":
206
+ return process.env["HF_MODEL"] ?? "Qwen/Qwen2.5-Coder-32B-Instruct";
163
207
  default:
164
208
  return "gpt-5.3-codex";
165
209
  }
166
210
  }
167
211
  function getDefaultProvider() {
168
212
  const provider = process.env["COCO_PROVIDER"]?.toLowerCase();
169
- if (provider && ["anthropic", "openai", "codex", "gemini", "kimi", "lmstudio", "ollama"].includes(provider)) {
213
+ if (provider && VALID_PROVIDERS.includes(provider)) {
170
214
  return provider;
171
215
  }
172
216
  return "anthropic";
173
217
  }
218
+ var VALID_PROVIDERS;
174
219
  var init_env = __esm({
175
220
  "src/config/env.ts"() {
176
221
  init_paths();
177
222
  loadGlobalCocoEnv();
223
+ VALID_PROVIDERS = [
224
+ "anthropic",
225
+ "openai",
226
+ "codex",
227
+ "gemini",
228
+ "kimi",
229
+ "kimi-code",
230
+ "lmstudio",
231
+ "ollama",
232
+ "groq",
233
+ "openrouter",
234
+ "mistral",
235
+ "deepseek",
236
+ "together",
237
+ "huggingface"
238
+ ];
178
239
  ({
179
240
  provider: getDefaultProvider(),
180
241
  getApiKey,
@@ -187,10 +248,10 @@ function getAllowedPaths() {
187
248
  return [...sessionAllowedPaths];
188
249
  }
189
250
  function isWithinAllowedPath(absolutePath, operation) {
190
- const normalizedTarget = path14__default.normalize(absolutePath);
251
+ const normalizedTarget = path15__default.normalize(absolutePath);
191
252
  for (const entry of sessionAllowedPaths) {
192
- const normalizedAllowed = path14__default.normalize(entry.path);
193
- if (normalizedTarget === normalizedAllowed || normalizedTarget.startsWith(normalizedAllowed + path14__default.sep)) {
253
+ const normalizedAllowed = path15__default.normalize(entry.path);
254
+ if (normalizedTarget === normalizedAllowed || normalizedTarget.startsWith(normalizedAllowed + path15__default.sep)) {
194
255
  if (operation === "read") return true;
195
256
  if (entry.level === "write") return true;
196
257
  }
@@ -198,8 +259,8 @@ function isWithinAllowedPath(absolutePath, operation) {
198
259
  return false;
199
260
  }
200
261
  function addAllowedPathToSession(dirPath, level) {
201
- const absolute = path14__default.resolve(dirPath);
202
- if (sessionAllowedPaths.some((e) => path14__default.normalize(e.path) === path14__default.normalize(absolute))) {
262
+ const absolute = path15__default.resolve(dirPath);
263
+ if (sessionAllowedPaths.some((e) => path15__default.normalize(e.path) === path15__default.normalize(absolute))) {
203
264
  return;
204
265
  }
205
266
  sessionAllowedPaths.push({
@@ -210,14 +271,14 @@ function addAllowedPathToSession(dirPath, level) {
210
271
  }
211
272
  async function persistAllowedPath(dirPath, level) {
212
273
  if (!currentProjectPath) return;
213
- const absolute = path14__default.resolve(dirPath);
274
+ const absolute = path15__default.resolve(dirPath);
214
275
  const store = await loadStore();
215
276
  if (!store.projects[currentProjectPath]) {
216
277
  store.projects[currentProjectPath] = [];
217
278
  }
218
279
  const entries = store.projects[currentProjectPath];
219
- const normalized = path14__default.normalize(absolute);
220
- if (entries.some((e) => path14__default.normalize(e.path) === normalized)) {
280
+ const normalized = path15__default.normalize(absolute);
281
+ if (entries.some((e) => path15__default.normalize(e.path) === normalized)) {
221
282
  return;
222
283
  }
223
284
  entries.push({
@@ -237,7 +298,7 @@ async function loadStore() {
237
298
  }
238
299
  async function saveStore(store) {
239
300
  try {
240
- await fs14__default.mkdir(path14__default.dirname(STORE_FILE), { recursive: true });
301
+ await fs14__default.mkdir(path15__default.dirname(STORE_FILE), { recursive: true });
241
302
  await fs14__default.writeFile(STORE_FILE, JSON.stringify(store, null, 2), "utf-8");
242
303
  } catch {
243
304
  }
@@ -246,7 +307,7 @@ var STORE_FILE, DEFAULT_STORE, sessionAllowedPaths, currentProjectPath;
246
307
  var init_allowed_paths = __esm({
247
308
  "src/tools/allowed-paths.ts"() {
248
309
  init_paths();
249
- STORE_FILE = path14__default.join(CONFIG_PATHS.home, "allowed-paths.json");
310
+ STORE_FILE = path15__default.join(CONFIG_PATHS.home, "allowed-paths.json");
250
311
  DEFAULT_STORE = {
251
312
  version: 1,
252
313
  projects: {}
@@ -327,7 +388,7 @@ __export(allow_path_prompt_exports, {
327
388
  promptAllowPath: () => promptAllowPath
328
389
  });
329
390
  async function promptAllowPath(dirPath) {
330
- const absolute = path14__default.resolve(dirPath);
391
+ const absolute = path15__default.resolve(dirPath);
331
392
  console.log();
332
393
  console.log(chalk3.yellow(" \u26A0 Access denied \u2014 path is outside the project directory"));
333
394
  console.log(chalk3.dim(` \u{1F4C1} ${absolute}`));
@@ -1705,13 +1766,13 @@ function createSpecificationGenerator(llm, config) {
1705
1766
  return new SpecificationGenerator(llm, config);
1706
1767
  }
1707
1768
  function getPersistencePaths(projectPath) {
1708
- const baseDir = path14__default.join(projectPath, ".coco", "spec");
1769
+ const baseDir = path15__default.join(projectPath, ".coco", "spec");
1709
1770
  return {
1710
1771
  baseDir,
1711
- sessionFile: path14__default.join(baseDir, "discovery-session.json"),
1712
- specFile: path14__default.join(baseDir, "spec.md"),
1713
- conversationLog: path14__default.join(baseDir, "conversation.jsonl"),
1714
- checkpointFile: path14__default.join(baseDir, "checkpoint.json")
1772
+ sessionFile: path15__default.join(baseDir, "discovery-session.json"),
1773
+ specFile: path15__default.join(baseDir, "spec.md"),
1774
+ conversationLog: path15__default.join(baseDir, "conversation.jsonl"),
1775
+ checkpointFile: path15__default.join(baseDir, "checkpoint.json")
1715
1776
  };
1716
1777
  }
1717
1778
  var SessionPersistence = class {
@@ -3741,7 +3802,7 @@ var OrchestrateExecutor = class {
3741
3802
  }
3742
3803
  async loadSpecification(projectPath) {
3743
3804
  try {
3744
- const jsonPath = path14__default.join(projectPath, ".coco", "spec", "spec.json");
3805
+ const jsonPath = path15__default.join(projectPath, ".coco", "spec", "spec.json");
3745
3806
  const jsonContent = await fs14__default.readFile(jsonPath, "utf-8");
3746
3807
  return JSON.parse(jsonContent);
3747
3808
  } catch {
@@ -3753,7 +3814,7 @@ var OrchestrateExecutor = class {
3753
3814
  version: "1.0.0",
3754
3815
  generatedAt: /* @__PURE__ */ new Date(),
3755
3816
  overview: {
3756
- name: path14__default.basename(projectPath),
3817
+ name: path15__default.basename(projectPath),
3757
3818
  description: "Project specification",
3758
3819
  goals: [],
3759
3820
  targetUsers: ["developers"],
@@ -3780,52 +3841,52 @@ var OrchestrateExecutor = class {
3780
3841
  };
3781
3842
  }
3782
3843
  async saveArchitecture(projectPath, architecture) {
3783
- const dir = path14__default.join(projectPath, ".coco", "architecture");
3844
+ const dir = path15__default.join(projectPath, ".coco", "architecture");
3784
3845
  await fs14__default.mkdir(dir, { recursive: true });
3785
- const mdPath = path14__default.join(dir, "ARCHITECTURE.md");
3846
+ const mdPath = path15__default.join(dir, "ARCHITECTURE.md");
3786
3847
  await fs14__default.writeFile(mdPath, generateArchitectureMarkdown(architecture), "utf-8");
3787
- const jsonPath = path14__default.join(dir, "architecture.json");
3848
+ const jsonPath = path15__default.join(dir, "architecture.json");
3788
3849
  await fs14__default.writeFile(jsonPath, JSON.stringify(architecture, null, 2), "utf-8");
3789
3850
  return mdPath;
3790
3851
  }
3791
3852
  async saveADRs(projectPath, adrs) {
3792
- const dir = path14__default.join(projectPath, ".coco", "architecture", "adrs");
3853
+ const dir = path15__default.join(projectPath, ".coco", "architecture", "adrs");
3793
3854
  await fs14__default.mkdir(dir, { recursive: true });
3794
3855
  const paths = [];
3795
- const indexPath = path14__default.join(dir, "README.md");
3856
+ const indexPath = path15__default.join(dir, "README.md");
3796
3857
  await fs14__default.writeFile(indexPath, generateADRIndexMarkdown(adrs), "utf-8");
3797
3858
  paths.push(indexPath);
3798
3859
  for (const adr of adrs) {
3799
3860
  const filename = getADRFilename(adr);
3800
- const adrPath = path14__default.join(dir, filename);
3861
+ const adrPath = path15__default.join(dir, filename);
3801
3862
  await fs14__default.writeFile(adrPath, generateADRMarkdown(adr), "utf-8");
3802
3863
  paths.push(adrPath);
3803
3864
  }
3804
3865
  return paths;
3805
3866
  }
3806
3867
  async saveBacklog(projectPath, backlogResult) {
3807
- const dir = path14__default.join(projectPath, ".coco", "planning");
3868
+ const dir = path15__default.join(projectPath, ".coco", "planning");
3808
3869
  await fs14__default.mkdir(dir, { recursive: true });
3809
- const mdPath = path14__default.join(dir, "BACKLOG.md");
3870
+ const mdPath = path15__default.join(dir, "BACKLOG.md");
3810
3871
  await fs14__default.writeFile(mdPath, generateBacklogMarkdown(backlogResult.backlog), "utf-8");
3811
- const jsonPath = path14__default.join(dir, "backlog.json");
3872
+ const jsonPath = path15__default.join(dir, "backlog.json");
3812
3873
  await fs14__default.writeFile(jsonPath, JSON.stringify(backlogResult, null, 2), "utf-8");
3813
3874
  return mdPath;
3814
3875
  }
3815
3876
  async saveSprint(projectPath, sprint, backlogResult) {
3816
- const dir = path14__default.join(projectPath, ".coco", "planning", "sprints");
3877
+ const dir = path15__default.join(projectPath, ".coco", "planning", "sprints");
3817
3878
  await fs14__default.mkdir(dir, { recursive: true });
3818
3879
  const filename = `${sprint.id}.md`;
3819
- const sprintPath = path14__default.join(dir, filename);
3880
+ const sprintPath = path15__default.join(dir, filename);
3820
3881
  await fs14__default.writeFile(sprintPath, generateSprintMarkdown(sprint, backlogResult.backlog), "utf-8");
3821
- const jsonPath = path14__default.join(dir, `${sprint.id}.json`);
3882
+ const jsonPath = path15__default.join(dir, `${sprint.id}.json`);
3822
3883
  await fs14__default.writeFile(jsonPath, JSON.stringify(sprint, null, 2), "utf-8");
3823
3884
  return sprintPath;
3824
3885
  }
3825
3886
  async saveDiagram(projectPath, id, mermaid) {
3826
- const dir = path14__default.join(projectPath, ".coco", "architecture", "diagrams");
3887
+ const dir = path15__default.join(projectPath, ".coco", "architecture", "diagrams");
3827
3888
  await fs14__default.mkdir(dir, { recursive: true });
3828
- const diagramPath = path14__default.join(dir, `${id}.mmd`);
3889
+ const diagramPath = path15__default.join(dir, `${id}.mmd`);
3829
3890
  await fs14__default.writeFile(diagramPath, mermaid, "utf-8");
3830
3891
  return diagramPath;
3831
3892
  }
@@ -3875,7 +3936,11 @@ var DEFAULT_QUALITY_THRESHOLDS = {
3875
3936
  target: {
3876
3937
  overall: 95,
3877
3938
  testCoverage: 90
3878
- }};
3939
+ },
3940
+ convergenceThreshold: 2,
3941
+ maxIterations: 10,
3942
+ minIterations: 2
3943
+ };
3879
3944
  async function detectTestFramework(projectPath) {
3880
3945
  try {
3881
3946
  const pkgPath = join(projectPath, "package.json");
@@ -3966,10 +4031,10 @@ var CoverageAnalyzer = class {
3966
4031
  join(this.projectPath, ".coverage", "coverage-summary.json"),
3967
4032
  join(this.projectPath, "coverage", "lcov-report", "coverage-summary.json")
3968
4033
  ];
3969
- for (const path37 of possiblePaths) {
4034
+ for (const path38 of possiblePaths) {
3970
4035
  try {
3971
- await access(path37, constants.R_OK);
3972
- const content = await readFile(path37, "utf-8");
4036
+ await access(path38, constants.R_OK);
4037
+ const content = await readFile(path38, "utf-8");
3973
4038
  const report = JSON.parse(content);
3974
4039
  return parseCoverageSummary(report);
3975
4040
  } catch {
@@ -4057,7 +4122,7 @@ var SECURITY_PATTERNS = [
4057
4122
  },
4058
4123
  // Command Injection
4059
4124
  {
4060
- regex: /exec\s*\(/g,
4125
+ regex: /(?:^|[\s;,({])exec\s*\(/gm,
4061
4126
  severity: "critical",
4062
4127
  type: "Command Injection",
4063
4128
  message: "Use of exec() can execute shell commands with user input",
@@ -4178,6 +4243,26 @@ var SECURITY_PATTERNS = [
4178
4243
  recommendation: "Validate JSON structure with JSON schema or Zod before parsing."
4179
4244
  }
4180
4245
  ];
4246
+ function computeSecurityScore(vulnerabilities) {
4247
+ let score = 100;
4248
+ for (const vuln of vulnerabilities) {
4249
+ switch (vuln.severity) {
4250
+ case "critical":
4251
+ score -= 25;
4252
+ break;
4253
+ case "high":
4254
+ score -= 10;
4255
+ break;
4256
+ case "medium":
4257
+ score -= 5;
4258
+ break;
4259
+ case "low":
4260
+ score -= 2;
4261
+ break;
4262
+ }
4263
+ }
4264
+ return Math.max(0, score);
4265
+ }
4181
4266
  var PatternSecurityScanner = class {
4182
4267
  /**
4183
4268
  * Scan files for security vulnerabilities using pattern matching
@@ -4228,32 +4313,8 @@ var PatternSecurityScanner = class {
4228
4313
  const lastLine = lines[lines.length - 1] ?? "";
4229
4314
  return lastLine.length + 1;
4230
4315
  }
4231
- /**
4232
- * Calculate security score based on vulnerabilities
4233
- * Critical: -25 points each
4234
- * High: -10 points each
4235
- * Medium: -5 points each
4236
- * Low: -2 points each
4237
- */
4238
4316
  calculateScore(vulnerabilities) {
4239
- let score = 100;
4240
- for (const vuln of vulnerabilities) {
4241
- switch (vuln.severity) {
4242
- case "critical":
4243
- score -= 25;
4244
- break;
4245
- case "high":
4246
- score -= 10;
4247
- break;
4248
- case "medium":
4249
- score -= 5;
4250
- break;
4251
- case "low":
4252
- score -= 2;
4253
- break;
4254
- }
4255
- }
4256
- return Math.max(0, score);
4317
+ return computeSecurityScore(vulnerabilities);
4257
4318
  }
4258
4319
  };
4259
4320
  var SnykSecurityScanner = class {
@@ -4309,24 +4370,7 @@ var SnykSecurityScanner = class {
4309
4370
  }));
4310
4371
  }
4311
4372
  calculateScore(vulnerabilities) {
4312
- let score = 100;
4313
- for (const vuln of vulnerabilities) {
4314
- switch (vuln.severity) {
4315
- case "critical":
4316
- score -= 25;
4317
- break;
4318
- case "high":
4319
- score -= 10;
4320
- break;
4321
- case "medium":
4322
- score -= 5;
4323
- break;
4324
- case "low":
4325
- score -= 2;
4326
- break;
4327
- }
4328
- }
4329
- return Math.max(0, score);
4373
+ return computeSecurityScore(vulnerabilities);
4330
4374
  }
4331
4375
  };
4332
4376
  var CompositeSecurityScanner = class {
@@ -4646,6 +4690,24 @@ var BuildVerifier = class {
4646
4690
  stderr: ""
4647
4691
  };
4648
4692
  }
4693
+ const SAFE_BUILD_PATTERN = /^(npm|pnpm|yarn|bun)\s+(run\s+)?[\w:.-]+$|^npx\s+tsc(\s+--[\w-]+)*$/;
4694
+ if (!SAFE_BUILD_PATTERN.test(buildCommand.trim())) {
4695
+ return {
4696
+ success: false,
4697
+ errors: [
4698
+ {
4699
+ file: "",
4700
+ line: 0,
4701
+ column: 0,
4702
+ message: `Unsafe build command rejected: ${buildCommand}`
4703
+ }
4704
+ ],
4705
+ warnings: [],
4706
+ duration: Date.now() - startTime,
4707
+ stdout: "",
4708
+ stderr: ""
4709
+ };
4710
+ }
4649
4711
  const { stdout, stderr } = await execAsync(buildCommand, {
4650
4712
  cwd: this.projectPath,
4651
4713
  timeout: 12e4,
@@ -4687,7 +4749,7 @@ var BuildVerifier = class {
4687
4749
  async verifyTypes() {
4688
4750
  const startTime = Date.now();
4689
4751
  try {
4690
- const hasTsConfig = await this.fileExists(path14.join(this.projectPath, "tsconfig.json"));
4752
+ const hasTsConfig = await this.fileExists(path15.join(this.projectPath, "tsconfig.json"));
4691
4753
  if (!hasTsConfig) {
4692
4754
  return {
4693
4755
  success: true,
@@ -4737,7 +4799,7 @@ var BuildVerifier = class {
4737
4799
  */
4738
4800
  async detectBuildCommand() {
4739
4801
  try {
4740
- const packageJsonPath = path14.join(this.projectPath, "package.json");
4802
+ const packageJsonPath = path15.join(this.projectPath, "package.json");
4741
4803
  const content = await fs14.readFile(packageJsonPath, "utf-8");
4742
4804
  const packageJson = JSON.parse(content);
4743
4805
  if (packageJson.scripts?.build) {
@@ -5154,10 +5216,6 @@ function analyzeRobustnessPatterns(ast) {
5154
5216
  if (currentFunctionHasTryCatch) {
5155
5217
  functionsWithTryCatch++;
5156
5218
  }
5157
- if (isFunctionNode) {
5158
- functions--;
5159
- functions++;
5160
- }
5161
5219
  insideFunction = previousInsideFunction;
5162
5220
  currentFunctionHasTryCatch = previousHasTryCatch;
5163
5221
  return;
@@ -5989,127 +6047,1511 @@ function analyzeMaintainabilityPatterns(ast, filePath) {
5989
6047
  traverse(child);
5990
6048
  }
5991
6049
  }
5992
- }
6050
+ }
6051
+ }
6052
+ traverse(ast);
6053
+ return {
6054
+ lineCount: 0,
6055
+ // Will be set separately from content
6056
+ functionCount,
6057
+ importCount,
6058
+ crossBoundaryImportCount
6059
+ };
6060
+ }
6061
+ function isCrossBoundaryImport(node, _filePath) {
6062
+ if (node.source.type !== "Literal" || typeof node.source.value !== "string") {
6063
+ return false;
6064
+ }
6065
+ const importPath = node.source.value;
6066
+ if (importPath.startsWith("node:")) {
6067
+ return false;
6068
+ }
6069
+ if (!importPath.startsWith(".")) {
6070
+ return false;
6071
+ }
6072
+ if (importPath.startsWith("../")) {
6073
+ return true;
6074
+ }
6075
+ if (importPath.startsWith("./")) {
6076
+ const relativePath = importPath.slice(2);
6077
+ return relativePath.includes("/");
6078
+ }
6079
+ return false;
6080
+ }
6081
+ function countLines(content) {
6082
+ return content.split("\n").length;
6083
+ }
6084
+ var MaintainabilityAnalyzer = class {
6085
+ constructor(projectPath) {
6086
+ this.projectPath = projectPath;
6087
+ }
6088
+ /**
6089
+ * Analyze maintainability of project files
6090
+ */
6091
+ async analyze(files) {
6092
+ const targetFiles = files ?? await this.findSourceFiles();
6093
+ if (targetFiles.length === 0) {
6094
+ return {
6095
+ score: 100,
6096
+ fileLengthScore: 100,
6097
+ functionCountScore: 100,
6098
+ dependencyCountScore: 100,
6099
+ couplingScore: 100,
6100
+ averageFileLength: 0,
6101
+ averageFunctionsPerFile: 0,
6102
+ averageImportsPerFile: 0,
6103
+ fileCount: 0,
6104
+ details: "No files to analyze"
6105
+ };
6106
+ }
6107
+ let totalLines = 0;
6108
+ let totalFunctions = 0;
6109
+ let totalImports = 0;
6110
+ let totalCrossBoundaryImports = 0;
6111
+ let filesAnalyzed = 0;
6112
+ for (const file of targetFiles) {
6113
+ try {
6114
+ const content = await readFile(file, "utf-8");
6115
+ const lineCount = countLines(content);
6116
+ const ast = parse(content, {
6117
+ loc: true,
6118
+ range: true,
6119
+ jsx: file.endsWith(".tsx") || file.endsWith(".jsx")
6120
+ });
6121
+ const result = analyzeMaintainabilityPatterns(ast, file);
6122
+ totalLines += lineCount;
6123
+ totalFunctions += result.functionCount;
6124
+ totalImports += result.importCount;
6125
+ totalCrossBoundaryImports += result.crossBoundaryImportCount;
6126
+ filesAnalyzed++;
6127
+ } catch {
6128
+ }
6129
+ }
6130
+ const averageFileLength = filesAnalyzed > 0 ? totalLines / filesAnalyzed : 0;
6131
+ const averageFunctionsPerFile = filesAnalyzed > 0 ? totalFunctions / filesAnalyzed : 0;
6132
+ const averageImportsPerFile = filesAnalyzed > 0 ? totalImports / filesAnalyzed : 0;
6133
+ const fileLengthScore = Math.max(0, Math.min(100, 100 - (averageFileLength - 200) * 0.33));
6134
+ const functionCountScore = Math.max(0, Math.min(100, 100 - (averageFunctionsPerFile - 10) * 5));
6135
+ const dependencyCountScore = Math.max(0, Math.min(100, 100 - (averageImportsPerFile - 5) * 5));
6136
+ const crossBoundaryRatio = totalImports > 0 ? totalCrossBoundaryImports / totalImports : 0;
6137
+ const couplingScore = Math.max(0, Math.min(100, 100 - crossBoundaryRatio * 100 * 0.5));
6138
+ const score = Math.round(
6139
+ fileLengthScore * 0.3 + functionCountScore * 0.25 + dependencyCountScore * 0.25 + couplingScore * 0.2
6140
+ );
6141
+ const details = [
6142
+ `${Math.round(averageFileLength)} avg lines/file`,
6143
+ `${averageFunctionsPerFile.toFixed(1)} avg functions/file`,
6144
+ `${averageImportsPerFile.toFixed(1)} avg imports/file`,
6145
+ `${Math.round(crossBoundaryRatio * 100)}% cross-boundary coupling`
6146
+ ].join(", ");
6147
+ return {
6148
+ score,
6149
+ fileLengthScore,
6150
+ functionCountScore,
6151
+ dependencyCountScore,
6152
+ couplingScore,
6153
+ averageFileLength,
6154
+ averageFunctionsPerFile,
6155
+ averageImportsPerFile,
6156
+ fileCount: filesAnalyzed,
6157
+ details
6158
+ };
6159
+ }
6160
+ async findSourceFiles() {
6161
+ return glob("**/*.{ts,js,tsx,jsx}", {
6162
+ cwd: this.projectPath,
6163
+ absolute: true,
6164
+ ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/dist/**", "**/build/**"]
6165
+ });
6166
+ }
6167
+ };
6168
+ var PROJECT_CONFIG_FILENAME = ".coco.config.json";
6169
+ var QualityWeightsOverrideSchema = z.object({
6170
+ correctness: z.number().min(0).max(1),
6171
+ completeness: z.number().min(0).max(1),
6172
+ robustness: z.number().min(0).max(1),
6173
+ readability: z.number().min(0).max(1),
6174
+ maintainability: z.number().min(0).max(1),
6175
+ complexity: z.number().min(0).max(1),
6176
+ duplication: z.number().min(0).max(1),
6177
+ testCoverage: z.number().min(0).max(1),
6178
+ testQuality: z.number().min(0).max(1),
6179
+ security: z.number().min(0).max(1),
6180
+ documentation: z.number().min(0).max(1),
6181
+ style: z.number().min(0).max(1)
6182
+ }).partial();
6183
+ var ProjectQualityOverridesSchema = z.object({
6184
+ /** Minimum overall score (0–100). Default: 85 */
6185
+ minScore: z.number().min(0).max(100).optional(),
6186
+ /** Minimum test-coverage percentage (0–100). Default: 80 */
6187
+ minCoverage: z.number().min(0).max(100).optional(),
6188
+ /** Maximum convergence iterations. Default: 10 */
6189
+ maxIterations: z.number().min(1).max(50).optional(),
6190
+ /** Required security score (0–100). Default: 100 */
6191
+ securityThreshold: z.number().min(0).max(100).optional(),
6192
+ /** Per-dimension weight overrides */
6193
+ weights: QualityWeightsOverrideSchema.optional(),
6194
+ /** Stored but not yet enforced by analyzers — reserved for a future release. */
6195
+ ignoreRules: z.array(z.string()).optional(),
6196
+ /** Stored but not yet enforced by analyzers — reserved for a future release. */
6197
+ ignoreFiles: z.array(z.string()).optional()
6198
+ });
6199
+ var ProjectAnalyzersConfigSchema = z.object({
6200
+ /** Restrict analysis to these language IDs (default: auto-detect) */
6201
+ enabledLanguages: z.array(z.string()).optional(),
6202
+ /** Java-specific options */
6203
+ java: z.object({
6204
+ /** Minimum line coverage expected from JaCoCo report */
6205
+ minCoverage: z.number().min(0).max(100).optional(),
6206
+ /** Custom path to jacoco.xml relative to project root */
6207
+ reportPath: z.string().optional()
6208
+ }).optional(),
6209
+ /** React-specific options */
6210
+ react: z.object({
6211
+ /** Run accessibility (a11y) checks. Default: true */
6212
+ checkA11y: z.boolean().optional(),
6213
+ /** Enforce React Rules of Hooks. Default: true */
6214
+ checkHooks: z.boolean().optional(),
6215
+ /** Run component-quality checks. Default: true */
6216
+ checkComponents: z.boolean().optional()
6217
+ }).optional()
6218
+ });
6219
+ var ProjectConfigSchema = z.object({
6220
+ /** Human-readable project name */
6221
+ name: z.string().optional(),
6222
+ /** SemVer string for the config schema itself */
6223
+ version: z.string().optional(),
6224
+ /** Short project description */
6225
+ description: z.string().optional(),
6226
+ /**
6227
+ * Primary project language ID.
6228
+ * Used when auto-detection is ambiguous.
6229
+ * @example "typescript" | "java" | "react-typescript"
6230
+ */
6231
+ language: z.string().optional(),
6232
+ /** Quality threshold and weight overrides */
6233
+ quality: ProjectQualityOverridesSchema.optional(),
6234
+ /** Analyzer-specific settings */
6235
+ analyzers: ProjectAnalyzersConfigSchema.optional(),
6236
+ /**
6237
+ * Path to a base config to inherit from (relative to this file).
6238
+ * Merged shallowly; this file wins on conflicts.
6239
+ * @note extend is resolved only one level deep — chaining (A extends B extends C) is not supported.
6240
+ * @example "../shared/.coco.config.json"
6241
+ */
6242
+ extend: z.string().optional()
6243
+ });
6244
+ function getProjectConfigPath(projectPath) {
6245
+ return join(resolve(projectPath), PROJECT_CONFIG_FILENAME);
6246
+ }
6247
+ async function loadProjectConfig(projectPath) {
6248
+ const configPath = getProjectConfigPath(projectPath);
6249
+ let raw;
6250
+ try {
6251
+ raw = await readFile(configPath, "utf-8");
6252
+ } catch (err) {
6253
+ if (err.code === "ENOENT") return null;
6254
+ throw err;
6255
+ }
6256
+ const parsed = JSON.parse(raw);
6257
+ const result = ProjectConfigSchema.safeParse(parsed);
6258
+ if (!result.success) {
6259
+ throw new Error(`Invalid ${PROJECT_CONFIG_FILENAME} at ${configPath}: ${result.error.message}`);
6260
+ }
6261
+ let config = result.data;
6262
+ if (config.extend) {
6263
+ const basePath = resolve(dirname(configPath), config.extend);
6264
+ let baseRaw;
6265
+ try {
6266
+ baseRaw = await readFile(basePath, "utf-8");
6267
+ } catch (err) {
6268
+ throw new Error(
6269
+ `Cannot extend "${config.extend}" \u2014 file not found at ${basePath}: ${String(err)}`
6270
+ );
6271
+ }
6272
+ const baseResult = ProjectConfigSchema.safeParse(JSON.parse(baseRaw));
6273
+ if (!baseResult.success) {
6274
+ throw new Error(
6275
+ `Invalid base config at "${config.extend}" (resolved to ${basePath}): ${baseResult.error.message}`
6276
+ );
6277
+ }
6278
+ config = mergeProjectConfigs(baseResult.data, config);
6279
+ }
6280
+ return config;
6281
+ }
6282
+ function mergeProjectConfigs(base, override) {
6283
+ const hasQuality = base.quality !== void 0 || override.quality !== void 0;
6284
+ const hasAnalyzers = base.analyzers !== void 0 || override.analyzers !== void 0;
6285
+ return {
6286
+ ...base,
6287
+ ...override,
6288
+ quality: hasQuality ? {
6289
+ ...base.quality,
6290
+ ...override.quality,
6291
+ weights: base.quality?.weights !== void 0 || override.quality?.weights !== void 0 ? { ...base.quality?.weights, ...override.quality?.weights } : void 0,
6292
+ ignoreRules: [
6293
+ ...base.quality?.ignoreRules ?? [],
6294
+ ...override.quality?.ignoreRules ?? []
6295
+ ],
6296
+ ignoreFiles: [
6297
+ ...base.quality?.ignoreFiles ?? [],
6298
+ ...override.quality?.ignoreFiles ?? []
6299
+ ]
6300
+ } : void 0,
6301
+ analyzers: hasAnalyzers ? {
6302
+ ...base.analyzers,
6303
+ ...override.analyzers,
6304
+ java: base.analyzers?.java !== void 0 || override.analyzers?.java !== void 0 ? { ...base.analyzers?.java, ...override.analyzers?.java } : void 0,
6305
+ react: base.analyzers?.react !== void 0 || override.analyzers?.react !== void 0 ? { ...base.analyzers?.react, ...override.analyzers?.react } : void 0
6306
+ } : void 0
6307
+ };
6308
+ }
6309
+
6310
+ // src/quality/quality-bridge.ts
6311
+ ({
6312
+ minScore: DEFAULT_QUALITY_THRESHOLDS.minimum.overall,
6313
+ targetScore: DEFAULT_QUALITY_THRESHOLDS.target.overall,
6314
+ maxIterations: DEFAULT_QUALITY_THRESHOLDS.maxIterations,
6315
+ stableDeltaThreshold: DEFAULT_QUALITY_THRESHOLDS.convergenceThreshold
6316
+ });
6317
+ function thresholdsFromProjectConfig(config) {
6318
+ const q = config.quality;
6319
+ if (!q) return {};
6320
+ const result = {};
6321
+ const hasMinimum = q.minScore !== void 0 || q.minCoverage !== void 0 || q.securityThreshold !== void 0;
6322
+ if (hasMinimum) {
6323
+ result.minimum = {
6324
+ overall: q.minScore ?? DEFAULT_QUALITY_THRESHOLDS.minimum.overall,
6325
+ testCoverage: q.minCoverage ?? DEFAULT_QUALITY_THRESHOLDS.minimum.testCoverage,
6326
+ security: q.securityThreshold ?? DEFAULT_QUALITY_THRESHOLDS.minimum.security
6327
+ };
6328
+ }
6329
+ if (q.maxIterations !== void 0) {
6330
+ result.maxIterations = q.maxIterations;
6331
+ result.convergenceThreshold = DEFAULT_QUALITY_THRESHOLDS.convergenceThreshold;
6332
+ }
6333
+ return result;
6334
+ }
6335
+ function mergeThresholds(base, overrides) {
6336
+ return {
6337
+ ...base,
6338
+ ...overrides,
6339
+ minimum: overrides.minimum ? { ...base.minimum, ...overrides.minimum } : base.minimum,
6340
+ target: overrides.target ? { ...base.target, ...overrides.target } : base.target
6341
+ };
6342
+ }
6343
+ function resolvedThresholds(projectConfig) {
6344
+ if (!projectConfig) return DEFAULT_QUALITY_THRESHOLDS;
6345
+ return mergeThresholds(DEFAULT_QUALITY_THRESHOLDS, thresholdsFromProjectConfig(projectConfig));
6346
+ }
6347
+ function weightsFromProjectConfig(config) {
6348
+ const overrides = config.quality?.weights;
6349
+ if (!overrides || Object.keys(overrides).length === 0) {
6350
+ return DEFAULT_QUALITY_WEIGHTS;
6351
+ }
6352
+ const merged = { ...DEFAULT_QUALITY_WEIGHTS, ...overrides };
6353
+ const total = Object.values(merged).reduce((s, v) => s + v, 0);
6354
+ if (total === 0) return DEFAULT_QUALITY_WEIGHTS;
6355
+ return Object.fromEntries(
6356
+ Object.entries(merged).map(([k, v]) => [k, v / total])
6357
+ );
6358
+ }
6359
+ function resolvedWeights(projectConfig) {
6360
+ if (!projectConfig) return DEFAULT_QUALITY_WEIGHTS;
6361
+ return weightsFromProjectConfig(projectConfig);
6362
+ }
6363
+ var EXTENSION_MAP = {
6364
+ ".ts": "typescript",
6365
+ ".d.ts": "typescript",
6366
+ ".tsx": "react-typescript",
6367
+ ".js": "javascript",
6368
+ ".mjs": "javascript",
6369
+ ".cjs": "javascript",
6370
+ ".jsx": "react-javascript",
6371
+ ".java": "java",
6372
+ ".py": "python",
6373
+ ".go": "go",
6374
+ ".rs": "rust"
6375
+ };
6376
+ function detectLanguage(filePath, content) {
6377
+ if (!filePath) return "unknown";
6378
+ const ext = getFileExtension(filePath);
6379
+ const langByExt = EXTENSION_MAP[ext];
6380
+ if (!langByExt) return "unknown";
6381
+ return langByExt;
6382
+ }
6383
+ function detectProjectLanguage(files) {
6384
+ if (!files.length) {
6385
+ return { language: "unknown", confidence: 0, evidence: [] };
6386
+ }
6387
+ const counts = /* @__PURE__ */ new Map();
6388
+ let totalSourceFiles = 0;
6389
+ for (const file of files) {
6390
+ const lang = detectLanguage(file);
6391
+ if (lang !== "unknown") {
6392
+ counts.set(lang, (counts.get(lang) ?? 0) + 1);
6393
+ totalSourceFiles++;
6394
+ }
6395
+ }
6396
+ if (totalSourceFiles === 0) {
6397
+ return { language: "unknown", confidence: 0, evidence: [] };
6398
+ }
6399
+ let maxCount = 0;
6400
+ let dominant = "unknown";
6401
+ for (const [lang, count] of counts) {
6402
+ if (count > maxCount) {
6403
+ maxCount = count;
6404
+ dominant = lang;
6405
+ }
6406
+ }
6407
+ const confidence = maxCount / totalSourceFiles;
6408
+ const evidence = buildEvidence(dominant, counts, totalSourceFiles, files);
6409
+ const tsxCount = counts.get("react-typescript") ?? 0;
6410
+ const tsCount = counts.get("typescript") ?? 0;
6411
+ if (dominant === "typescript" && tsxCount > 0 && tsxCount >= tsCount * 0.3) {
6412
+ return {
6413
+ language: "react-typescript",
6414
+ confidence,
6415
+ evidence: [...evidence, `${tsxCount} React (.tsx) files detected`]
6416
+ };
6417
+ }
6418
+ return { language: dominant, confidence, evidence };
6419
+ }
6420
+ function getFileExtension(filePath) {
6421
+ const base = path15.basename(filePath);
6422
+ if (base.endsWith(".d.ts")) return ".d.ts";
6423
+ return path15.extname(filePath).toLowerCase();
6424
+ }
6425
+ function buildEvidence(dominant, counts, totalSourceFiles, files) {
6426
+ const evidence = [];
6427
+ const dominantCount = counts.get(dominant) ?? 0;
6428
+ evidence.push(`${dominantCount} of ${totalSourceFiles} source files are ${dominant}`);
6429
+ const configFiles = ["tsconfig.json", "pom.xml", "build.gradle", "Cargo.toml", "go.mod"];
6430
+ for (const cfg of configFiles) {
6431
+ if (files.some((f) => path15.basename(f) === cfg)) {
6432
+ evidence.push(`Found ${cfg}`);
6433
+ }
6434
+ }
6435
+ return evidence;
6436
+ }
6437
+ async function findJavaFiles(projectPath, options) {
6438
+ const { includeTests = true, srcPattern = "**/*.java" } = options ?? {};
6439
+ const ignore = ["**/node_modules/**", "**/target/**", "**/build/**"];
6440
+ if (!includeTests) {
6441
+ ignore.push("**/*Test*.java", "**/*Spec*.java");
6442
+ }
6443
+ return glob(srcPattern, {
6444
+ cwd: projectPath,
6445
+ absolute: true,
6446
+ ignore
6447
+ });
6448
+ }
6449
+ var BRANCH_KEYWORDS = /\b(if|else if|for|while|do|case|catch|&&|\|\||\?)\b/g;
6450
+ var METHOD_PATTERN = /(?:public|private|protected|static|final|synchronized|abstract)\s+[\w<>[\]]+\s+\w+\s*\([^)]*\)\s*(?:throws\s+[\w,\s]+)?\s*\{/g;
6451
+ var JavaComplexityAnalyzer = class {
6452
+ constructor(projectPath) {
6453
+ this.projectPath = projectPath;
6454
+ }
6455
+ async analyze(files) {
6456
+ const javaFiles = files ?? await findJavaFiles(this.projectPath);
6457
+ if (!javaFiles.length) {
6458
+ return { score: 100, totalMethods: 0, averageComplexity: 1, complexMethods: [] };
6459
+ }
6460
+ const fileContents = await Promise.all(
6461
+ javaFiles.map(async (f) => ({
6462
+ path: f,
6463
+ content: await readFile(f, "utf-8").catch(() => "")
6464
+ }))
6465
+ );
6466
+ return this.analyzeContent(fileContents);
6467
+ }
6468
+ analyzeContent(files) {
6469
+ if (!files.length) {
6470
+ return { score: 100, totalMethods: 0, averageComplexity: 1, complexMethods: [] };
6471
+ }
6472
+ let totalComplexity = 0;
6473
+ let totalMethods = 0;
6474
+ const complexMethods = [];
6475
+ for (const { path: filePath, content } of files) {
6476
+ const methods = this.extractMethodBlocks(content);
6477
+ for (const method of methods) {
6478
+ const complexity = this.calculateComplexity(method.body);
6479
+ totalComplexity += complexity;
6480
+ totalMethods++;
6481
+ if (complexity > 10) {
6482
+ complexMethods.push({ method: method.name, file: filePath, complexity });
6483
+ }
6484
+ }
6485
+ }
6486
+ const averageComplexity = totalMethods > 0 ? totalComplexity / totalMethods : 1;
6487
+ const score = Math.max(0, Math.min(100, Math.round(100 - (averageComplexity - 1) * 6.5)));
6488
+ return { score, totalMethods, averageComplexity, complexMethods };
6489
+ }
6490
+ extractMethodBlocks(content) {
6491
+ const methods = [];
6492
+ const methodRegex = new RegExp(METHOD_PATTERN.source, "g");
6493
+ let match;
6494
+ while ((match = methodRegex.exec(content)) !== null) {
6495
+ const nameMatch = /\s(\w+)\s*\(/.exec(match[0]);
6496
+ const methodName = nameMatch ? nameMatch[1] ?? "unknown" : "unknown";
6497
+ const body = this.extractBlock(content, match.index + match[0].length - 1);
6498
+ methods.push({ name: methodName, body });
6499
+ }
6500
+ return methods;
6501
+ }
6502
+ extractBlock(content, openBraceIndex) {
6503
+ let depth = 1;
6504
+ let i = openBraceIndex + 1;
6505
+ while (i < content.length && depth > 0) {
6506
+ if (content[i] === "{") depth++;
6507
+ else if (content[i] === "}") depth--;
6508
+ i++;
6509
+ }
6510
+ return content.slice(openBraceIndex, i);
6511
+ }
6512
+ calculateComplexity(body) {
6513
+ const matches = body.match(BRANCH_KEYWORDS) ?? [];
6514
+ return 1 + matches.length;
6515
+ }
6516
+ };
6517
+ var JAVA_SECURITY_PATTERNS = [
6518
+ // SQL Injection — string concatenation in any execute/executeQuery/executeUpdate call
6519
+ {
6520
+ regex: /\.execute[A-Za-z]*\s*\(\s*["'][^"']*["']\s*\+/,
6521
+ severity: "critical",
6522
+ type: "SQL Injection",
6523
+ description: "String concatenation in SQL query \u2014 vulnerable to injection",
6524
+ recommendation: "Use PreparedStatement with parameterized queries",
6525
+ cwe: "CWE-89"
6526
+ },
6527
+ {
6528
+ regex: /createQuery\s*\(\s*["'].*\+|createNativeQuery\s*\(\s*["'].*\+/,
6529
+ severity: "critical",
6530
+ type: "SQL Injection (JPQL)",
6531
+ description: "String concatenation in JPQL/native query",
6532
+ recommendation: "Use named parameters with setParameter()",
6533
+ cwe: "CWE-89"
6534
+ },
6535
+ // Hardcoded Credentials
6536
+ {
6537
+ regex: /(?:password|passwd|secret|apiKey|api_key)\s*=\s*["'][^"']{4,}["']/i,
6538
+ severity: "high",
6539
+ type: "Hardcoded Credential",
6540
+ description: "Hardcoded credential or secret detected",
6541
+ recommendation: "Store credentials in environment variables or a secrets manager",
6542
+ cwe: "CWE-798"
6543
+ },
6544
+ // Unsafe Deserialization
6545
+ {
6546
+ regex: /new\s+(?:java\.io\.)?ObjectInputStream/,
6547
+ severity: "high",
6548
+ type: "Unsafe Deserialization",
6549
+ description: "Unsafe Java deserialization can lead to RCE",
6550
+ recommendation: "Use safer serialization formats (JSON) or whitelist classes",
6551
+ cwe: "CWE-502"
6552
+ },
6553
+ // Path Traversal
6554
+ {
6555
+ regex: /new\s+(?:java\.io\.)?File\s*\(\s*(?:request\.|user|input)/,
6556
+ severity: "high",
6557
+ type: "Path Traversal",
6558
+ description: "File path constructed from user input",
6559
+ recommendation: "Sanitize and validate file paths; use Paths.get() with canonical path check",
6560
+ cwe: "CWE-22"
6561
+ },
6562
+ // Command Injection
6563
+ {
6564
+ regex: /Runtime\.getRuntime\(\)\.exec\s*\(\s*[^"]/,
6565
+ severity: "critical",
6566
+ type: "Command Injection",
6567
+ description: "Dynamic command execution \u2014 vulnerable to injection",
6568
+ recommendation: "Use ProcessBuilder with a fixed command array",
6569
+ cwe: "CWE-78"
6570
+ },
6571
+ // XXE
6572
+ {
6573
+ regex: /DocumentBuilderFactory\s*\.\s*newInstance\s*\(\s*\)/,
6574
+ severity: "high",
6575
+ type: "XML External Entity (XXE)",
6576
+ description: "XML parsing without disabling external entities",
6577
+ recommendation: "Disable external entities: factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)",
6578
+ cwe: "CWE-611"
6579
+ },
6580
+ // Insecure Random
6581
+ {
6582
+ regex: /new\s+(?:java\.util\.)?Random\s*\(\s*\)(?!\s*\/\/.*secure)/,
6583
+ severity: "medium",
6584
+ type: "Insecure Random",
6585
+ description: "java.util.Random is not cryptographically secure",
6586
+ recommendation: "Use SecureRandom for security-sensitive operations",
6587
+ cwe: "CWE-338"
6588
+ }
6589
+ ];
6590
+ var JavaSecurityAnalyzer = class {
6591
+ constructor(projectPath) {
6592
+ this.projectPath = projectPath;
6593
+ }
6594
+ async analyze(files) {
6595
+ const javaFiles = files ?? await findJavaFiles(this.projectPath, { includeTests: false });
6596
+ if (!javaFiles.length) {
6597
+ return { score: 100, vulnerabilities: [] };
6598
+ }
6599
+ const fileContents = await Promise.all(
6600
+ javaFiles.map(async (f) => ({
6601
+ path: f,
6602
+ content: await readFile(f, "utf-8").catch(() => "")
6603
+ }))
6604
+ );
6605
+ return this.analyzeContent(fileContents);
6606
+ }
6607
+ analyzeContent(files) {
6608
+ if (!files.length) return { score: 100, vulnerabilities: [] };
6609
+ const vulnerabilities = [];
6610
+ for (const { path: filePath, content } of files) {
6611
+ const lines = content.split("\n");
6612
+ for (const pattern of JAVA_SECURITY_PATTERNS) {
6613
+ for (let i = 0; i < lines.length; i++) {
6614
+ const line = lines[i] ?? "";
6615
+ if (pattern.regex.test(line)) {
6616
+ vulnerabilities.push({
6617
+ severity: pattern.severity,
6618
+ type: pattern.type,
6619
+ file: filePath,
6620
+ line: i + 1,
6621
+ description: pattern.description,
6622
+ recommendation: pattern.recommendation,
6623
+ cwe: pattern.cwe
6624
+ });
6625
+ }
6626
+ }
6627
+ }
6628
+ }
6629
+ const score = this.calculateScore(vulnerabilities);
6630
+ return { score, vulnerabilities };
6631
+ }
6632
+ calculateScore(vulns) {
6633
+ let score = 100;
6634
+ for (const v of vulns) {
6635
+ switch (v.severity) {
6636
+ case "critical":
6637
+ score -= 30;
6638
+ break;
6639
+ case "high":
6640
+ score -= 15;
6641
+ break;
6642
+ case "medium":
6643
+ score -= 7;
6644
+ break;
6645
+ case "low":
6646
+ score -= 3;
6647
+ break;
6648
+ }
6649
+ }
6650
+ return Math.max(0, score);
6651
+ }
6652
+ };
6653
+ var MAX_LINE_LENGTH = 120;
6654
+ var JavaStyleAnalyzer = class {
6655
+ constructor(projectPath) {
6656
+ this.projectPath = projectPath;
6657
+ }
6658
+ async analyze(files) {
6659
+ const javaFiles = files ?? await findJavaFiles(this.projectPath);
6660
+ if (!javaFiles.length) return { score: 100, violations: [] };
6661
+ const fileContents = await Promise.all(
6662
+ javaFiles.map(async (f) => ({
6663
+ path: f,
6664
+ content: await readFile(f, "utf-8").catch(() => "")
6665
+ }))
6666
+ );
6667
+ return this.analyzeContent(fileContents);
6668
+ }
6669
+ analyzeContent(files) {
6670
+ if (!files.length) return { score: 100, violations: [] };
6671
+ const violations = [];
6672
+ for (const { path: filePath, content } of files) {
6673
+ violations.push(...this.checkFile(filePath, content));
6674
+ }
6675
+ const deduction = violations.reduce((sum, v) => sum + (v.severity === "error" ? 10 : 5), 0);
6676
+ const score = Math.max(0, 100 - deduction);
6677
+ return { score, violations };
6678
+ }
6679
+ checkFile(filePath, content) {
6680
+ const violations = [];
6681
+ const lines = content.split("\n");
6682
+ for (let i = 0; i < lines.length; i++) {
6683
+ const line = lines[i] ?? "";
6684
+ const lineNum = i + 1;
6685
+ if (line.length > MAX_LINE_LENGTH) {
6686
+ violations.push({
6687
+ rule: "LineLength",
6688
+ file: filePath,
6689
+ line: lineNum,
6690
+ message: `Line length ${line.length} exceeds ${MAX_LINE_LENGTH} characters`,
6691
+ severity: "warning"
6692
+ });
6693
+ }
6694
+ const classMatch = /^(?:public|private|protected)?\s*(?:class|interface|enum|record)\s+([a-z]\w*)/.exec(line);
6695
+ if (classMatch) {
6696
+ violations.push({
6697
+ rule: "TypeName",
6698
+ file: filePath,
6699
+ line: lineNum,
6700
+ message: `Type name '${classMatch[1]}' should start with uppercase letter (PascalCase)`,
6701
+ severity: "error"
6702
+ });
6703
+ }
6704
+ const methodMatch = /\b(?:public|private|protected|static)\s+(?!class|interface|enum|record|new\b)(?:void|[\w<>[\]]+)\s+([A-Z]\w*)\s*\(/.exec(
6705
+ line
6706
+ );
6707
+ if (methodMatch && !line.trim().startsWith("class") && !line.includes("class ")) {
6708
+ violations.push({
6709
+ rule: "MethodName",
6710
+ file: filePath,
6711
+ line: lineNum,
6712
+ message: `Method name '${methodMatch[1]}' should start with lowercase letter (camelCase)`,
6713
+ severity: "error"
6714
+ });
6715
+ }
6716
+ const constantMatch = /\bpublic\s+static\s+final\s+\w+\s+([a-z][a-zA-Z]+)\s*=/.exec(line);
6717
+ if (constantMatch) {
6718
+ violations.push({
6719
+ rule: "ConstantName",
6720
+ file: filePath,
6721
+ line: lineNum,
6722
+ message: `Constant '${constantMatch[1]}' should be UPPER_SNAKE_CASE`,
6723
+ severity: "warning"
6724
+ });
6725
+ }
6726
+ const paramsMatch = /\w+\s+\w+\s*\(([^)]+)\)/.exec(line);
6727
+ if (paramsMatch) {
6728
+ const paramCount = (paramsMatch[1] ?? "").split(",").length;
6729
+ if (paramCount > 5) {
6730
+ violations.push({
6731
+ rule: "ParameterNumber",
6732
+ file: filePath,
6733
+ line: lineNum,
6734
+ message: `Method has ${paramCount} parameters (max recommended: 5)`,
6735
+ severity: "warning"
6736
+ });
6737
+ }
6738
+ }
6739
+ if (/\)\{/.test(line) || /\belse\{/.test(line)) {
6740
+ violations.push({
6741
+ rule: "WhitespaceAround",
6742
+ file: filePath,
6743
+ line: lineNum,
6744
+ message: "Missing space before '{'",
6745
+ severity: "warning"
6746
+ });
6747
+ }
6748
+ }
6749
+ return violations;
6750
+ }
6751
+ };
6752
+ var JavaDocumentationAnalyzer = class {
6753
+ constructor(projectPath) {
6754
+ this.projectPath = projectPath;
6755
+ }
6756
+ async analyze(files) {
6757
+ const javaFiles = files ?? await findJavaFiles(this.projectPath, { srcPattern: "src/main/**/*.java" });
6758
+ if (!javaFiles.length) {
6759
+ return {
6760
+ score: 100,
6761
+ javadocCoverage: 1,
6762
+ totalMethods: 0,
6763
+ documentedMethods: 0,
6764
+ undocumentedPublicMethods: []
6765
+ };
6766
+ }
6767
+ const fileContents = await Promise.all(
6768
+ javaFiles.map(async (f) => ({
6769
+ path: f,
6770
+ content: await readFile(f, "utf-8").catch(() => "")
6771
+ }))
6772
+ );
6773
+ return this.analyzeContent(fileContents);
6774
+ }
6775
+ analyzeContent(files) {
6776
+ if (!files.length) {
6777
+ return {
6778
+ score: 100,
6779
+ javadocCoverage: 1,
6780
+ totalMethods: 0,
6781
+ documentedMethods: 0,
6782
+ undocumentedPublicMethods: []
6783
+ };
6784
+ }
6785
+ let totalMethods = 0;
6786
+ let documentedMethods = 0;
6787
+ const undocumentedPublicMethods = [];
6788
+ for (const { content } of files) {
6789
+ const lines = content.split("\n");
6790
+ for (let i = 0; i < lines.length; i++) {
6791
+ const line = lines[i] ?? "";
6792
+ if (/\b(?:public)\s+(?!class|interface|enum|record)[\w<>[\]]+\s+\w+\s*\(/.test(line)) {
6793
+ totalMethods++;
6794
+ const isDocumented = this.hasPrecedingJavadoc(lines, i);
6795
+ if (isDocumented) {
6796
+ documentedMethods++;
6797
+ } else {
6798
+ const nameMatch = /\s(\w+)\s*\(/.exec(line);
6799
+ if (nameMatch) undocumentedPublicMethods.push(nameMatch[1] ?? "unknown");
6800
+ }
6801
+ }
6802
+ }
6803
+ }
6804
+ const javadocCoverage = totalMethods > 0 ? documentedMethods / totalMethods : 1;
6805
+ const score = Math.round(javadocCoverage * 100);
6806
+ return { score, javadocCoverage, totalMethods, documentedMethods, undocumentedPublicMethods };
6807
+ }
6808
+ hasPrecedingJavadoc(lines, methodLineIndex) {
6809
+ for (let i = methodLineIndex - 1; i >= 0; i--) {
6810
+ const prevLine = (lines[i] ?? "").trim();
6811
+ if (prevLine === "") continue;
6812
+ if (prevLine.endsWith("*/")) {
6813
+ for (let j = i; j >= 0; j--) {
6814
+ const docLine = (lines[j] ?? "").trim();
6815
+ if (docLine.startsWith("/**")) return true;
6816
+ if (!docLine.startsWith("*") && !docLine.startsWith("/**")) break;
6817
+ }
6818
+ }
6819
+ break;
6820
+ }
6821
+ return false;
6822
+ }
6823
+ };
6824
+ var JACOCO_REPORT_PATHS = [
6825
+ "target/site/jacoco/jacoco.xml",
6826
+ "build/reports/jacoco/test/jacocoTestReport.xml",
6827
+ "build/reports/jacoco/jacocoTestReport.xml"
6828
+ ];
6829
+ var JavaCoverageAnalyzer = class {
6830
+ constructor(projectPath) {
6831
+ this.projectPath = projectPath;
6832
+ }
6833
+ async analyze() {
6834
+ for (const reportPath of JACOCO_REPORT_PATHS) {
6835
+ try {
6836
+ const xml2 = await readFile(join(this.projectPath, reportPath), "utf-8");
6837
+ const result = this.parseJacocoXml(xml2);
6838
+ return { ...result, reportFound: true };
6839
+ } catch {
6840
+ }
6841
+ }
6842
+ return { score: 50, lineCoverage: 0, branchCoverage: 0, methodCoverage: 0, reportFound: false };
6843
+ }
6844
+ parseJacocoXml(xml2) {
6845
+ if (!xml2.trim()) {
6846
+ return {
6847
+ score: 0,
6848
+ lineCoverage: 0,
6849
+ branchCoverage: 0,
6850
+ methodCoverage: 0,
6851
+ reportFound: false
6852
+ };
6853
+ }
6854
+ const lineCoverage = this.extractCoverage(xml2, "LINE");
6855
+ const branchCoverage = this.extractCoverage(xml2, "BRANCH");
6856
+ const methodCoverage = this.extractCoverage(xml2, "METHOD");
6857
+ const score = Math.round(lineCoverage * 0.5 + branchCoverage * 0.35 + methodCoverage * 0.15);
6858
+ return { score, lineCoverage, branchCoverage, methodCoverage, reportFound: true };
6859
+ }
6860
+ extractCoverage(xml2, type) {
6861
+ const regex = new RegExp(`<counter\\s+type="${type}"\\s+missed="(\\d+)"\\s+covered="(\\d+)"`);
6862
+ const match = regex.exec(xml2);
6863
+ if (!match) return 0;
6864
+ const missed = parseInt(match[1] ?? "0", 10);
6865
+ const covered = parseInt(match[2] ?? "0", 10);
6866
+ const total = missed + covered;
6867
+ return total > 0 ? Math.round(covered / total * 100) : 0;
6868
+ }
6869
+ };
6870
+ function registerJavaAnalyzers(registry, projectPath) {
6871
+ const complexityAnalyzer = new JavaComplexityAnalyzer(projectPath);
6872
+ const securityAnalyzer = new JavaSecurityAnalyzer(projectPath);
6873
+ const styleAnalyzer = new JavaStyleAnalyzer(projectPath);
6874
+ const documentationAnalyzer = new JavaDocumentationAnalyzer(projectPath);
6875
+ const coverageAnalyzer = new JavaCoverageAnalyzer(projectPath);
6876
+ registry.register({
6877
+ dimensionId: "complexity",
6878
+ language: "java",
6879
+ async analyze(input) {
6880
+ const result = await complexityAnalyzer.analyze(input.files);
6881
+ return { score: result.score, issues: [] };
6882
+ }
6883
+ });
6884
+ registry.register({
6885
+ dimensionId: "security",
6886
+ language: "java",
6887
+ async analyze(input) {
6888
+ const contents = await Promise.all(
6889
+ input.files.map(async (f) => ({
6890
+ path: f,
6891
+ content: await readFile(f, "utf-8").catch(() => "")
6892
+ }))
6893
+ );
6894
+ const result = securityAnalyzer.analyzeContent(contents);
6895
+ return {
6896
+ score: result.score,
6897
+ issues: result.vulnerabilities.map((v) => ({
6898
+ dimension: "security",
6899
+ severity: v.severity === "critical" ? "critical" : "major",
6900
+ message: `[${v.type}] ${v.description}`,
6901
+ file: v.file,
6902
+ line: v.line,
6903
+ suggestion: v.recommendation
6904
+ }))
6905
+ };
6906
+ }
6907
+ });
6908
+ registry.register({
6909
+ dimensionId: "style",
6910
+ language: "java",
6911
+ async analyze(input) {
6912
+ const contents = await Promise.all(
6913
+ input.files.map(async (f) => ({
6914
+ path: f,
6915
+ content: await readFile(f, "utf-8").catch(() => "")
6916
+ }))
6917
+ );
6918
+ const result = styleAnalyzer.analyzeContent(contents);
6919
+ return { score: result.score, issues: [] };
6920
+ }
6921
+ });
6922
+ registry.register({
6923
+ dimensionId: "documentation",
6924
+ language: "java",
6925
+ async analyze(input) {
6926
+ const contents = await Promise.all(
6927
+ input.files.map(async (f) => ({
6928
+ path: f,
6929
+ content: await readFile(f, "utf-8").catch(() => "")
6930
+ }))
6931
+ );
6932
+ const result = documentationAnalyzer.analyzeContent(contents);
6933
+ return { score: result.score, issues: [] };
6934
+ }
6935
+ });
6936
+ registry.register({
6937
+ dimensionId: "testCoverage",
6938
+ language: "java",
6939
+ async analyze() {
6940
+ const result = await coverageAnalyzer.analyze();
6941
+ return { score: result.score, issues: [] };
6942
+ }
6943
+ });
6944
+ }
6945
+ async function loadFiles(files) {
6946
+ return Promise.all(
6947
+ files.map(async (f) => ({
6948
+ path: f,
6949
+ content: await readFile(f, "utf-8").catch(() => "")
6950
+ }))
6951
+ );
6952
+ }
6953
+ async function findReactFiles(projectPath, pattern = "**/*.{tsx,jsx}") {
6954
+ return glob(pattern, {
6955
+ cwd: projectPath,
6956
+ absolute: true,
6957
+ ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"]
6958
+ });
6959
+ }
6960
+ var ReactComponentAnalyzer = class {
6961
+ constructor(projectPath) {
6962
+ this.projectPath = projectPath;
6963
+ }
6964
+ async analyze(files) {
6965
+ const reactFiles = files ?? await findReactFiles(this.projectPath);
6966
+ if (!reactFiles.length) return { score: 100, totalComponents: 0, issues: [] };
6967
+ const fileContents = await loadFiles(reactFiles);
6968
+ return this.analyzeContent(fileContents);
6969
+ }
6970
+ analyzeContent(files) {
6971
+ if (!files.length) return { score: 100, totalComponents: 0, issues: [] };
6972
+ const issues = [];
6973
+ let totalComponents = 0;
6974
+ for (const { path: filePath, content } of files) {
6975
+ const lines = content.split("\n");
6976
+ let inComponent = false;
6977
+ for (let i = 0; i < lines.length; i++) {
6978
+ const line = lines[i] ?? "";
6979
+ const lineNum = i + 1;
6980
+ if (/\bfunction\s+[A-Z]\w*\s*\(|const\s+[A-Z]\w*\s*=\s*(?:\([^)]*\)|[a-z]\w*)\s*=>/.test(line)) {
6981
+ totalComponents++;
6982
+ inComponent = true;
6983
+ }
6984
+ if (/\.map\s*\(/.test(line)) {
6985
+ const block = lines.slice(i, i + 4).join("\n");
6986
+ if (/<[a-zA-Z]/.test(block) && !/key\s*=/.test(block)) {
6987
+ issues.push({
6988
+ rule: "react/missing-key",
6989
+ file: filePath,
6990
+ line: lineNum,
6991
+ message: "Missing 'key' prop in list rendering \u2014 elements need unique keys",
6992
+ severity: "error"
6993
+ });
6994
+ }
6995
+ }
6996
+ const lowerFnMatch = /\bfunction\s+([a-z]\w*)\s*\([^)]*\)\s*\{/.exec(line);
6997
+ if (lowerFnMatch && inComponent) {
6998
+ const nextContent = lines.slice(i, i + 20).join("\n");
6999
+ if (/<[A-Z]|return\s*\(/.test(nextContent)) {
7000
+ issues.push({
7001
+ rule: "react/component-naming",
7002
+ file: filePath,
7003
+ line: lineNum,
7004
+ message: `Component '${lowerFnMatch[1]}' should use PascalCase naming`,
7005
+ severity: "warning"
7006
+ });
7007
+ }
7008
+ }
7009
+ if (/\bfunction\s+[A-Z]\w*\s*\(\s*props\s*\)/.test(line)) {
7010
+ issues.push({
7011
+ rule: "react/prop-types",
7012
+ file: filePath,
7013
+ line: lineNum,
7014
+ message: "Component props are not typed \u2014 use a TypeScript interface or destructure with types",
7015
+ severity: "error"
7016
+ });
7017
+ }
7018
+ if (/^\s*export\s+(?:default\s+)?function\s+[A-Z]\w*/.test(line)) {
7019
+ let prevLineIdx = i - 1;
7020
+ while (prevLineIdx >= 0 && (lines[prevLineIdx] ?? "").trim() === "") {
7021
+ prevLineIdx--;
7022
+ }
7023
+ const prevLine = (lines[prevLineIdx] ?? "").trim();
7024
+ if (!prevLine.endsWith("*/")) {
7025
+ issues.push({
7026
+ rule: "react/missing-jsdoc",
7027
+ file: filePath,
7028
+ line: lineNum,
7029
+ message: "Exported component missing JSDoc comment",
7030
+ severity: "warning"
7031
+ });
7032
+ }
7033
+ }
7034
+ if (/document\.getElementById|document\.querySelector|document\.createElement/.test(line)) {
7035
+ issues.push({
7036
+ rule: "react/no-direct-dom-manipulation",
7037
+ file: filePath,
7038
+ line: lineNum,
7039
+ message: "Avoid direct DOM manipulation in React \u2014 use refs or state instead",
7040
+ severity: "warning"
7041
+ });
7042
+ }
7043
+ if (/dangerouslySetInnerHTML/.test(line) && !/sanitize|DOMPurify|xss/.test(line)) {
7044
+ issues.push({
7045
+ rule: "react/no-danger",
7046
+ file: filePath,
7047
+ line: lineNum,
7048
+ message: "dangerouslySetInnerHTML can lead to XSS \u2014 ensure content is sanitized",
7049
+ severity: "error"
7050
+ });
7051
+ }
7052
+ }
7053
+ }
7054
+ const deduction = issues.reduce((s, i) => s + (i.severity === "error" ? 10 : 5), 0);
7055
+ const score = Math.max(0, 100 - deduction);
7056
+ return { score, totalComponents, issues };
7057
+ }
7058
+ };
7059
+ var ReactA11yAnalyzer = class {
7060
+ constructor(projectPath) {
7061
+ this.projectPath = projectPath;
7062
+ }
7063
+ async analyze(files) {
7064
+ const reactFiles = files ?? await findReactFiles(this.projectPath);
7065
+ if (!reactFiles.length) return { score: 100, violations: [] };
7066
+ const fileContents = await loadFiles(reactFiles);
7067
+ return this.analyzeContent(fileContents);
7068
+ }
7069
+ analyzeContent(files) {
7070
+ if (!files.length) return { score: 100, violations: [] };
7071
+ const violations = [];
7072
+ for (const { path: filePath, content } of files) {
7073
+ violations.push(...this.checkFile(filePath, content));
7074
+ }
7075
+ const deduction = violations.reduce((s, v) => s + (v.severity === "error" ? 12 : 6), 0);
7076
+ const score = Math.max(0, 100 - deduction);
7077
+ return { score, violations };
7078
+ }
7079
+ checkFile(filePath, content) {
7080
+ const violations = [];
7081
+ const lines = content.split("\n");
7082
+ for (let i = 0; i < lines.length; i++) {
7083
+ const line = lines[i] ?? "";
7084
+ const lineNum = i + 1;
7085
+ if (/<img\b/.test(line)) {
7086
+ const imgBlock = lines.slice(i, Math.min(i + 4, lines.length)).join("\n");
7087
+ if (!/alt\s*=/.test(imgBlock)) {
7088
+ violations.push({
7089
+ rule: "jsx-a11y/alt-text",
7090
+ file: filePath,
7091
+ line: lineNum,
7092
+ message: "<img> element missing 'alt' attribute",
7093
+ severity: "error",
7094
+ wcag: "WCAG 1.1.1"
7095
+ });
7096
+ }
7097
+ }
7098
+ if (/<a\b/.test(line)) {
7099
+ const aBlock = lines.slice(i, Math.min(i + 4, lines.length)).join("\n");
7100
+ if (!/href\s*=/.test(aBlock) && !/ role\s*=/.test(aBlock)) {
7101
+ violations.push({
7102
+ rule: "jsx-a11y/anchor-has-content",
7103
+ file: filePath,
7104
+ line: lineNum,
7105
+ message: "<a> element missing href \u2014 use a <button> for non-navigation actions",
7106
+ severity: "error",
7107
+ wcag: "WCAG 2.1.1"
7108
+ });
7109
+ }
7110
+ }
7111
+ if (/<(?:div|span)\b[^>]*onClick[^>]*>/.test(line)) {
7112
+ const hasKeyboardSupport = /onKey(?:Down|Up|Press)|role\s*=\s*["'](?:button|link|menuitem)|tabIndex/.test(line);
7113
+ if (!hasKeyboardSupport) {
7114
+ const context = lines.slice(Math.max(0, i - 1), i + 3).join(" ");
7115
+ if (!/onKey(?:Down|Up|Press)|tabIndex/.test(context)) {
7116
+ violations.push({
7117
+ rule: "jsx-a11y/interactive-supports-focus",
7118
+ file: filePath,
7119
+ line: lineNum,
7120
+ message: "Non-interactive element with onClick handler \u2014 add keyboard support (onKeyDown, tabIndex, role)",
7121
+ severity: "warning",
7122
+ wcag: "WCAG 2.1.1"
7123
+ });
7124
+ }
7125
+ }
7126
+ }
7127
+ if (/<input\b[^>]*>/.test(line) && !/aria-label|aria-labelledby|id\s*=/.test(line)) {
7128
+ violations.push({
7129
+ rule: "jsx-a11y/label-association",
7130
+ file: filePath,
7131
+ line: lineNum,
7132
+ message: "<input> missing label association (aria-label, aria-labelledby, or id for <label>)",
7133
+ severity: "warning",
7134
+ wcag: "WCAG 1.3.1"
7135
+ });
7136
+ }
7137
+ if (/<video\b[^>]*autoPlay[^>]*>/.test(line) && !/controls/.test(line)) {
7138
+ violations.push({
7139
+ rule: "jsx-a11y/media-has-caption",
7140
+ file: filePath,
7141
+ line: lineNum,
7142
+ message: "Autoplaying video without controls \u2014 add controls attribute",
7143
+ severity: "warning",
7144
+ wcag: "WCAG 1.2.2"
7145
+ });
7146
+ }
7147
+ }
7148
+ return violations;
7149
+ }
7150
+ };
7151
+ var ReactHookAnalyzer = class {
7152
+ constructor(projectPath) {
7153
+ this.projectPath = projectPath;
7154
+ }
7155
+ async analyze(files) {
7156
+ const reactFiles = files ?? await findReactFiles(this.projectPath, "**/*.{tsx,jsx,ts,js}");
7157
+ if (!reactFiles.length) return { score: 100, violations: [] };
7158
+ const fileContents = await loadFiles(reactFiles);
7159
+ return this.analyzeContent(fileContents);
7160
+ }
7161
+ analyzeContent(files) {
7162
+ if (!files.length) return { score: 100, violations: [] };
7163
+ const violations = [];
7164
+ for (const { path: filePath, content } of files) {
7165
+ violations.push(...this.checkFile(filePath, content));
7166
+ }
7167
+ const deduction = violations.reduce((s, v) => s + (v.severity === "error" ? 15 : 7), 0);
7168
+ const score = Math.max(0, 100 - deduction);
7169
+ return { score, violations };
7170
+ }
7171
+ checkFile(filePath, content) {
7172
+ const violations = [];
7173
+ const lines = content.split("\n");
7174
+ let conditionalDepth = 0;
7175
+ let loopDepth = 0;
7176
+ for (let i = 0; i < lines.length; i++) {
7177
+ const line = lines[i] ?? "";
7178
+ const lineNum = i + 1;
7179
+ const trimmed = line.trim();
7180
+ const openBraces = (trimmed.match(/\{/g) ?? []).length;
7181
+ const closeBraces = (trimmed.match(/\}/g) ?? []).length;
7182
+ if (/^\s*(?:if\s*\(|else\s+if\s*\(|else\s*\{)/.test(line) && openBraces > closeBraces) {
7183
+ conditionalDepth++;
7184
+ }
7185
+ if (/^\s*(?:for\s*\(|while\s*\()/.test(line) && openBraces > closeBraces) {
7186
+ loopDepth++;
7187
+ }
7188
+ if (/\.forEach\s*\(/.test(line) && openBraces > closeBraces) {
7189
+ loopDepth++;
7190
+ }
7191
+ if (/\buseEffect\s*\(/.test(line)) {
7192
+ const block = lines.slice(i, i + 5).join("\n");
7193
+ if (!/,\s*\[/.test(block)) {
7194
+ violations.push({
7195
+ rule: "react-hooks/exhaustive-deps",
7196
+ file: filePath,
7197
+ line: lineNum,
7198
+ message: "useEffect without dependency array \u2014 runs on every render (possibly unintentional)",
7199
+ severity: "warning"
7200
+ });
7201
+ }
7202
+ }
7203
+ if (conditionalDepth > 0 && /\buse[A-Z]\w*\s*\(/.test(line)) {
7204
+ violations.push({
7205
+ rule: "react-hooks/rules-of-hooks",
7206
+ file: filePath,
7207
+ line: lineNum,
7208
+ message: "Hook called inside conditional \u2014 violates Rules of Hooks",
7209
+ severity: "error"
7210
+ });
7211
+ }
7212
+ if (loopDepth > 0 && /\buse[A-Z]\w*\s*\(/.test(line)) {
7213
+ violations.push({
7214
+ rule: "react-hooks/rules-of-hooks",
7215
+ file: filePath,
7216
+ line: lineNum,
7217
+ message: "Hook called inside loop \u2014 violates Rules of Hooks",
7218
+ severity: "error"
7219
+ });
7220
+ }
7221
+ const netBraces = openBraces - closeBraces;
7222
+ if (netBraces < 0) {
7223
+ if (conditionalDepth > 0) conditionalDepth = Math.max(0, conditionalDepth + netBraces);
7224
+ if (loopDepth > 0) loopDepth = Math.max(0, loopDepth + netBraces);
7225
+ }
7226
+ }
7227
+ return violations;
7228
+ }
7229
+ };
7230
+ function registerReactAnalyzers(registry, projectPath) {
7231
+ const componentAnalyzer = new ReactComponentAnalyzer(projectPath);
7232
+ const a11yAnalyzer = new ReactA11yAnalyzer(projectPath);
7233
+ const hookAnalyzer = new ReactHookAnalyzer(projectPath);
7234
+ for (const lang of ["react-typescript", "react-javascript"]) {
7235
+ registry.register({
7236
+ dimensionId: "style",
7237
+ language: lang,
7238
+ async analyze(input) {
7239
+ const contents = await Promise.all(
7240
+ input.files.map(async (f) => ({
7241
+ path: f,
7242
+ content: await readFile(f, "utf-8").catch(() => "")
7243
+ }))
7244
+ );
7245
+ const result = componentAnalyzer.analyzeContent(contents);
7246
+ return {
7247
+ score: result.score,
7248
+ issues: result.issues.map((i) => ({
7249
+ dimension: "style",
7250
+ severity: i.severity === "error" ? "major" : "minor",
7251
+ message: i.message,
7252
+ file: i.file,
7253
+ line: i.line
7254
+ }))
7255
+ };
7256
+ }
7257
+ });
7258
+ registry.register({
7259
+ dimensionId: "robustness",
7260
+ language: lang,
7261
+ async analyze(input) {
7262
+ const contents = await Promise.all(
7263
+ input.files.map(async (f) => ({
7264
+ path: f,
7265
+ content: await readFile(f, "utf-8").catch(() => "")
7266
+ }))
7267
+ );
7268
+ const result = a11yAnalyzer.analyzeContent(contents);
7269
+ return {
7270
+ score: result.score,
7271
+ issues: result.violations.map((v) => ({
7272
+ dimension: "robustness",
7273
+ severity: v.severity === "error" ? "major" : "minor",
7274
+ message: v.message,
7275
+ file: v.file,
7276
+ line: v.line
7277
+ }))
7278
+ };
7279
+ }
7280
+ });
7281
+ registry.register({
7282
+ dimensionId: "correctness",
7283
+ language: lang,
7284
+ async analyze(input) {
7285
+ const contents = await Promise.all(
7286
+ input.files.map(async (f) => ({
7287
+ path: f,
7288
+ content: await readFile(f, "utf-8").catch(() => "")
7289
+ }))
7290
+ );
7291
+ const result = hookAnalyzer.analyzeContent(contents);
7292
+ return {
7293
+ score: result.score,
7294
+ issues: result.violations.map((v) => ({
7295
+ dimension: "correctness",
7296
+ severity: v.severity === "error" ? "critical" : "minor",
7297
+ message: v.message,
7298
+ file: v.file,
7299
+ line: v.line
7300
+ }))
7301
+ };
7302
+ }
7303
+ });
5993
7304
  }
5994
- traverse(ast);
5995
- return {
5996
- lineCount: 0,
5997
- // Will be set separately from content
5998
- functionCount,
5999
- importCount,
6000
- crossBoundaryImportCount
6001
- };
6002
7305
  }
6003
- function isCrossBoundaryImport(node, _filePath) {
6004
- if (node.source.type !== "Literal" || typeof node.source.value !== "string") {
6005
- return false;
6006
- }
6007
- const importPath = node.source.value;
6008
- if (importPath.startsWith("node:")) {
6009
- return false;
6010
- }
6011
- if (!importPath.startsWith(".")) {
6012
- return false;
7306
+ var DimensionRegistry = class {
7307
+ analyzers = [];
7308
+ /**
7309
+ * Register a dimension analyzer plugin.
7310
+ * If an analyzer for the same (language, dimensionId) pair already exists,
7311
+ * it is replaced — preventing order-dependent duplicate accumulation.
7312
+ */
7313
+ register(analyzer) {
7314
+ const existing = this.analyzers.findIndex(
7315
+ (a) => a.language === analyzer.language && a.dimensionId === analyzer.dimensionId
7316
+ );
7317
+ if (existing !== -1) {
7318
+ this.analyzers[existing] = analyzer;
7319
+ } else {
7320
+ this.analyzers.push(analyzer);
7321
+ }
6013
7322
  }
6014
- if (importPath.startsWith("../")) {
6015
- return true;
7323
+ /**
7324
+ * Get all analyzers applicable for a given language.
7325
+ * Includes both language-specific analyzers and "all" analyzers.
7326
+ *
7327
+ * @param language - Target language
7328
+ * @param dimensionId - Optional filter for a specific dimension
7329
+ */
7330
+ getAnalyzers(language, dimensionId) {
7331
+ return this.analyzers.filter(
7332
+ (a) => (a.language === language || a.language === "all") && (dimensionId === void 0 || a.dimensionId === dimensionId)
7333
+ );
6016
7334
  }
6017
- if (importPath.startsWith("./")) {
6018
- const relativePath = importPath.slice(2);
6019
- return relativePath.includes("/");
7335
+ /**
7336
+ * Check if any analyzers are registered for a given language
7337
+ * (includes "all" analyzers)
7338
+ */
7339
+ hasAnalyzers(language) {
7340
+ return this.analyzers.some((a) => a.language === language || a.language === "all");
6020
7341
  }
6021
- return false;
6022
- }
6023
- function countLines(content) {
6024
- return content.split("\n").length;
6025
- }
6026
- var MaintainabilityAnalyzer = class {
6027
- constructor(projectPath) {
6028
- this.projectPath = projectPath;
7342
+ /**
7343
+ * Get all languages that have registered analyzers
7344
+ * (excludes the "all" pseudo-language)
7345
+ */
7346
+ getSupportedLanguages() {
7347
+ const languages = /* @__PURE__ */ new Set();
7348
+ for (const a of this.analyzers) {
7349
+ if (a.language !== "all") {
7350
+ languages.add(a.language);
7351
+ }
7352
+ }
7353
+ return Array.from(languages);
6029
7354
  }
6030
7355
  /**
6031
- * Analyze maintainability of project files
7356
+ * Run all matching analyzers for the given input and return combined results.
7357
+ * Analyzers run in parallel for performance.
6032
7358
  */
6033
- async analyze(files) {
6034
- const targetFiles = files ?? await this.findSourceFiles();
6035
- if (targetFiles.length === 0) {
7359
+ async analyze(input) {
7360
+ const matching = this.getAnalyzers(input.language);
7361
+ if (!matching.length) return [];
7362
+ const settled = await Promise.allSettled(
7363
+ matching.map(
7364
+ (analyzer) => analyzer.analyze(input).then((result) => ({ dimensionId: analyzer.dimensionId, result }))
7365
+ )
7366
+ );
7367
+ return settled.filter((s) => {
7368
+ if (s.status === "rejected") {
7369
+ process.stderr.write(`[DimensionRegistry] Analyzer failed: ${String(s.reason)}
7370
+ `);
7371
+ }
7372
+ return s.status === "fulfilled";
7373
+ }).map((s) => s.value);
7374
+ }
7375
+ };
7376
+ function wrapAnalyzer(dimensionId, language, analyzerFn) {
7377
+ return {
7378
+ dimensionId,
7379
+ language,
7380
+ async analyze(input) {
7381
+ const raw = await analyzerFn(input);
6036
7382
  return {
6037
- score: 100,
6038
- fileLengthScore: 100,
6039
- functionCountScore: 100,
6040
- dependencyCountScore: 100,
6041
- couplingScore: 100,
6042
- averageFileLength: 0,
6043
- averageFunctionsPerFile: 0,
6044
- averageImportsPerFile: 0,
6045
- fileCount: 0,
6046
- details: "No files to analyze"
7383
+ score: raw.score,
7384
+ issues: raw.issues ?? []
6047
7385
  };
6048
7386
  }
6049
- let totalLines = 0;
6050
- let totalFunctions = 0;
6051
- let totalImports = 0;
6052
- let totalCrossBoundaryImports = 0;
6053
- let filesAnalyzed = 0;
6054
- for (const file of targetFiles) {
6055
- try {
6056
- const content = await readFile(file, "utf-8");
6057
- const lineCount = countLines(content);
6058
- const ast = parse(content, {
6059
- loc: true,
6060
- range: true,
6061
- jsx: file.endsWith(".tsx") || file.endsWith(".jsx")
6062
- });
6063
- const result = analyzeMaintainabilityPatterns(ast, file);
6064
- totalLines += lineCount;
6065
- totalFunctions += result.functionCount;
6066
- totalImports += result.importCount;
6067
- totalCrossBoundaryImports += result.crossBoundaryImportCount;
6068
- filesAnalyzed++;
6069
- } catch {
6070
- }
6071
- }
6072
- const averageFileLength = filesAnalyzed > 0 ? totalLines / filesAnalyzed : 0;
6073
- const averageFunctionsPerFile = filesAnalyzed > 0 ? totalFunctions / filesAnalyzed : 0;
6074
- const averageImportsPerFile = filesAnalyzed > 0 ? totalImports / filesAnalyzed : 0;
6075
- const fileLengthScore = Math.max(0, Math.min(100, 100 - (averageFileLength - 200) * 0.33));
6076
- const functionCountScore = Math.max(0, Math.min(100, 100 - (averageFunctionsPerFile - 10) * 5));
6077
- const dependencyCountScore = Math.max(0, Math.min(100, 100 - (averageImportsPerFile - 5) * 5));
6078
- const crossBoundaryRatio = totalImports > 0 ? totalCrossBoundaryImports / totalImports : 0;
6079
- const couplingScore = Math.max(0, Math.min(100, 100 - crossBoundaryRatio * 100 * 0.5));
6080
- const score = Math.round(
6081
- fileLengthScore * 0.3 + functionCountScore * 0.25 + dependencyCountScore * 0.25 + couplingScore * 0.2
7387
+ };
7388
+ }
7389
+ function createDefaultRegistry(projectPath) {
7390
+ const registry = new DimensionRegistry();
7391
+ const coverageAnalyzer = new CoverageAnalyzer(projectPath);
7392
+ const securityScanner = new CompositeSecurityScanner(projectPath, false);
7393
+ const complexityAnalyzer = new ComplexityAnalyzer(projectPath);
7394
+ const duplicationAnalyzer = new DuplicationAnalyzer(projectPath);
7395
+ const correctnessAnalyzer = new CorrectnessAnalyzer(projectPath);
7396
+ const completenessAnalyzer = new CompletenessAnalyzer(projectPath);
7397
+ const robustnessAnalyzer = new RobustnessAnalyzer(projectPath);
7398
+ const testQualityAnalyzer = new TestQualityAnalyzer(projectPath);
7399
+ const documentationAnalyzer = new DocumentationAnalyzer(projectPath);
7400
+ const styleAnalyzer = new StyleAnalyzer(projectPath);
7401
+ const readabilityAnalyzer = new ReadabilityAnalyzer(projectPath);
7402
+ const maintainabilityAnalyzer = new MaintainabilityAnalyzer(projectPath);
7403
+ const baseLangs = ["typescript", "javascript"];
7404
+ const reactLangs = ["react-typescript", "react-javascript"];
7405
+ for (const lang of baseLangs) {
7406
+ registry.register(
7407
+ wrapAnalyzer("correctness", lang, async () => {
7408
+ const r = await correctnessAnalyzer.analyze();
7409
+ return { score: r.score };
7410
+ })
7411
+ );
7412
+ registry.register(
7413
+ wrapAnalyzer("completeness", lang, async (input) => {
7414
+ const r = await completenessAnalyzer.analyze(input.files);
7415
+ return { score: r.score };
7416
+ })
7417
+ );
7418
+ registry.register(
7419
+ wrapAnalyzer("robustness", lang, async (input) => {
7420
+ const r = await robustnessAnalyzer.analyze(input.files);
7421
+ return { score: r.score };
7422
+ })
7423
+ );
7424
+ registry.register(
7425
+ wrapAnalyzer("readability", lang, async (input) => {
7426
+ const r = await readabilityAnalyzer.analyze(input.files);
7427
+ return { score: r.score };
7428
+ })
7429
+ );
7430
+ registry.register(
7431
+ wrapAnalyzer("maintainability", lang, async (input) => {
7432
+ const r = await maintainabilityAnalyzer.analyze(input.files);
7433
+ return { score: r.score };
7434
+ })
7435
+ );
7436
+ registry.register(
7437
+ wrapAnalyzer("complexity", lang, async (input) => {
7438
+ const r = await complexityAnalyzer.analyze(input.files);
7439
+ return { score: r.score };
7440
+ })
7441
+ );
7442
+ registry.register(
7443
+ wrapAnalyzer("duplication", lang, async (input) => {
7444
+ const r = await duplicationAnalyzer.analyze(input.files);
7445
+ return { score: Math.max(0, 100 - r.percentage) };
7446
+ })
7447
+ );
7448
+ registry.register(
7449
+ wrapAnalyzer("testCoverage", lang, async () => {
7450
+ const r = await coverageAnalyzer.analyze();
7451
+ return { score: r?.lines.percentage ?? 0 };
7452
+ })
7453
+ );
7454
+ registry.register(
7455
+ wrapAnalyzer("testQuality", lang, async () => {
7456
+ const r = await testQualityAnalyzer.analyze();
7457
+ return { score: r.score };
7458
+ })
7459
+ );
7460
+ registry.register(
7461
+ wrapAnalyzer("security", lang, async (input) => {
7462
+ const fileContents = await Promise.all(
7463
+ input.files.map(async (f) => ({
7464
+ path: f,
7465
+ content: await readFile(f, "utf-8").catch(() => "")
7466
+ }))
7467
+ );
7468
+ const r = await securityScanner.scan(fileContents);
7469
+ return { score: r.score };
7470
+ })
7471
+ );
7472
+ registry.register(
7473
+ wrapAnalyzer("documentation", lang, async (input) => {
7474
+ const r = await documentationAnalyzer.analyze(input.files);
7475
+ return { score: r.score };
7476
+ })
7477
+ );
7478
+ registry.register(
7479
+ wrapAnalyzer("style", lang, async () => {
7480
+ const r = await styleAnalyzer.analyze();
7481
+ return { score: r.score };
7482
+ })
6082
7483
  );
6083
- const details = [
6084
- `${Math.round(averageFileLength)} avg lines/file`,
6085
- `${averageFunctionsPerFile.toFixed(1)} avg functions/file`,
6086
- `${averageImportsPerFile.toFixed(1)} avg imports/file`,
6087
- `${Math.round(crossBoundaryRatio * 100)}% cross-boundary coupling`
6088
- ].join(", ");
6089
- return {
6090
- score,
6091
- fileLengthScore,
6092
- functionCountScore,
6093
- dependencyCountScore,
6094
- couplingScore,
6095
- averageFileLength,
6096
- averageFunctionsPerFile,
6097
- averageImportsPerFile,
6098
- fileCount: filesAnalyzed,
6099
- details
6100
- };
6101
7484
  }
6102
- async findSourceFiles() {
6103
- return glob("**/*.{ts,js,tsx,jsx}", {
6104
- cwd: this.projectPath,
6105
- absolute: true,
6106
- ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/dist/**", "**/build/**"]
6107
- });
7485
+ for (const lang of reactLangs) {
7486
+ registry.register(
7487
+ wrapAnalyzer("completeness", lang, async (input) => {
7488
+ const r = await completenessAnalyzer.analyze(input.files);
7489
+ return { score: r.score };
7490
+ })
7491
+ );
7492
+ registry.register(
7493
+ wrapAnalyzer("readability", lang, async (input) => {
7494
+ const r = await readabilityAnalyzer.analyze(input.files);
7495
+ return { score: r.score };
7496
+ })
7497
+ );
7498
+ registry.register(
7499
+ wrapAnalyzer("maintainability", lang, async (input) => {
7500
+ const r = await maintainabilityAnalyzer.analyze(input.files);
7501
+ return { score: r.score };
7502
+ })
7503
+ );
7504
+ registry.register(
7505
+ wrapAnalyzer("complexity", lang, async (input) => {
7506
+ const r = await complexityAnalyzer.analyze(input.files);
7507
+ return { score: r.score };
7508
+ })
7509
+ );
7510
+ registry.register(
7511
+ wrapAnalyzer("duplication", lang, async (input) => {
7512
+ const r = await duplicationAnalyzer.analyze(input.files);
7513
+ return { score: Math.max(0, 100 - r.percentage) };
7514
+ })
7515
+ );
7516
+ registry.register(
7517
+ wrapAnalyzer("testCoverage", lang, async () => {
7518
+ const r = await coverageAnalyzer.analyze();
7519
+ return { score: r?.lines.percentage ?? 0 };
7520
+ })
7521
+ );
7522
+ registry.register(
7523
+ wrapAnalyzer("testQuality", lang, async () => {
7524
+ const r = await testQualityAnalyzer.analyze();
7525
+ return { score: r.score };
7526
+ })
7527
+ );
7528
+ registry.register(
7529
+ wrapAnalyzer("security", lang, async (input) => {
7530
+ const fileContents = await Promise.all(
7531
+ input.files.map(async (f) => ({
7532
+ path: f,
7533
+ content: await readFile(f, "utf-8").catch(() => "")
7534
+ }))
7535
+ );
7536
+ const r = await securityScanner.scan(fileContents);
7537
+ return { score: r.score };
7538
+ })
7539
+ );
7540
+ registry.register(
7541
+ wrapAnalyzer("documentation", lang, async (input) => {
7542
+ const r = await documentationAnalyzer.analyze(input.files);
7543
+ return { score: r.score };
7544
+ })
7545
+ );
6108
7546
  }
6109
- };
7547
+ return registry;
7548
+ }
7549
+
7550
+ // src/quality/evaluator.ts
6110
7551
  var QualityEvaluator = class {
6111
- constructor(projectPath, useSnyk = false) {
7552
+ constructor(projectPath, useSnyk = false, registry) {
6112
7553
  this.projectPath = projectPath;
7554
+ this.registry = registry;
6113
7555
  this.coverageAnalyzer = new CoverageAnalyzer(projectPath);
6114
7556
  this.securityScanner = new CompositeSecurityScanner(projectPath, useSnyk);
6115
7557
  this.complexityAnalyzer = new ComplexityAnalyzer(projectPath);
@@ -6145,7 +7587,7 @@ var QualityEvaluator = class {
6145
7587
  const fileContents = await Promise.all(
6146
7588
  targetFiles.map(async (file) => ({
6147
7589
  path: file,
6148
- content: await readFile(file, "utf-8")
7590
+ content: await readFile(file, "utf-8").catch(() => "")
6149
7591
  }))
6150
7592
  );
6151
7593
  const [
@@ -6189,8 +7631,27 @@ var QualityEvaluator = class {
6189
7631
  testQuality: testQualityResult.score,
6190
7632
  documentation: documentationResult.score
6191
7633
  };
7634
+ if (this.registry) {
7635
+ const { language } = detectProjectLanguage(targetFiles);
7636
+ const registryResults = await this.registry.analyze({
7637
+ projectPath: this.projectPath,
7638
+ files: targetFiles,
7639
+ language
7640
+ });
7641
+ for (const { dimensionId, result } of registryResults) {
7642
+ if (dimensionId in dimensions) {
7643
+ dimensions[dimensionId] = result.score;
7644
+ }
7645
+ }
7646
+ }
7647
+ let projectConfig = null;
7648
+ try {
7649
+ projectConfig = await loadProjectConfig(this.projectPath);
7650
+ } catch {
7651
+ }
7652
+ const weights = resolvedWeights(projectConfig);
6192
7653
  const overall = Object.entries(dimensions).reduce((sum, [key, value]) => {
6193
- const weight = DEFAULT_QUALITY_WEIGHTS[key] ?? 0;
7654
+ const weight = weights[key] ?? 0;
6194
7655
  return sum + value * weight;
6195
7656
  }, 0);
6196
7657
  const scores = {
@@ -6208,13 +7669,15 @@ var QualityEvaluator = class {
6208
7669
  documentationResult
6209
7670
  );
6210
7671
  const suggestions = this.generateSuggestions(dimensions);
6211
- const meetsMinimum = scores.overall >= DEFAULT_QUALITY_THRESHOLDS.minimum.overall && dimensions.testCoverage >= DEFAULT_QUALITY_THRESHOLDS.minimum.testCoverage && dimensions.security >= DEFAULT_QUALITY_THRESHOLDS.minimum.security;
6212
- const meetsTarget = scores.overall >= DEFAULT_QUALITY_THRESHOLDS.target.overall && dimensions.testCoverage >= DEFAULT_QUALITY_THRESHOLDS.target.testCoverage;
7672
+ const thresholds = resolvedThresholds(projectConfig);
7673
+ const meetsMinimum = scores.overall >= thresholds.minimum.overall && dimensions.testCoverage >= thresholds.minimum.testCoverage && dimensions.security >= thresholds.minimum.security;
7674
+ const meetsTarget = scores.overall >= thresholds.target.overall && dimensions.testCoverage >= thresholds.target.testCoverage;
7675
+ const converged = scores.overall >= thresholds.target.overall;
6213
7676
  return {
6214
7677
  scores,
6215
7678
  meetsMinimum,
6216
7679
  meetsTarget,
6217
- converged: false,
7680
+ converged,
6218
7681
  issues,
6219
7682
  suggestions
6220
7683
  };
@@ -6363,8 +7826,11 @@ var QualityEvaluator = class {
6363
7826
  });
6364
7827
  }
6365
7828
  };
6366
- function createQualityEvaluator(projectPath, useSnyk) {
6367
- return new QualityEvaluator(projectPath, useSnyk);
7829
+ function createQualityEvaluatorWithRegistry(projectPath, useSnyk) {
7830
+ const registry = createDefaultRegistry(projectPath);
7831
+ registerJavaAnalyzers(registry, projectPath);
7832
+ registerReactAnalyzers(registry, projectPath);
7833
+ return new QualityEvaluator(projectPath, useSnyk ?? false, registry);
6368
7834
  }
6369
7835
 
6370
7836
  // src/phases/complete/prompts.ts
@@ -6674,7 +8140,7 @@ function setupFileLogging(logger, logDir, name) {
6674
8140
  if (!fs4__default.existsSync(logDir)) {
6675
8141
  fs4__default.mkdirSync(logDir, { recursive: true });
6676
8142
  }
6677
- const logFile = path14__default.join(logDir, `${name}.log`);
8143
+ const logFile = path15__default.join(logDir, `${name}.log`);
6678
8144
  logger.attachTransport((logObj) => {
6679
8145
  const line = JSON.stringify(logObj) + "\n";
6680
8146
  fs4__default.appendFileSync(logFile, line);
@@ -8057,13 +9523,13 @@ var CompleteExecutor = class {
8057
9523
  */
8058
9524
  async checkpoint(context) {
8059
9525
  if (this.checkpointState && this.currentSprint) {
8060
- const checkpointPath = path14__default.join(
9526
+ const checkpointPath = path15__default.join(
8061
9527
  context.projectPath,
8062
9528
  ".coco",
8063
9529
  "checkpoints",
8064
9530
  `complete-${this.currentSprint.id}.json`
8065
9531
  );
8066
- await fs14__default.mkdir(path14__default.dirname(checkpointPath), { recursive: true });
9532
+ await fs14__default.mkdir(path15__default.dirname(checkpointPath), { recursive: true });
8067
9533
  await fs14__default.writeFile(checkpointPath, JSON.stringify(this.checkpointState, null, 2), "utf-8");
8068
9534
  }
8069
9535
  return {
@@ -8084,7 +9550,7 @@ var CompleteExecutor = class {
8084
9550
  const sprintId = checkpoint.resumePoint;
8085
9551
  if (sprintId === "start") return;
8086
9552
  try {
8087
- const checkpointPath = path14__default.join(
9553
+ const checkpointPath = path15__default.join(
8088
9554
  context.projectPath,
8089
9555
  ".coco",
8090
9556
  "checkpoints",
@@ -8337,8 +9803,8 @@ var CompleteExecutor = class {
8337
9803
  };
8338
9804
  const saveFiles = async (files) => {
8339
9805
  for (const file of files) {
8340
- const filePath = path14__default.join(context.projectPath, file.path);
8341
- const dir = path14__default.dirname(filePath);
9806
+ const filePath = path15__default.join(context.projectPath, file.path);
9807
+ const dir = path15__default.dirname(filePath);
8342
9808
  await fs14__default.mkdir(dir, { recursive: true });
8343
9809
  if (file.action === "delete") {
8344
9810
  await fs14__default.unlink(filePath).catch(() => {
@@ -8470,7 +9936,7 @@ var CompleteExecutor = class {
8470
9936
  */
8471
9937
  async loadBacklog(projectPath) {
8472
9938
  try {
8473
- const backlogPath = path14__default.join(projectPath, ".coco", "planning", "backlog.json");
9939
+ const backlogPath = path15__default.join(projectPath, ".coco", "planning", "backlog.json");
8474
9940
  const content = await fs14__default.readFile(backlogPath, "utf-8");
8475
9941
  const data = JSON.parse(content);
8476
9942
  return data.backlog;
@@ -8483,11 +9949,11 @@ var CompleteExecutor = class {
8483
9949
  */
8484
9950
  async loadCurrentSprint(projectPath) {
8485
9951
  try {
8486
- const sprintsDir = path14__default.join(projectPath, ".coco", "planning", "sprints");
9952
+ const sprintsDir = path15__default.join(projectPath, ".coco", "planning", "sprints");
8487
9953
  const files = await fs14__default.readdir(sprintsDir);
8488
9954
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
8489
9955
  if (jsonFiles.length === 0) return null;
8490
- const sprintPath = path14__default.join(sprintsDir, jsonFiles[0] || "");
9956
+ const sprintPath = path15__default.join(sprintsDir, jsonFiles[0] || "");
8491
9957
  const content = await fs14__default.readFile(sprintPath, "utf-8");
8492
9958
  const sprint = JSON.parse(content);
8493
9959
  sprint.startDate = new Date(sprint.startDate);
@@ -8500,11 +9966,11 @@ var CompleteExecutor = class {
8500
9966
  * Save sprint results
8501
9967
  */
8502
9968
  async saveSprintResults(projectPath, result) {
8503
- const resultsDir = path14__default.join(projectPath, ".coco", "results");
9969
+ const resultsDir = path15__default.join(projectPath, ".coco", "results");
8504
9970
  await fs14__default.mkdir(resultsDir, { recursive: true });
8505
- const resultsPath = path14__default.join(resultsDir, `${result.sprintId}-results.json`);
9971
+ const resultsPath = path15__default.join(resultsDir, `${result.sprintId}-results.json`);
8506
9972
  await fs14__default.writeFile(resultsPath, JSON.stringify(result, null, 2), "utf-8");
8507
- const mdPath = path14__default.join(resultsDir, `${result.sprintId}-results.md`);
9973
+ const mdPath = path15__default.join(resultsDir, `${result.sprintId}-results.md`);
8508
9974
  await fs14__default.writeFile(mdPath, this.generateResultsMarkdown(result), "utf-8");
8509
9975
  return resultsPath;
8510
9976
  }
@@ -9729,8 +11195,8 @@ var OutputExecutor = class {
9729
11195
  const cicdGenerator = new CICDGenerator(metadata, cicdConfig);
9730
11196
  const cicdFiles = cicdGenerator.generate();
9731
11197
  for (const file of cicdFiles) {
9732
- const filePath = path14__default.join(context.projectPath, file.path);
9733
- await this.ensureDir(path14__default.dirname(filePath));
11198
+ const filePath = path15__default.join(context.projectPath, file.path);
11199
+ await this.ensureDir(path15__default.dirname(filePath));
9734
11200
  await fs14__default.writeFile(filePath, file.content, "utf-8");
9735
11201
  artifacts.push({
9736
11202
  type: "cicd",
@@ -9741,7 +11207,7 @@ var OutputExecutor = class {
9741
11207
  if (this.config.docker.enabled) {
9742
11208
  const dockerGenerator = new DockerGenerator(metadata);
9743
11209
  const dockerfile2 = dockerGenerator.generateDockerfile();
9744
- const dockerfilePath = path14__default.join(context.projectPath, "Dockerfile");
11210
+ const dockerfilePath = path15__default.join(context.projectPath, "Dockerfile");
9745
11211
  await fs14__default.writeFile(dockerfilePath, dockerfile2, "utf-8");
9746
11212
  artifacts.push({
9747
11213
  type: "deployment",
@@ -9749,7 +11215,7 @@ var OutputExecutor = class {
9749
11215
  description: "Dockerfile"
9750
11216
  });
9751
11217
  const dockerignore = dockerGenerator.generateDockerignore();
9752
- const dockerignorePath = path14__default.join(context.projectPath, ".dockerignore");
11218
+ const dockerignorePath = path15__default.join(context.projectPath, ".dockerignore");
9753
11219
  await fs14__default.writeFile(dockerignorePath, dockerignore, "utf-8");
9754
11220
  artifacts.push({
9755
11221
  type: "deployment",
@@ -9758,7 +11224,7 @@ var OutputExecutor = class {
9758
11224
  });
9759
11225
  if (this.config.docker.compose) {
9760
11226
  const compose = dockerGenerator.generateDockerCompose();
9761
- const composePath = path14__default.join(context.projectPath, "docker-compose.yml");
11227
+ const composePath = path15__default.join(context.projectPath, "docker-compose.yml");
9762
11228
  await fs14__default.writeFile(composePath, compose, "utf-8");
9763
11229
  artifacts.push({
9764
11230
  type: "deployment",
@@ -9770,7 +11236,7 @@ var OutputExecutor = class {
9770
11236
  const docsGenerator = new DocsGenerator(metadata);
9771
11237
  const docs = docsGenerator.generate();
9772
11238
  if (this.config.docs.readme) {
9773
- const readmePath = path14__default.join(context.projectPath, "README.md");
11239
+ const readmePath = path15__default.join(context.projectPath, "README.md");
9774
11240
  await fs14__default.writeFile(readmePath, docs.readme, "utf-8");
9775
11241
  artifacts.push({
9776
11242
  type: "documentation",
@@ -9779,7 +11245,7 @@ var OutputExecutor = class {
9779
11245
  });
9780
11246
  }
9781
11247
  if (this.config.docs.contributing) {
9782
- const contributingPath = path14__default.join(context.projectPath, "CONTRIBUTING.md");
11248
+ const contributingPath = path15__default.join(context.projectPath, "CONTRIBUTING.md");
9783
11249
  await fs14__default.writeFile(contributingPath, docs.contributing, "utf-8");
9784
11250
  artifacts.push({
9785
11251
  type: "documentation",
@@ -9788,7 +11254,7 @@ var OutputExecutor = class {
9788
11254
  });
9789
11255
  }
9790
11256
  if (this.config.docs.changelog) {
9791
- const changelogPath = path14__default.join(context.projectPath, "CHANGELOG.md");
11257
+ const changelogPath = path15__default.join(context.projectPath, "CHANGELOG.md");
9792
11258
  await fs14__default.writeFile(changelogPath, docs.changelog, "utf-8");
9793
11259
  artifacts.push({
9794
11260
  type: "documentation",
@@ -9797,10 +11263,10 @@ var OutputExecutor = class {
9797
11263
  });
9798
11264
  }
9799
11265
  if (this.config.docs.api) {
9800
- const docsDir = path14__default.join(context.projectPath, "docs");
11266
+ const docsDir = path15__default.join(context.projectPath, "docs");
9801
11267
  await this.ensureDir(docsDir);
9802
11268
  if (docs.api) {
9803
- const apiPath = path14__default.join(docsDir, "api.md");
11269
+ const apiPath = path15__default.join(docsDir, "api.md");
9804
11270
  await fs14__default.writeFile(apiPath, docs.api, "utf-8");
9805
11271
  artifacts.push({
9806
11272
  type: "documentation",
@@ -9809,7 +11275,7 @@ var OutputExecutor = class {
9809
11275
  });
9810
11276
  }
9811
11277
  if (docs.deployment) {
9812
- const deployPath = path14__default.join(docsDir, "deployment.md");
11278
+ const deployPath = path15__default.join(docsDir, "deployment.md");
9813
11279
  await fs14__default.writeFile(deployPath, docs.deployment, "utf-8");
9814
11280
  artifacts.push({
9815
11281
  type: "documentation",
@@ -9818,7 +11284,7 @@ var OutputExecutor = class {
9818
11284
  });
9819
11285
  }
9820
11286
  if (docs.development) {
9821
- const devPath = path14__default.join(docsDir, "development.md");
11287
+ const devPath = path15__default.join(docsDir, "development.md");
9822
11288
  await fs14__default.writeFile(devPath, docs.development, "utf-8");
9823
11289
  artifacts.push({
9824
11290
  type: "documentation",
@@ -9884,16 +11350,16 @@ var OutputExecutor = class {
9884
11350
  */
9885
11351
  async loadMetadata(projectPath) {
9886
11352
  try {
9887
- const packagePath = path14__default.join(projectPath, "package.json");
11353
+ const packagePath = path15__default.join(projectPath, "package.json");
9888
11354
  const content = await fs14__default.readFile(packagePath, "utf-8");
9889
11355
  const pkg = JSON.parse(content);
9890
11356
  let packageManager = "npm";
9891
11357
  try {
9892
- await fs14__default.access(path14__default.join(projectPath, "pnpm-lock.yaml"));
11358
+ await fs14__default.access(path15__default.join(projectPath, "pnpm-lock.yaml"));
9893
11359
  packageManager = "pnpm";
9894
11360
  } catch {
9895
11361
  try {
9896
- await fs14__default.access(path14__default.join(projectPath, "yarn.lock"));
11362
+ await fs14__default.access(path15__default.join(projectPath, "yarn.lock"));
9897
11363
  packageManager = "yarn";
9898
11364
  } catch {
9899
11365
  }
@@ -9905,7 +11371,7 @@ var OutputExecutor = class {
9905
11371
  repository = pkg.repository.url;
9906
11372
  }
9907
11373
  return {
9908
- name: pkg.name || path14__default.basename(projectPath),
11374
+ name: pkg.name || path15__default.basename(projectPath),
9909
11375
  description: pkg.description || "",
9910
11376
  version: pkg.version || "0.1.0",
9911
11377
  language: "typescript",
@@ -9919,7 +11385,7 @@ var OutputExecutor = class {
9919
11385
  };
9920
11386
  } catch {
9921
11387
  return {
9922
- name: path14__default.basename(projectPath),
11388
+ name: path15__default.basename(projectPath),
9923
11389
  description: "",
9924
11390
  version: "0.1.0",
9925
11391
  language: "typescript",
@@ -9951,7 +11417,7 @@ var DEFAULT_RETRY_CONFIG = {
9951
11417
  jitterFactor: 0.1
9952
11418
  };
9953
11419
  function sleep(ms) {
9954
- return new Promise((resolve2) => setTimeout(resolve2, ms));
11420
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
9955
11421
  }
9956
11422
  function calculateDelay(baseDelay, jitterFactor, maxDelay) {
9957
11423
  const jitter = baseDelay * jitterFactor * (Math.random() * 2 - 1);
@@ -10054,7 +11520,7 @@ var AnthropicProvider = class {
10054
11520
  model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
10055
11521
  max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
10056
11522
  temperature: options?.temperature ?? this.config.temperature ?? 0,
10057
- system: options?.system,
11523
+ system: this.extractSystem(messages, options?.system),
10058
11524
  messages: this.convertMessages(messages),
10059
11525
  stop_sequences: options?.stopSequences
10060
11526
  });
@@ -10084,7 +11550,7 @@ var AnthropicProvider = class {
10084
11550
  model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
10085
11551
  max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
10086
11552
  temperature: options?.temperature ?? this.config.temperature ?? 0,
10087
- system: options?.system,
11553
+ system: this.extractSystem(messages, options?.system),
10088
11554
  messages: this.convertMessages(messages),
10089
11555
  tools: this.convertTools(options.tools),
10090
11556
  tool_choice: options.toolChoice ? this.convertToolChoice(options.toolChoice) : void 0
@@ -10116,7 +11582,7 @@ var AnthropicProvider = class {
10116
11582
  model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
10117
11583
  max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
10118
11584
  temperature: options?.temperature ?? this.config.temperature ?? 0,
10119
- system: options?.system,
11585
+ system: this.extractSystem(messages, options?.system),
10120
11586
  messages: this.convertMessages(messages)
10121
11587
  });
10122
11588
  for await (const event of stream) {
@@ -10142,7 +11608,7 @@ var AnthropicProvider = class {
10142
11608
  model: options?.model ?? this.config.model ?? DEFAULT_MODEL,
10143
11609
  max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
10144
11610
  temperature: options?.temperature ?? this.config.temperature ?? 0,
10145
- system: options?.system,
11611
+ system: this.extractSystem(messages, options?.system),
10146
11612
  messages: this.convertMessages(messages),
10147
11613
  tools: this.convertTools(options.tools),
10148
11614
  tool_choice: options.toolChoice ? this.convertToolChoice(options.toolChoice) : void 0
@@ -10265,6 +11731,22 @@ var AnthropicProvider = class {
10265
11731
  });
10266
11732
  }
10267
11733
  }
11734
+ /**
11735
+ * Extract system prompt from messages array or options.
11736
+ *
11737
+ * The agent-loop passes the system message as the first element of the
11738
+ * messages array (role: "system"). convertMessages() strips it out because
11739
+ * Anthropic requires it as a top-level parameter — but all callers forgot
11740
+ * to also pass it via options.system. This helper bridges that gap.
11741
+ */
11742
+ extractSystem(messages, optionsSystem) {
11743
+ if (optionsSystem !== void 0) return optionsSystem;
11744
+ const systemMsg = messages.find((m) => m.role === "system");
11745
+ if (!systemMsg) return void 0;
11746
+ if (typeof systemMsg.content === "string") return systemMsg.content;
11747
+ const text = systemMsg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
11748
+ return text || void 0;
11749
+ }
10268
11750
  /**
10269
11751
  * Convert messages to Anthropic format
10270
11752
  */
@@ -10425,6 +11907,7 @@ var CONTEXT_WINDOWS2 = {
10425
11907
  "kimi-k2.5": 262144,
10426
11908
  "kimi-k2-0324": 131072,
10427
11909
  "kimi-latest": 131072,
11910
+ "kimi-for-coding": 131072,
10428
11911
  "moonshot-v1-8k": 8e3,
10429
11912
  "moonshot-v1-32k": 32e3,
10430
11913
  "moonshot-v1-128k": 128e3,
@@ -10454,7 +11937,32 @@ var CONTEXT_WINDOWS2 = {
10454
11937
  // Mistral models
10455
11938
  "mistral-7b": 32768,
10456
11939
  "mistral-nemo": 128e3,
10457
- "mixtral-8x7b": 32768
11940
+ "mixtral-8x7b": 32768,
11941
+ // Mistral AI API models
11942
+ "codestral-latest": 32768,
11943
+ "mistral-large-latest": 131072,
11944
+ "mistral-small-latest": 131072,
11945
+ "open-mixtral-8x22b": 65536,
11946
+ // Groq-hosted models (same models, fast inference)
11947
+ "llama-3.3-70b-versatile": 128e3,
11948
+ "llama-3.1-8b-instant": 128e3,
11949
+ "mixtral-8x7b-32768": 32768,
11950
+ "gemma2-9b-it": 8192,
11951
+ // DeepSeek API models
11952
+ "deepseek-chat": 65536,
11953
+ "deepseek-reasoner": 65536,
11954
+ // Together AI model IDs
11955
+ "Qwen/Qwen2.5-Coder-32B-Instruct": 32768,
11956
+ "meta-llama/Meta-Llama-3.1-70B-Instruct": 128e3,
11957
+ "mistralai/Mixtral-8x7B-Instruct-v0.1": 32768,
11958
+ // HuggingFace model IDs
11959
+ "meta-llama/Llama-3.3-70B-Instruct": 128e3,
11960
+ "microsoft/Phi-4": 16384,
11961
+ // OpenRouter model IDs
11962
+ "anthropic/claude-opus-4-6": 2e5,
11963
+ "openai/gpt-5.3-codex": 4e5,
11964
+ "google/gemini-3-flash-preview": 1e6,
11965
+ "meta-llama/llama-3.3-70b-instruct": 128e3
10458
11966
  };
10459
11967
  var MODELS_WITHOUT_TEMPERATURE = [
10460
11968
  "o1",
@@ -10476,7 +11984,12 @@ var LOCAL_MODEL_PATTERNS = [
10476
11984
  "gemma",
10477
11985
  "starcoder"
10478
11986
  ];
10479
- var MODELS_WITH_THINKING_MODE = ["kimi-k2.5", "kimi-k2-0324", "kimi-latest"];
11987
+ var MODELS_WITH_THINKING_MODE = [
11988
+ "kimi-k2.5",
11989
+ "kimi-k2-0324",
11990
+ "kimi-latest",
11991
+ "kimi-for-coding"
11992
+ ];
10480
11993
  var OpenAIProvider = class {
10481
11994
  id;
10482
11995
  name;
@@ -10741,7 +12254,7 @@ var OpenAIProvider = class {
10741
12254
  input = JSON.parse(repaired);
10742
12255
  console.log(`[${this.name}] \u2713 Successfully repaired JSON for ${builder.name}`);
10743
12256
  }
10744
- } catch (repairError) {
12257
+ } catch {
10745
12258
  console.error(
10746
12259
  `[${this.name}] Cannot repair JSON for ${builder.name}, using empty object`
10747
12260
  );
@@ -11095,6 +12608,20 @@ function createKimiProvider(config) {
11095
12608
  }
11096
12609
  return provider;
11097
12610
  }
12611
+ function createKimiCodeProvider(config) {
12612
+ const provider = new OpenAIProvider("kimi-code", "Kimi Code");
12613
+ const kimiCodeConfig = {
12614
+ ...config,
12615
+ baseUrl: config?.baseUrl ?? process.env["KIMI_CODE_BASE_URL"] ?? "https://api.kimi.com/coding/v1",
12616
+ apiKey: config?.apiKey ?? process.env["KIMI_CODE_API_KEY"],
12617
+ model: config?.model ?? "kimi-for-coding"
12618
+ };
12619
+ if (kimiCodeConfig.apiKey) {
12620
+ provider.initialize(kimiCodeConfig).catch(() => {
12621
+ });
12622
+ }
12623
+ return provider;
12624
+ }
11098
12625
  var OAUTH_CONFIGS = {
11099
12626
  /**
11100
12627
  * OpenAI OAuth (ChatGPT Plus/Pro subscriptions)
@@ -11148,11 +12675,11 @@ async function refreshAccessToken(provider, refreshToken) {
11148
12675
  }
11149
12676
  function getTokenStoragePath(provider) {
11150
12677
  const home = process.env.HOME || process.env.USERPROFILE || "";
11151
- return path14.join(home, ".coco", "tokens", `${provider}.json`);
12678
+ return path15.join(home, ".coco", "tokens", `${provider}.json`);
11152
12679
  }
11153
12680
  async function saveTokens(provider, tokens) {
11154
12681
  const filePath = getTokenStoragePath(provider);
11155
- const dir = path14.dirname(filePath);
12682
+ const dir = path15.dirname(filePath);
11156
12683
  await fs14.mkdir(dir, { recursive: true, mode: 448 });
11157
12684
  await fs14.writeFile(filePath, JSON.stringify(tokens, null, 2), { mode: 384 });
11158
12685
  }
@@ -11197,6 +12724,17 @@ async function getValidAccessToken(provider) {
11197
12724
  }
11198
12725
  return { accessToken: tokens.accessToken, isNew: false };
11199
12726
  }
12727
+ function detectWSL() {
12728
+ if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) return true;
12729
+ try {
12730
+ return /microsoft/i.test(readFileSync("/proc/version", "utf-8"));
12731
+ } catch {
12732
+ return false;
12733
+ }
12734
+ }
12735
+ var isWSL = detectWSL();
12736
+
12737
+ // src/auth/flow.ts
11200
12738
  promisify(execFile);
11201
12739
  var execAsync2 = promisify(exec);
11202
12740
  async function getADCAccessToken() {
@@ -11492,7 +13030,7 @@ var CodexProvider = class {
11492
13030
  const chunk = content.slice(i, i + chunkSize);
11493
13031
  yield { type: "text", text: chunk };
11494
13032
  if (i + chunkSize < content.length) {
11495
- await new Promise((resolve2) => setTimeout(resolve2, 5));
13033
+ await new Promise((resolve3) => setTimeout(resolve3, 5));
11496
13034
  }
11497
13035
  }
11498
13036
  }
@@ -11587,7 +13125,7 @@ var GeminiProvider = class {
11587
13125
  temperature: options?.temperature ?? this.config.temperature ?? 0,
11588
13126
  stopSequences: options?.stopSequences
11589
13127
  },
11590
- systemInstruction: options?.system
13128
+ systemInstruction: this.extractSystem(messages, options?.system)
11591
13129
  });
11592
13130
  const { history, lastMessage } = this.convertMessages(messages);
11593
13131
  const chat = model.startChat({ history });
@@ -11615,7 +13153,7 @@ var GeminiProvider = class {
11615
13153
  maxOutputTokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
11616
13154
  temperature: options?.temperature ?? this.config.temperature ?? 0
11617
13155
  },
11618
- systemInstruction: options?.system,
13156
+ systemInstruction: this.extractSystem(messages, options?.system),
11619
13157
  tools,
11620
13158
  toolConfig: {
11621
13159
  functionCallingConfig: {
@@ -11644,7 +13182,7 @@ var GeminiProvider = class {
11644
13182
  maxOutputTokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
11645
13183
  temperature: options?.temperature ?? this.config.temperature ?? 0
11646
13184
  },
11647
- systemInstruction: options?.system
13185
+ systemInstruction: this.extractSystem(messages, options?.system)
11648
13186
  });
11649
13187
  const { history, lastMessage } = this.convertMessages(messages);
11650
13188
  const chat = model.startChat({ history });
@@ -11678,7 +13216,7 @@ var GeminiProvider = class {
11678
13216
  maxOutputTokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
11679
13217
  temperature: options?.temperature ?? this.config.temperature ?? 0
11680
13218
  },
11681
- systemInstruction: options?.system,
13219
+ systemInstruction: this.extractSystem(messages, options?.system),
11682
13220
  tools,
11683
13221
  toolConfig: {
11684
13222
  functionCallingConfig: {
@@ -11779,6 +13317,21 @@ var GeminiProvider = class {
11779
13317
  });
11780
13318
  }
11781
13319
  }
13320
+ /**
13321
+ * Extract system prompt from messages array or options.
13322
+ *
13323
+ * convertMessages() skips system-role messages ("handled via systemInstruction"),
13324
+ * but all callers forgot to also pass it via options.system. This helper bridges
13325
+ * that gap — mirrors the same fix applied to AnthropicProvider.
13326
+ */
13327
+ extractSystem(messages, optionsSystem) {
13328
+ if (optionsSystem !== void 0) return optionsSystem;
13329
+ const systemMsg = messages.find((m) => m.role === "system");
13330
+ if (!systemMsg) return void 0;
13331
+ if (typeof systemMsg.content === "string") return systemMsg.content;
13332
+ const text = systemMsg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
13333
+ return text || void 0;
13334
+ }
11782
13335
  /**
11783
13336
  * Convert messages to Gemini format
11784
13337
  */
@@ -11977,6 +13530,10 @@ async function createProvider(type, config = {}) {
11977
13530
  provider = createKimiProvider(mergedConfig);
11978
13531
  await provider.initialize(mergedConfig);
11979
13532
  return provider;
13533
+ case "kimi-code":
13534
+ provider = createKimiCodeProvider(mergedConfig);
13535
+ await provider.initialize(mergedConfig);
13536
+ return provider;
11980
13537
  case "lmstudio":
11981
13538
  provider = new OpenAIProvider("lmstudio", "LM Studio");
11982
13539
  mergedConfig.baseUrl = mergedConfig.baseUrl ?? "http://localhost:1234/v1";
@@ -11987,6 +13544,30 @@ async function createProvider(type, config = {}) {
11987
13544
  mergedConfig.baseUrl = mergedConfig.baseUrl ?? "http://localhost:11434/v1";
11988
13545
  mergedConfig.apiKey = mergedConfig.apiKey ?? "ollama";
11989
13546
  break;
13547
+ case "groq":
13548
+ provider = new OpenAIProvider("groq", "Groq");
13549
+ mergedConfig.baseUrl = mergedConfig.baseUrl ?? "https://api.groq.com/openai/v1";
13550
+ break;
13551
+ case "openrouter":
13552
+ provider = new OpenAIProvider("openrouter", "OpenRouter");
13553
+ mergedConfig.baseUrl = mergedConfig.baseUrl ?? "https://openrouter.ai/api/v1";
13554
+ break;
13555
+ case "mistral":
13556
+ provider = new OpenAIProvider("mistral", "Mistral AI");
13557
+ mergedConfig.baseUrl = mergedConfig.baseUrl ?? "https://api.mistral.ai/v1";
13558
+ break;
13559
+ case "deepseek":
13560
+ provider = new OpenAIProvider("deepseek", "DeepSeek");
13561
+ mergedConfig.baseUrl = mergedConfig.baseUrl ?? "https://api.deepseek.com/v1";
13562
+ break;
13563
+ case "together":
13564
+ provider = new OpenAIProvider("together", "Together AI");
13565
+ mergedConfig.baseUrl = mergedConfig.baseUrl ?? "https://api.together.xyz/v1";
13566
+ break;
13567
+ case "huggingface":
13568
+ provider = new OpenAIProvider("huggingface", "HuggingFace Inference");
13569
+ mergedConfig.baseUrl = mergedConfig.baseUrl ?? "https://api-inference.huggingface.co/v1";
13570
+ break;
11990
13571
  default:
11991
13572
  throw new ProviderError(`Unknown provider type: ${type}`, {
11992
13573
  provider: type
@@ -12185,28 +13766,28 @@ async function createPhaseContext(config, state) {
12185
13766
  };
12186
13767
  const tools = {
12187
13768
  file: {
12188
- async read(path37) {
13769
+ async read(path38) {
12189
13770
  const fs36 = await import('fs/promises');
12190
- return fs36.readFile(path37, "utf-8");
13771
+ return fs36.readFile(path38, "utf-8");
12191
13772
  },
12192
- async write(path37, content) {
13773
+ async write(path38, content) {
12193
13774
  const fs36 = await import('fs/promises');
12194
13775
  const nodePath = await import('path');
12195
- await fs36.mkdir(nodePath.dirname(path37), { recursive: true });
12196
- await fs36.writeFile(path37, content, "utf-8");
13776
+ await fs36.mkdir(nodePath.dirname(path38), { recursive: true });
13777
+ await fs36.writeFile(path38, content, "utf-8");
12197
13778
  },
12198
- async exists(path37) {
13779
+ async exists(path38) {
12199
13780
  const fs36 = await import('fs/promises');
12200
13781
  try {
12201
- await fs36.access(path37);
13782
+ await fs36.access(path38);
12202
13783
  return true;
12203
13784
  } catch {
12204
13785
  return false;
12205
13786
  }
12206
13787
  },
12207
13788
  async glob(pattern) {
12208
- const { glob: glob15 } = await import('glob');
12209
- return glob15(pattern, { cwd: state.path });
13789
+ const { glob: glob17 } = await import('glob');
13790
+ return glob17(pattern, { cwd: state.path });
12210
13791
  }
12211
13792
  },
12212
13793
  bash: {
@@ -12478,7 +14059,22 @@ function generateId() {
12478
14059
  return `proj_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
12479
14060
  }
12480
14061
  var ProviderConfigSchema = z.object({
12481
- type: z.enum(["anthropic", "openai", "gemini", "kimi", "lmstudio", "ollama"]).default("anthropic"),
14062
+ type: z.enum([
14063
+ "anthropic",
14064
+ "openai",
14065
+ "codex",
14066
+ "gemini",
14067
+ "kimi",
14068
+ "kimi-code",
14069
+ "lmstudio",
14070
+ "ollama",
14071
+ "groq",
14072
+ "openrouter",
14073
+ "mistral",
14074
+ "deepseek",
14075
+ "together",
14076
+ "huggingface"
14077
+ ]).default("anthropic"),
12482
14078
  apiKey: z.string().optional(),
12483
14079
  model: z.string().default("claude-sonnet-4-20250514"),
12484
14080
  maxTokens: z.number().min(1).max(2e5).default(8192),
@@ -12509,7 +14105,7 @@ var StackConfigSchema = z.object({
12509
14105
  profile: z.string().optional()
12510
14106
  // Custom profile path
12511
14107
  });
12512
- var ProjectConfigSchema = z.object({
14108
+ var ProjectConfigSchema2 = z.object({
12513
14109
  name: z.string().min(1),
12514
14110
  version: z.string().default("0.1.0"),
12515
14111
  description: z.string().optional()
@@ -12582,8 +14178,22 @@ var ShipConfigSchema = z.object({
12582
14178
  /** CI check poll interval in ms (default 15 seconds) */
12583
14179
  ciCheckPollMs: z.number().default(15e3)
12584
14180
  });
14181
+ var SkillsConfigSchema = z.object({
14182
+ /** Enable/disable skills system */
14183
+ enabled: z.boolean().default(true),
14184
+ /** Override global skills directory */
14185
+ globalDir: z.string().optional(),
14186
+ /** Override project skills directory */
14187
+ projectDir: z.string().optional(),
14188
+ /** Auto-activate skills based on user messages */
14189
+ autoActivate: z.boolean().default(true),
14190
+ /** Maximum concurrent active markdown skills */
14191
+ maxActiveSkills: z.number().min(1).max(10).default(3),
14192
+ /** List of skill IDs to disable */
14193
+ disabled: z.array(z.string()).default([])
14194
+ });
12585
14195
  var CocoConfigSchema = z.object({
12586
- project: ProjectConfigSchema,
14196
+ project: ProjectConfigSchema2,
12587
14197
  provider: ProviderConfigSchema.default({
12588
14198
  type: "anthropic",
12589
14199
  model: "claude-sonnet-4-20250514",
@@ -12609,7 +14219,8 @@ var CocoConfigSchema = z.object({
12609
14219
  integrations: IntegrationsConfigSchema.optional(),
12610
14220
  mcp: MCPConfigSchema.optional(),
12611
14221
  tools: ToolsConfigSchema.optional(),
12612
- ship: ShipConfigSchema.optional()
14222
+ ship: ShipConfigSchema.optional(),
14223
+ skills: SkillsConfigSchema.optional()
12613
14224
  });
12614
14225
  function createDefaultConfigObject(projectName, language = "typescript") {
12615
14226
  return {
@@ -12652,7 +14263,7 @@ async function loadConfig(configPath) {
12652
14263
  if (globalConfig) {
12653
14264
  config = deepMergeConfig(config, globalConfig);
12654
14265
  }
12655
- const projectConfigPath = configPath || getProjectConfigPath();
14266
+ const projectConfigPath = configPath || getProjectConfigPath2();
12656
14267
  const projectConfig = await loadConfigFile(projectConfigPath);
12657
14268
  if (projectConfig) {
12658
14269
  config = deepMergeConfig(config, projectConfig);
@@ -12717,8 +14328,8 @@ function deepMergeConfig(base, override) {
12717
14328
  ...base.tools || override.tools ? { tools: { ...base.tools, ...override.tools } } : {}
12718
14329
  };
12719
14330
  }
12720
- function getProjectConfigPath() {
12721
- return path14__default.join(process.cwd(), ".coco", "config.json");
14331
+ function getProjectConfigPath2() {
14332
+ return path15__default.join(process.cwd(), ".coco", "config.json");
12722
14333
  }
12723
14334
  async function saveConfig(config, configPath, global = false) {
12724
14335
  const result = CocoConfigSchema.safeParse(config);
@@ -12729,11 +14340,11 @@ async function saveConfig(config, configPath, global = false) {
12729
14340
  }));
12730
14341
  throw new ConfigError("Cannot save invalid configuration", {
12731
14342
  issues,
12732
- configPath: configPath || getProjectConfigPath()
14343
+ configPath: configPath || getProjectConfigPath2()
12733
14344
  });
12734
14345
  }
12735
- const resolvedPath = configPath || (global ? CONFIG_PATHS.config : getProjectConfigPath());
12736
- const dir = path14__default.dirname(resolvedPath);
14346
+ const resolvedPath = configPath || (global ? CONFIG_PATHS.config : getProjectConfigPath2());
14347
+ const dir = path15__default.dirname(resolvedPath);
12737
14348
  await fs14__default.mkdir(dir, { recursive: true });
12738
14349
  const content = JSON.stringify(result.data, null, 2);
12739
14350
  await fs14__default.writeFile(resolvedPath, content, "utf-8");
@@ -12752,7 +14363,7 @@ async function configExists(configPath, scope = "any") {
12752
14363
  }
12753
14364
  if (scope === "project" || scope === "any") {
12754
14365
  try {
12755
- await fs14__default.access(getProjectConfigPath());
14366
+ await fs14__default.access(getProjectConfigPath2());
12756
14367
  return true;
12757
14368
  } catch {
12758
14369
  if (scope === "project") return false;
@@ -12797,7 +14408,7 @@ function hasNullByte(str) {
12797
14408
  }
12798
14409
  function normalizePath(filePath) {
12799
14410
  let normalized = filePath.replace(/\0/g, "");
12800
- normalized = path14__default.normalize(normalized);
14411
+ normalized = path15__default.normalize(normalized);
12801
14412
  return normalized;
12802
14413
  }
12803
14414
  function isPathAllowed(filePath, operation) {
@@ -12805,31 +14416,31 @@ function isPathAllowed(filePath, operation) {
12805
14416
  return { allowed: false, reason: "Path contains invalid characters" };
12806
14417
  }
12807
14418
  const normalized = normalizePath(filePath);
12808
- const absolute = path14__default.resolve(normalized);
14419
+ const absolute = path15__default.resolve(normalized);
12809
14420
  const cwd = process.cwd();
12810
14421
  for (const blocked of BLOCKED_PATHS) {
12811
- const normalizedBlocked = path14__default.normalize(blocked);
12812
- if (absolute === normalizedBlocked || absolute.startsWith(normalizedBlocked + path14__default.sep)) {
14422
+ const normalizedBlocked = path15__default.normalize(blocked);
14423
+ if (absolute === normalizedBlocked || absolute.startsWith(normalizedBlocked + path15__default.sep)) {
12813
14424
  return { allowed: false, reason: `Access to system path '${blocked}' is not allowed` };
12814
14425
  }
12815
14426
  }
12816
14427
  const home = process.env.HOME;
12817
14428
  if (home) {
12818
- const normalizedHome = path14__default.normalize(home);
12819
- const normalizedCwd = path14__default.normalize(cwd);
14429
+ const normalizedHome = path15__default.normalize(home);
14430
+ const normalizedCwd = path15__default.normalize(cwd);
12820
14431
  if (absolute.startsWith(normalizedHome) && !absolute.startsWith(normalizedCwd)) {
12821
14432
  if (isWithinAllowedPath(absolute, operation)) ; else if (operation === "read") {
12822
14433
  const allowedHomeReads = [".gitconfig", ".zshrc", ".bashrc"];
12823
- const basename4 = path14__default.basename(absolute);
12824
- if (!allowedHomeReads.includes(basename4)) {
12825
- const targetDir = path14__default.dirname(absolute);
14434
+ const basename5 = path15__default.basename(absolute);
14435
+ if (!allowedHomeReads.includes(basename5)) {
14436
+ const targetDir = path15__default.dirname(absolute);
12826
14437
  return {
12827
14438
  allowed: false,
12828
14439
  reason: `Reading files outside project directory is not allowed. Use /allow-path ${targetDir} to grant access.`
12829
14440
  };
12830
14441
  }
12831
14442
  } else {
12832
- const targetDir = path14__default.dirname(absolute);
14443
+ const targetDir = path15__default.dirname(absolute);
12833
14444
  return {
12834
14445
  allowed: false,
12835
14446
  reason: `${operation} operations outside project directory are not allowed. Use /allow-path ${targetDir} to grant access.`
@@ -12838,12 +14449,12 @@ function isPathAllowed(filePath, operation) {
12838
14449
  }
12839
14450
  }
12840
14451
  if (operation === "write" || operation === "delete") {
12841
- const basename4 = path14__default.basename(absolute);
14452
+ const basename5 = path15__default.basename(absolute);
12842
14453
  for (const pattern of SENSITIVE_PATTERNS) {
12843
- if (pattern.test(basename4)) {
14454
+ if (pattern.test(basename5)) {
12844
14455
  return {
12845
14456
  allowed: false,
12846
- reason: `Operation on sensitive file '${basename4}' requires explicit confirmation`
14457
+ reason: `Operation on sensitive file '${basename5}' requires explicit confirmation`
12847
14458
  };
12848
14459
  }
12849
14460
  }
@@ -12874,7 +14485,7 @@ Examples:
12874
14485
  async execute({ path: filePath, encoding, maxSize }) {
12875
14486
  validatePath(filePath, "read");
12876
14487
  try {
12877
- const absolutePath = path14__default.resolve(filePath);
14488
+ const absolutePath = path15__default.resolve(filePath);
12878
14489
  const stats = await fs14__default.stat(absolutePath);
12879
14490
  const maxBytes = maxSize ?? DEFAULT_MAX_FILE_SIZE;
12880
14491
  let truncated = false;
@@ -12925,7 +14536,7 @@ Examples:
12925
14536
  async execute({ path: filePath, content, createDirs, dryRun }) {
12926
14537
  validatePath(filePath, "write");
12927
14538
  try {
12928
- const absolutePath = path14__default.resolve(filePath);
14539
+ const absolutePath = path15__default.resolve(filePath);
12929
14540
  let wouldCreate = false;
12930
14541
  try {
12931
14542
  await fs14__default.access(absolutePath);
@@ -12941,7 +14552,7 @@ Examples:
12941
14552
  };
12942
14553
  }
12943
14554
  if (createDirs) {
12944
- await fs14__default.mkdir(path14__default.dirname(absolutePath), { recursive: true });
14555
+ await fs14__default.mkdir(path15__default.dirname(absolutePath), { recursive: true });
12945
14556
  }
12946
14557
  await fs14__default.writeFile(absolutePath, content, "utf-8");
12947
14558
  const stats = await fs14__default.stat(absolutePath);
@@ -12979,7 +14590,7 @@ Examples:
12979
14590
  async execute({ path: filePath, oldText, newText, all, dryRun }) {
12980
14591
  validatePath(filePath, "write");
12981
14592
  try {
12982
- const absolutePath = path14__default.resolve(filePath);
14593
+ const absolutePath = path15__default.resolve(filePath);
12983
14594
  let content = await fs14__default.readFile(absolutePath, "utf-8");
12984
14595
  let replacements = 0;
12985
14596
  if (all) {
@@ -13068,7 +14679,7 @@ Examples:
13068
14679
  }),
13069
14680
  async execute({ path: filePath }) {
13070
14681
  try {
13071
- const absolutePath = path14__default.resolve(filePath);
14682
+ const absolutePath = path15__default.resolve(filePath);
13072
14683
  const stats = await fs14__default.stat(absolutePath);
13073
14684
  return {
13074
14685
  exists: true,
@@ -13099,12 +14710,12 @@ Examples:
13099
14710
  }),
13100
14711
  async execute({ path: dirPath, recursive }) {
13101
14712
  try {
13102
- const absolutePath = path14__default.resolve(dirPath);
14713
+ const absolutePath = path15__default.resolve(dirPath);
13103
14714
  const entries = [];
13104
14715
  async function listDir(dir, prefix = "") {
13105
14716
  const items = await fs14__default.readdir(dir, { withFileTypes: true });
13106
14717
  for (const item of items) {
13107
- const fullPath = path14__default.join(dir, item.name);
14718
+ const fullPath = path15__default.join(dir, item.name);
13108
14719
  const relativePath = prefix ? `${prefix}/${item.name}` : item.name;
13109
14720
  if (item.isDirectory()) {
13110
14721
  entries.push({ name: relativePath, type: "directory" });
@@ -13151,7 +14762,7 @@ Examples:
13151
14762
  }
13152
14763
  validatePath(filePath, "delete");
13153
14764
  try {
13154
- const absolutePath = path14__default.resolve(filePath);
14765
+ const absolutePath = path15__default.resolve(filePath);
13155
14766
  const stats = await fs14__default.stat(absolutePath);
13156
14767
  if (stats.isDirectory()) {
13157
14768
  if (!recursive) {
@@ -13167,7 +14778,7 @@ Examples:
13167
14778
  } catch (error) {
13168
14779
  if (error instanceof ToolError) throw error;
13169
14780
  if (error.code === "ENOENT") {
13170
- return { deleted: false, path: path14__default.resolve(filePath) };
14781
+ return { deleted: false, path: path15__default.resolve(filePath) };
13171
14782
  }
13172
14783
  throw new FileSystemError(`Failed to delete: ${filePath}`, {
13173
14784
  path: filePath,
@@ -13195,8 +14806,8 @@ Examples:
13195
14806
  validatePath(source, "read");
13196
14807
  validatePath(destination, "write");
13197
14808
  try {
13198
- const srcPath = path14__default.resolve(source);
13199
- const destPath = path14__default.resolve(destination);
14809
+ const srcPath = path15__default.resolve(source);
14810
+ const destPath = path15__default.resolve(destination);
13200
14811
  if (!overwrite) {
13201
14812
  try {
13202
14813
  await fs14__default.access(destPath);
@@ -13212,7 +14823,7 @@ Examples:
13212
14823
  }
13213
14824
  }
13214
14825
  }
13215
- await fs14__default.mkdir(path14__default.dirname(destPath), { recursive: true });
14826
+ await fs14__default.mkdir(path15__default.dirname(destPath), { recursive: true });
13216
14827
  await fs14__default.copyFile(srcPath, destPath);
13217
14828
  const stats = await fs14__default.stat(destPath);
13218
14829
  return {
@@ -13248,8 +14859,8 @@ Examples:
13248
14859
  validatePath(source, "delete");
13249
14860
  validatePath(destination, "write");
13250
14861
  try {
13251
- const srcPath = path14__default.resolve(source);
13252
- const destPath = path14__default.resolve(destination);
14862
+ const srcPath = path15__default.resolve(source);
14863
+ const destPath = path15__default.resolve(destination);
13253
14864
  if (!overwrite) {
13254
14865
  try {
13255
14866
  await fs14__default.access(destPath);
@@ -13265,7 +14876,7 @@ Examples:
13265
14876
  }
13266
14877
  }
13267
14878
  }
13268
- await fs14__default.mkdir(path14__default.dirname(destPath), { recursive: true });
14879
+ await fs14__default.mkdir(path15__default.dirname(destPath), { recursive: true });
13269
14880
  await fs14__default.rename(srcPath, destPath);
13270
14881
  return {
13271
14882
  source: srcPath,
@@ -13300,10 +14911,10 @@ Examples:
13300
14911
  }),
13301
14912
  async execute({ path: dirPath, depth, showHidden, dirsOnly }) {
13302
14913
  try {
13303
- const absolutePath = path14__default.resolve(dirPath ?? ".");
14914
+ const absolutePath = path15__default.resolve(dirPath ?? ".");
13304
14915
  let totalFiles = 0;
13305
14916
  let totalDirs = 0;
13306
- const lines = [path14__default.basename(absolutePath) + "/"];
14917
+ const lines = [path15__default.basename(absolutePath) + "/"];
13307
14918
  async function buildTree(dir, prefix, currentDepth) {
13308
14919
  if (currentDepth > (depth ?? 4)) return;
13309
14920
  let items = await fs14__default.readdir(dir, { withFileTypes: true });
@@ -13326,7 +14937,7 @@ Examples:
13326
14937
  if (item.isDirectory()) {
13327
14938
  totalDirs++;
13328
14939
  lines.push(`${prefix}${connector}${item.name}/`);
13329
- await buildTree(path14__default.join(dir, item.name), prefix + childPrefix, currentDepth + 1);
14940
+ await buildTree(path15__default.join(dir, item.name), prefix + childPrefix, currentDepth + 1);
13330
14941
  } else {
13331
14942
  totalFiles++;
13332
14943
  lines.push(`${prefix}${connector}${item.name}`);
@@ -14287,7 +15898,7 @@ var checkAgentCapabilityTool = defineTool({
14287
15898
  var simpleAgentTools = [spawnSimpleAgentTool, checkAgentCapabilityTool];
14288
15899
  async function detectTestFramework2(cwd) {
14289
15900
  try {
14290
- const pkgPath = path14__default.join(cwd, "package.json");
15901
+ const pkgPath = path15__default.join(cwd, "package.json");
14291
15902
  const pkgContent = await fs14__default.readFile(pkgPath, "utf-8");
14292
15903
  const pkg = JSON.parse(pkgContent);
14293
15904
  const deps = {
@@ -14464,9 +16075,9 @@ Examples:
14464
16075
  const projectDir = cwd ?? process.cwd();
14465
16076
  try {
14466
16077
  const coverageLocations = [
14467
- path14__default.join(projectDir, "coverage", "coverage-summary.json"),
14468
- path14__default.join(projectDir, "coverage", "coverage-final.json"),
14469
- path14__default.join(projectDir, ".nyc_output", "coverage-summary.json")
16078
+ path15__default.join(projectDir, "coverage", "coverage-summary.json"),
16079
+ path15__default.join(projectDir, "coverage", "coverage-final.json"),
16080
+ path15__default.join(projectDir, ".nyc_output", "coverage-summary.json")
14470
16081
  ];
14471
16082
  for (const location of coverageLocations) {
14472
16083
  try {
@@ -14522,7 +16133,7 @@ Examples:
14522
16133
  var testTools = [runTestsTool, getCoverageTool, runTestFileTool];
14523
16134
  async function detectLinter2(cwd) {
14524
16135
  try {
14525
- const pkgPath = path14__default.join(cwd, "package.json");
16136
+ const pkgPath = path15__default.join(cwd, "package.json");
14526
16137
  const pkgContent = await fs14__default.readFile(pkgPath, "utf-8");
14527
16138
  const pkg = JSON.parse(pkgContent);
14528
16139
  const deps = {
@@ -14710,8 +16321,8 @@ Examples:
14710
16321
  }
14711
16322
  });
14712
16323
  async function findSourceFiles(cwd) {
14713
- const { glob: glob15 } = await import('glob');
14714
- return glob15("src/**/*.{ts,js,tsx,jsx}", {
16324
+ const { glob: glob17 } = await import('glob');
16325
+ return glob17("src/**/*.{ts,js,tsx,jsx}", {
14715
16326
  cwd,
14716
16327
  absolute: true,
14717
16328
  ignore: ["**/*.test.*", "**/*.spec.*", "**/node_modules/**"]
@@ -14785,7 +16396,7 @@ Examples:
14785
16396
  async execute({ cwd, files, useSnyk }) {
14786
16397
  const projectDir = cwd ?? process.cwd();
14787
16398
  try {
14788
- const evaluator = createQualityEvaluator(projectDir, useSnyk);
16399
+ const evaluator = createQualityEvaluatorWithRegistry(projectDir, useSnyk);
14789
16400
  const evaluation = await evaluator.evaluate(files);
14790
16401
  return evaluation.scores;
14791
16402
  } catch (error) {
@@ -14828,7 +16439,7 @@ Examples:
14828
16439
  caseSensitive,
14829
16440
  wholeWord
14830
16441
  }) {
14831
- const targetPath = searchPath ? path14__default.resolve(searchPath) : process.cwd();
16442
+ const targetPath = searchPath ? path15__default.resolve(searchPath) : process.cwd();
14832
16443
  const matches = [];
14833
16444
  let filesSearched = 0;
14834
16445
  const filesWithMatches = /* @__PURE__ */ new Set();
@@ -14895,7 +16506,7 @@ Examples:
14895
16506
  contextAfter.push(lines[j] ?? "");
14896
16507
  }
14897
16508
  matches.push({
14898
- file: path14__default.relative(process.cwd(), file),
16509
+ file: path15__default.relative(process.cwd(), file),
14899
16510
  line: i + 1,
14900
16511
  column: match.index + 1,
14901
16512
  content: line,
@@ -14946,7 +16557,7 @@ Examples:
14946
16557
  }),
14947
16558
  async execute({ file, pattern, caseSensitive }) {
14948
16559
  try {
14949
- const absolutePath = path14__default.resolve(file);
16560
+ const absolutePath = path15__default.resolve(file);
14950
16561
  const content = await fs14__default.readFile(absolutePath, "utf-8");
14951
16562
  const lines = content.split("\n");
14952
16563
  const matches = [];
@@ -15123,7 +16734,7 @@ async function detectPackageManager(cwd) {
15123
16734
  ];
15124
16735
  for (const { file, pm } of lockfiles) {
15125
16736
  try {
15126
- await fs14__default.access(path14__default.join(cwd, file));
16737
+ await fs14__default.access(path15__default.join(cwd, file));
15127
16738
  return pm;
15128
16739
  } catch {
15129
16740
  }
@@ -15388,7 +16999,7 @@ ${message}
15388
16999
  });
15389
17000
  try {
15390
17001
  try {
15391
- await fs14__default.access(path14__default.join(projectDir, "Makefile"));
17002
+ await fs14__default.access(path15__default.join(projectDir, "Makefile"));
15392
17003
  } catch {
15393
17004
  throw new ToolError("No Makefile found in directory", { tool: "make" });
15394
17005
  }
@@ -15560,7 +17171,32 @@ CONFIG_PATHS.home;
15560
17171
 
15561
17172
  // src/cli/repl/session.ts
15562
17173
  init_paths();
15563
- path14__default.dirname(CONFIG_PATHS.trustedTools);
17174
+ z.object({
17175
+ name: z.string().min(1).max(64),
17176
+ description: z.string().min(1).max(1024),
17177
+ version: z.string().default("1.0.0"),
17178
+ license: z.string().optional(),
17179
+ globs: z.union([z.string(), z.array(z.string())]).optional(),
17180
+ // skills.sh standard fields
17181
+ "disable-model-invocation": z.boolean().optional(),
17182
+ "allowed-tools": z.union([z.string(), z.array(z.string())]).optional(),
17183
+ "argument-hint": z.string().optional(),
17184
+ compatibility: z.string().max(500).optional(),
17185
+ model: z.string().optional(),
17186
+ context: z.enum(["fork", "agent", "inline"]).optional(),
17187
+ // Top-level tags/author (skills.sh style) — also accepted inside metadata
17188
+ tags: z.union([z.string(), z.array(z.string())]).optional(),
17189
+ author: z.string().optional(),
17190
+ // Nested metadata (Coco style)
17191
+ metadata: z.object({
17192
+ author: z.string().optional(),
17193
+ tags: z.array(z.string()).optional(),
17194
+ category: z.string().optional()
17195
+ }).passthrough().optional()
17196
+ });
17197
+
17198
+ // src/cli/repl/session.ts
17199
+ path15__default.dirname(CONFIG_PATHS.trustedTools);
15564
17200
  CONFIG_PATHS.trustedTools;
15565
17201
 
15566
17202
  // src/cli/repl/recommended-permissions.ts
@@ -15862,7 +17498,7 @@ async function enforceRateLimit() {
15862
17498
  const now = Date.now();
15863
17499
  const elapsed = now - lastRequestTime;
15864
17500
  if (elapsed < MIN_REQUEST_INTERVAL_MS) {
15865
- await new Promise((resolve2) => setTimeout(resolve2, MIN_REQUEST_INTERVAL_MS - elapsed));
17501
+ await new Promise((resolve3) => setTimeout(resolve3, MIN_REQUEST_INTERVAL_MS - elapsed));
15866
17502
  }
15867
17503
  lastRequestTime = Date.now();
15868
17504
  }
@@ -16669,7 +18305,7 @@ var getTerminalWidth = () => process.stdout.columns || 80;
16669
18305
  function stripAnsi(str) {
16670
18306
  return str.replace(/\x1b\[[0-9;]*m/g, "");
16671
18307
  }
16672
- function detectLanguage(filePath) {
18308
+ function detectLanguage2(filePath) {
16673
18309
  const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
16674
18310
  const extMap = {
16675
18311
  ts: "typescript",
@@ -16714,7 +18350,7 @@ function renderDiff(diff, options) {
16714
18350
  }
16715
18351
  function renderFileBlock(file, opts) {
16716
18352
  const { maxWidth, showLineNumbers, compact } = opts;
16717
- const lang = detectLanguage(file.path);
18353
+ const lang = detectLanguage2(file.path);
16718
18354
  const contentWidth = Math.max(1, maxWidth - 4);
16719
18355
  const typeLabel = file.type === "modified" ? "modified" : file.type === "added" ? "new file" : file.type === "deleted" ? "deleted" : `renamed from ${file.oldPath}`;
16720
18356
  const statsLabel = ` +${file.additions} -${file.deletions}`;
@@ -16862,9 +18498,9 @@ async function fileExists(filePath) {
16862
18498
  return false;
16863
18499
  }
16864
18500
  }
16865
- async function fileExists2(path37) {
18501
+ async function fileExists2(path38) {
16866
18502
  try {
16867
- await access(path37);
18503
+ await access(path38);
16868
18504
  return true;
16869
18505
  } catch {
16870
18506
  return false;
@@ -17119,7 +18755,7 @@ async function checkTestCoverage(diff, cwd) {
17119
18755
  );
17120
18756
  if (!hasTestChange) {
17121
18757
  const ext = src.path.match(/\.(ts|tsx|js|jsx)$/)?.[0] ?? ".ts";
17122
- const testExists = await fileExists(path14__default.join(cwd, `${baseName}.test${ext}`)) || await fileExists(path14__default.join(cwd, `${baseName}.spec${ext}`));
18758
+ const testExists = await fileExists(path15__default.join(cwd, `${baseName}.test${ext}`)) || await fileExists(path15__default.join(cwd, `${baseName}.spec${ext}`));
17123
18759
  if (testExists) {
17124
18760
  if (src.additions >= TEST_COVERAGE_LARGE_CHANGE_THRESHOLD) {
17125
18761
  findings.push({
@@ -17338,8 +18974,8 @@ Examples:
17338
18974
  });
17339
18975
  var reviewTools = [reviewCodeTool];
17340
18976
  var fs23 = await import('fs/promises');
17341
- var path24 = await import('path');
17342
- var { glob: glob12 } = await import('glob');
18977
+ var path25 = await import('path');
18978
+ var { glob: glob14 } = await import('glob');
17343
18979
  var DEFAULT_MAX_FILES = 200;
17344
18980
  var LANGUAGE_EXTENSIONS = {
17345
18981
  typescript: [".ts", ".tsx", ".mts", ".cts"],
@@ -17363,8 +18999,8 @@ var DEFAULT_EXCLUDES = [
17363
18999
  "**/*.min.*",
17364
19000
  "**/*.d.ts"
17365
19001
  ];
17366
- function detectLanguage2(filePath) {
17367
- const ext = path24.extname(filePath).toLowerCase();
19002
+ function detectLanguage3(filePath) {
19003
+ const ext = path25.extname(filePath).toLowerCase();
17368
19004
  for (const [lang, extensions] of Object.entries(LANGUAGE_EXTENSIONS)) {
17369
19005
  if (extensions.includes(ext)) return lang;
17370
19006
  }
@@ -17773,7 +19409,7 @@ Examples:
17773
19409
  }),
17774
19410
  async execute({ path: rootPath, include, exclude, languages, maxFiles, depth }) {
17775
19411
  const startTime = performance.now();
17776
- const absPath = path24.resolve(rootPath);
19412
+ const absPath = path25.resolve(rootPath);
17777
19413
  try {
17778
19414
  const stat2 = await fs23.stat(absPath);
17779
19415
  if (!stat2.isDirectory()) {
@@ -17800,7 +19436,7 @@ Examples:
17800
19436
  pattern = `**/*{${allExts.join(",")}}`;
17801
19437
  }
17802
19438
  const excludePatterns = [...DEFAULT_EXCLUDES, ...exclude ?? []];
17803
- const files = await glob12(pattern, {
19439
+ const files = await glob14(pattern, {
17804
19440
  cwd: absPath,
17805
19441
  ignore: excludePatterns,
17806
19442
  nodir: true,
@@ -17812,8 +19448,8 @@ Examples:
17812
19448
  let totalDefinitions = 0;
17813
19449
  let exportedSymbols = 0;
17814
19450
  for (const file of limitedFiles) {
17815
- const fullPath = path24.join(absPath, file);
17816
- const language = detectLanguage2(file);
19451
+ const fullPath = path25.join(absPath, file);
19452
+ const language = detectLanguage3(file);
17817
19453
  if (!language) continue;
17818
19454
  if (languages && !languages.includes(language)) {
17819
19455
  continue;
@@ -17853,9 +19489,9 @@ Examples:
17853
19489
  var codebaseMapTools = [codebaseMapTool];
17854
19490
  init_paths();
17855
19491
  var fs24 = await import('fs/promises');
17856
- var path25 = await import('path');
19492
+ var path26 = await import('path');
17857
19493
  var crypto3 = await import('crypto');
17858
- var GLOBAL_MEMORIES_DIR = path25.join(COCO_HOME, "memories");
19494
+ var GLOBAL_MEMORIES_DIR = path26.join(COCO_HOME, "memories");
17859
19495
  var PROJECT_MEMORIES_DIR = ".coco/memories";
17860
19496
  var DEFAULT_MAX_MEMORIES = 1e3;
17861
19497
  async function ensureDir(dirPath) {
@@ -17866,7 +19502,7 @@ function getMemoriesDir(scope) {
17866
19502
  }
17867
19503
  async function loadIndex(scope) {
17868
19504
  const dir = getMemoriesDir(scope);
17869
- const indexPath = path25.join(dir, "index.json");
19505
+ const indexPath = path26.join(dir, "index.json");
17870
19506
  try {
17871
19507
  const content = await fs24.readFile(indexPath, "utf-8");
17872
19508
  return JSON.parse(content);
@@ -17877,12 +19513,12 @@ async function loadIndex(scope) {
17877
19513
  async function saveIndex(scope, index) {
17878
19514
  const dir = getMemoriesDir(scope);
17879
19515
  await ensureDir(dir);
17880
- const indexPath = path25.join(dir, "index.json");
19516
+ const indexPath = path26.join(dir, "index.json");
17881
19517
  await fs24.writeFile(indexPath, JSON.stringify(index, null, 2), "utf-8");
17882
19518
  }
17883
19519
  async function loadMemory(scope, id) {
17884
19520
  const dir = getMemoriesDir(scope);
17885
- const memPath = path25.join(dir, `${id}.json`);
19521
+ const memPath = path26.join(dir, `${id}.json`);
17886
19522
  try {
17887
19523
  const content = await fs24.readFile(memPath, "utf-8");
17888
19524
  return JSON.parse(content);
@@ -17893,7 +19529,7 @@ async function loadMemory(scope, id) {
17893
19529
  async function saveMemory(scope, memory) {
17894
19530
  const dir = getMemoriesDir(scope);
17895
19531
  await ensureDir(dir);
17896
- const memPath = path25.join(dir, `${memory.id}.json`);
19532
+ const memPath = path26.join(dir, `${memory.id}.json`);
17897
19533
  await fs24.writeFile(memPath, JSON.stringify(memory, null, 2), "utf-8");
17898
19534
  }
17899
19535
  var createMemoryTool = defineTool({
@@ -18225,8 +19861,8 @@ Examples:
18225
19861
  });
18226
19862
  var checkpointTools = [createCheckpointTool, restoreCheckpointTool, listCheckpointsTool];
18227
19863
  var fs26 = await import('fs/promises');
18228
- var path26 = await import('path');
18229
- var { glob: glob13 } = await import('glob');
19864
+ var path27 = await import('path');
19865
+ var { glob: glob15 } = await import('glob');
18230
19866
  var INDEX_DIR = ".coco/search-index";
18231
19867
  var DEFAULT_CHUNK_SIZE = 20;
18232
19868
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -18351,7 +19987,7 @@ async function getEmbedding(text) {
18351
19987
  }
18352
19988
  async function loadIndex2(indexDir) {
18353
19989
  try {
18354
- const indexPath = path26.join(indexDir, "index.json");
19990
+ const indexPath = path27.join(indexDir, "index.json");
18355
19991
  const content = await fs26.readFile(indexPath, "utf-8");
18356
19992
  return JSON.parse(content);
18357
19993
  } catch {
@@ -18360,11 +19996,11 @@ async function loadIndex2(indexDir) {
18360
19996
  }
18361
19997
  async function saveIndex2(indexDir, index) {
18362
19998
  await fs26.mkdir(indexDir, { recursive: true });
18363
- const indexPath = path26.join(indexDir, "index.json");
19999
+ const indexPath = path27.join(indexDir, "index.json");
18364
20000
  await fs26.writeFile(indexPath, JSON.stringify(index), "utf-8");
18365
20001
  }
18366
20002
  function isBinary(filePath) {
18367
- return BINARY_EXTENSIONS.has(path26.extname(filePath).toLowerCase());
20003
+ return BINARY_EXTENSIONS.has(path27.extname(filePath).toLowerCase());
18368
20004
  }
18369
20005
  var semanticSearchTool = defineTool({
18370
20006
  name: "semantic_search",
@@ -18389,12 +20025,12 @@ Examples:
18389
20025
  const effectivePath = rootPath ?? ".";
18390
20026
  const effectiveMaxResults = maxResults ?? 10;
18391
20027
  const effectiveThreshold = threshold ?? 0.3;
18392
- const absPath = path26.resolve(effectivePath);
18393
- const indexDir = path26.join(absPath, INDEX_DIR);
20028
+ const absPath = path27.resolve(effectivePath);
20029
+ const indexDir = path27.join(absPath, INDEX_DIR);
18394
20030
  let index = reindex ? null : await loadIndex2(indexDir);
18395
20031
  if (!index) {
18396
20032
  const pattern = include ?? "**/*";
18397
- const files = await glob13(pattern, {
20033
+ const files = await glob15(pattern, {
18398
20034
  cwd: absPath,
18399
20035
  ignore: DEFAULT_EXCLUDES2,
18400
20036
  nodir: true,
@@ -18403,7 +20039,7 @@ Examples:
18403
20039
  const chunks = [];
18404
20040
  for (const file of files) {
18405
20041
  if (isBinary(file)) continue;
18406
- const fullPath = path26.join(absPath, file);
20042
+ const fullPath = path27.join(absPath, file);
18407
20043
  try {
18408
20044
  const stat2 = await fs26.stat(fullPath);
18409
20045
  const content = await fs26.readFile(fullPath, "utf-8");
@@ -18466,11 +20102,11 @@ Examples:
18466
20102
  });
18467
20103
  var semanticSearchTools = [semanticSearchTool];
18468
20104
  var fs27 = await import('fs/promises');
18469
- var path27 = await import('path');
18470
- var { glob: glob14 } = await import('glob');
20105
+ var path28 = await import('path');
20106
+ var { glob: glob16 } = await import('glob');
18471
20107
  async function parseClassRelationships(rootPath, include) {
18472
20108
  const pattern = include ?? "**/*.{ts,tsx,js,jsx}";
18473
- const files = await glob14(pattern, {
20109
+ const files = await glob16(pattern, {
18474
20110
  cwd: rootPath,
18475
20111
  ignore: ["**/node_modules/**", "**/dist/**", "**/*.test.*", "**/*.d.ts"],
18476
20112
  nodir: true
@@ -18479,7 +20115,7 @@ async function parseClassRelationships(rootPath, include) {
18479
20115
  const interfaces = [];
18480
20116
  for (const file of files.slice(0, 100)) {
18481
20117
  try {
18482
- const content = await fs27.readFile(path27.join(rootPath, file), "utf-8");
20118
+ const content = await fs27.readFile(path28.join(rootPath, file), "utf-8");
18483
20119
  const lines = content.split("\n");
18484
20120
  for (let i = 0; i < lines.length; i++) {
18485
20121
  const line = lines[i];
@@ -18605,7 +20241,7 @@ async function generateArchitectureDiagram(rootPath) {
18605
20241
  const lines = ["graph TD"];
18606
20242
  let nodeCount = 0;
18607
20243
  let edgeCount = 0;
18608
- const rootName = path27.basename(rootPath);
20244
+ const rootName = path28.basename(rootPath);
18609
20245
  lines.push(` ROOT["${rootName}"]`);
18610
20246
  nodeCount++;
18611
20247
  for (const dir of dirs) {
@@ -18615,7 +20251,7 @@ async function generateArchitectureDiagram(rootPath) {
18615
20251
  nodeCount++;
18616
20252
  edgeCount++;
18617
20253
  try {
18618
- const subEntries = await fs27.readdir(path27.join(rootPath, dir.name), {
20254
+ const subEntries = await fs27.readdir(path28.join(rootPath, dir.name), {
18619
20255
  withFileTypes: true
18620
20256
  });
18621
20257
  const subDirs = subEntries.filter(
@@ -18738,7 +20374,7 @@ Examples:
18738
20374
  tool: "generate_diagram"
18739
20375
  });
18740
20376
  }
18741
- const absPath = rootPath ? path27.resolve(rootPath) : process.cwd();
20377
+ const absPath = rootPath ? path28.resolve(rootPath) : process.cwd();
18742
20378
  switch (type) {
18743
20379
  case "class":
18744
20380
  return generateClassDiagram(absPath, include);
@@ -18800,7 +20436,7 @@ Examples:
18800
20436
  });
18801
20437
  var diagramTools = [generateDiagramTool];
18802
20438
  var fs28 = await import('fs/promises');
18803
- var path28 = await import('path');
20439
+ var path29 = await import('path');
18804
20440
  var DEFAULT_MAX_PAGES = 20;
18805
20441
  var MAX_FILE_SIZE = 50 * 1024 * 1024;
18806
20442
  function parsePageRange(rangeStr, totalPages) {
@@ -18835,7 +20471,7 @@ Examples:
18835
20471
  }),
18836
20472
  async execute({ path: filePath, pages, maxPages }) {
18837
20473
  const startTime = performance.now();
18838
- const absPath = path28.resolve(filePath);
20474
+ const absPath = path29.resolve(filePath);
18839
20475
  try {
18840
20476
  const stat2 = await fs28.stat(absPath);
18841
20477
  if (!stat2.isFile()) {
@@ -18915,7 +20551,7 @@ Examples:
18915
20551
  });
18916
20552
  var pdfTools = [readPdfTool];
18917
20553
  var fs29 = await import('fs/promises');
18918
- var path29 = await import('path');
20554
+ var path30 = await import('path');
18919
20555
  var SUPPORTED_FORMATS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"]);
18920
20556
  var MAX_IMAGE_SIZE = 20 * 1024 * 1024;
18921
20557
  var MIME_TYPES = {
@@ -18943,15 +20579,15 @@ Examples:
18943
20579
  async execute({ path: filePath, prompt, provider }) {
18944
20580
  const startTime = performance.now();
18945
20581
  const effectivePrompt = prompt ?? "Describe this image in detail. If it's code or a UI, identify the key elements.";
18946
- const absPath = path29.resolve(filePath);
20582
+ const absPath = path30.resolve(filePath);
18947
20583
  const cwd = process.cwd();
18948
- if (!absPath.startsWith(cwd + path29.sep) && absPath !== cwd) {
20584
+ if (!absPath.startsWith(cwd + path30.sep) && absPath !== cwd) {
18949
20585
  throw new ToolError(
18950
20586
  `Path traversal denied: '${filePath}' resolves outside the project directory`,
18951
20587
  { tool: "read_image" }
18952
20588
  );
18953
20589
  }
18954
- const ext = path29.extname(absPath).toLowerCase();
20590
+ const ext = path30.extname(absPath).toLowerCase();
18955
20591
  if (!SUPPORTED_FORMATS.has(ext)) {
18956
20592
  throw new ToolError(
18957
20593
  `Unsupported image format '${ext}'. Supported: ${Array.from(SUPPORTED_FORMATS).join(", ")}`,
@@ -19093,7 +20729,7 @@ Examples:
19093
20729
  }
19094
20730
  });
19095
20731
  var imageTools = [readImageTool];
19096
- var path30 = await import('path');
20732
+ var path31 = await import('path');
19097
20733
  var DANGEROUS_PATTERNS2 = [
19098
20734
  /\bDROP\s+(?:TABLE|DATABASE|INDEX|VIEW)\b/i,
19099
20735
  /\bTRUNCATE\b/i,
@@ -19124,7 +20760,7 @@ Examples:
19124
20760
  async execute({ database, query, params, readonly: isReadonlyParam }) {
19125
20761
  const isReadonly = isReadonlyParam ?? true;
19126
20762
  const startTime = performance.now();
19127
- const absPath = path30.resolve(database);
20763
+ const absPath = path31.resolve(database);
19128
20764
  if (isReadonly && isDangerousSql(query)) {
19129
20765
  throw new ToolError(
19130
20766
  "Write operations (INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, CREATE) are blocked in readonly mode. Set readonly: false to allow writes.",
@@ -19197,7 +20833,7 @@ Examples:
19197
20833
  }),
19198
20834
  async execute({ database, table }) {
19199
20835
  const startTime = performance.now();
19200
- const absPath = path30.resolve(database);
20836
+ const absPath = path31.resolve(database);
19201
20837
  try {
19202
20838
  const { default: Database } = await import('better-sqlite3');
19203
20839
  const db = new Database(absPath, { readonly: true, fileMustExist: true });
@@ -19251,7 +20887,7 @@ Examples:
19251
20887
  });
19252
20888
  var databaseTools = [sqlQueryTool, inspectSchemaTool];
19253
20889
  var fs30 = await import('fs/promises');
19254
- var path31 = await import('path');
20890
+ var path32 = await import('path');
19255
20891
  var AnalyzeFileSchema = z.object({
19256
20892
  filePath: z.string().describe("Path to file to analyze"),
19257
20893
  includeAst: z.boolean().default(false).describe("Include AST in result")
@@ -19348,8 +20984,8 @@ async function analyzeFile(filePath, includeAst = false) {
19348
20984
  };
19349
20985
  }
19350
20986
  async function analyzeDirectory(dirPath) {
19351
- const { glob: glob15 } = await import('glob');
19352
- const files = await glob15("**/*.{ts,tsx,js,jsx}", {
20987
+ const { glob: glob17 } = await import('glob');
20988
+ const files = await glob17("**/*.{ts,tsx,js,jsx}", {
19353
20989
  cwd: dirPath,
19354
20990
  ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
19355
20991
  absolute: true
@@ -19361,10 +20997,10 @@ async function analyzeDirectory(dirPath) {
19361
20997
  try {
19362
20998
  const analysis = await analyzeFile(file, false);
19363
20999
  totalLines += analysis.lines;
19364
- const ext = path31.extname(file);
21000
+ const ext = path32.extname(file);
19365
21001
  filesByType[ext] = (filesByType[ext] || 0) + 1;
19366
21002
  fileStats.push({
19367
- file: path31.relative(dirPath, file),
21003
+ file: path32.relative(dirPath, file),
19368
21004
  lines: analysis.lines,
19369
21005
  complexity: analysis.complexity.cyclomatic
19370
21006
  });
@@ -19877,7 +21513,7 @@ var calculateCodeScoreTool = defineTool({
19877
21513
  });
19878
21514
  var smartSuggestionsTools = [suggestImprovementsTool, calculateCodeScoreTool];
19879
21515
  var fs32 = await import('fs/promises');
19880
- var path32 = await import('path');
21516
+ var path33 = await import('path');
19881
21517
  var ContextMemoryStore = class {
19882
21518
  items = /* @__PURE__ */ new Map();
19883
21519
  learnings = /* @__PURE__ */ new Map();
@@ -19897,7 +21533,7 @@ var ContextMemoryStore = class {
19897
21533
  }
19898
21534
  }
19899
21535
  async save() {
19900
- const dir = path32.dirname(this.storePath);
21536
+ const dir = path33.dirname(this.storePath);
19901
21537
  await fs32.mkdir(dir, { recursive: true });
19902
21538
  const data = {
19903
21539
  sessionId: this.sessionId,
@@ -20071,7 +21707,7 @@ var contextEnhancerTools = [
20071
21707
  getLearnedPatternsTool
20072
21708
  ];
20073
21709
  var fs33 = await import('fs/promises');
20074
- var path33 = await import('path');
21710
+ var path34 = await import('path');
20075
21711
  async function discoverSkills(skillsDir) {
20076
21712
  try {
20077
21713
  const files = await fs33.readdir(skillsDir);
@@ -20087,7 +21723,7 @@ async function loadSkillMetadata(skillPath) {
20087
21723
  const descMatch = content.match(/@description\s+(.+)/);
20088
21724
  const versionMatch = content.match(/@version\s+(\S+)/);
20089
21725
  return {
20090
- name: nameMatch?.[1] || path33.basename(skillPath, path33.extname(skillPath)),
21726
+ name: nameMatch?.[1] || path34.basename(skillPath, path34.extname(skillPath)),
20091
21727
  description: descMatch?.[1] || "No description",
20092
21728
  version: versionMatch?.[1] || "1.0.0",
20093
21729
  dependencies: []
@@ -20131,7 +21767,7 @@ var discoverSkillsTool = defineTool({
20131
21767
  const { skillsDir } = input;
20132
21768
  const skills = await discoverSkills(skillsDir);
20133
21769
  const metadata = await Promise.all(
20134
- skills.map((s) => loadSkillMetadata(path33.join(skillsDir, s)))
21770
+ skills.map((s) => loadSkillMetadata(path34.join(skillsDir, s)))
20135
21771
  );
20136
21772
  return {
20137
21773
  skillsDir,
@@ -20622,16 +22258,18 @@ var DANGEROUS_ARG_PATTERNS = [
20622
22258
  /\beval\s+/,
20623
22259
  /\bcurl\s+.*\|\s*(ba)?sh/
20624
22260
  ];
20625
- function getSystemOpenCommand() {
20626
- return process.platform === "darwin" ? "open" : "xdg-open";
22261
+ function getSystemOpen(target) {
22262
+ if (process.platform === "darwin") return { cmd: "open", args: [target] };
22263
+ if (isWSL) return { cmd: "cmd.exe", args: ["/c", "start", "", target] };
22264
+ return { cmd: "xdg-open", args: [target] };
20627
22265
  }
20628
22266
  function hasNullByte2(str) {
20629
22267
  return str.includes("\0");
20630
22268
  }
20631
22269
  function isBlockedPath(absolute) {
20632
22270
  for (const blocked of BLOCKED_PATHS2) {
20633
- const normalizedBlocked = path14__default.normalize(blocked);
20634
- if (absolute === normalizedBlocked || absolute.startsWith(normalizedBlocked + path14__default.sep)) {
22271
+ const normalizedBlocked = path15__default.normalize(blocked);
22272
+ if (absolute === normalizedBlocked || absolute.startsWith(normalizedBlocked + path15__default.sep)) {
20635
22273
  return blocked;
20636
22274
  }
20637
22275
  }
@@ -20689,7 +22327,7 @@ Examples:
20689
22327
  throw new ToolError("Invalid file path", { tool: "open_file" });
20690
22328
  }
20691
22329
  const workDir = cwd ?? process.cwd();
20692
- const absolute = path14__default.isAbsolute(filePath) ? path14__default.normalize(filePath) : path14__default.resolve(workDir, filePath);
22330
+ const absolute = path15__default.isAbsolute(filePath) ? path15__default.normalize(filePath) : path15__default.resolve(workDir, filePath);
20693
22331
  const blockedBy = isBlockedPath(absolute);
20694
22332
  if (blockedBy) {
20695
22333
  throw new ToolError(`Access to system path '${blockedBy}' is not allowed`, {
@@ -20702,8 +22340,8 @@ Examples:
20702
22340
  throw new ToolError(`File not found: ${absolute}`, { tool: "open_file" });
20703
22341
  }
20704
22342
  if (mode === "open") {
20705
- const cmd = getSystemOpenCommand();
20706
- await execa(cmd, [absolute], { timeout: 1e4 });
22343
+ const { cmd, args: args2 } = getSystemOpen(absolute);
22344
+ await execa(cmd, args2, { timeout: 1e4 });
20707
22345
  return {
20708
22346
  action: "opened",
20709
22347
  path: absolute,
@@ -20712,14 +22350,14 @@ Examples:
20712
22350
  };
20713
22351
  }
20714
22352
  if (isBlockedExecFile(absolute)) {
20715
- throw new ToolError(`Execution of sensitive file is blocked: ${path14__default.basename(absolute)}`, {
22353
+ throw new ToolError(`Execution of sensitive file is blocked: ${path15__default.basename(absolute)}`, {
20716
22354
  tool: "open_file"
20717
22355
  });
20718
22356
  }
20719
22357
  if (args.length > 0 && hasDangerousArgs(args)) {
20720
22358
  throw new ToolError("Arguments contain dangerous patterns", { tool: "open_file" });
20721
22359
  }
20722
- const ext = path14__default.extname(absolute);
22360
+ const ext = path15__default.extname(absolute);
20723
22361
  const interpreter = getInterpreter(ext);
20724
22362
  const executable = await isExecutable(absolute);
20725
22363
  let command;
@@ -20732,7 +22370,7 @@ Examples:
20732
22370
  cmdArgs = [...args];
20733
22371
  } else {
20734
22372
  throw new ToolError(
20735
- `Cannot execute '${path14__default.basename(absolute)}': no known interpreter for '${ext || "(no extension)"}' and file is not executable`,
22373
+ `Cannot execute '${path15__default.basename(absolute)}': no known interpreter for '${ext || "(no extension)"}' and file is not executable`,
20736
22374
  { tool: "open_file" }
20737
22375
  );
20738
22376
  }
@@ -20784,7 +22422,7 @@ Examples:
20784
22422
  reason: z.string().optional().describe("Why access is needed (shown to user for context)")
20785
22423
  }),
20786
22424
  async execute({ path: dirPath, reason }) {
20787
- const absolute = path14__default.resolve(dirPath);
22425
+ const absolute = path15__default.resolve(dirPath);
20788
22426
  if (isWithinAllowedPath(absolute, "read")) {
20789
22427
  return {
20790
22428
  authorized: true,
@@ -20793,8 +22431,8 @@ Examples:
20793
22431
  };
20794
22432
  }
20795
22433
  for (const blocked of BLOCKED_SYSTEM_PATHS) {
20796
- const normalizedBlocked = path14__default.normalize(blocked);
20797
- if (absolute === normalizedBlocked || absolute.startsWith(normalizedBlocked + path14__default.sep)) {
22434
+ const normalizedBlocked = path15__default.normalize(blocked);
22435
+ if (absolute === normalizedBlocked || absolute.startsWith(normalizedBlocked + path15__default.sep)) {
20798
22436
  return {
20799
22437
  authorized: false,
20800
22438
  path: absolute,
@@ -20803,7 +22441,7 @@ Examples:
20803
22441
  }
20804
22442
  }
20805
22443
  const cwd = process.cwd();
20806
- if (absolute === path14__default.normalize(cwd) || absolute.startsWith(path14__default.normalize(cwd) + path14__default.sep)) {
22444
+ if (absolute === path15__default.normalize(cwd) || absolute.startsWith(path15__default.normalize(cwd) + path15__default.sep)) {
20807
22445
  return {
20808
22446
  authorized: true,
20809
22447
  path: absolute,
@@ -20827,7 +22465,7 @@ Examples:
20827
22465
  };
20828
22466
  }
20829
22467
  const existing = getAllowedPaths();
20830
- if (existing.some((e) => path14__default.normalize(e.path) === path14__default.normalize(absolute))) {
22468
+ if (existing.some((e) => path15__default.normalize(e.path) === path15__default.normalize(absolute))) {
20831
22469
  return {
20832
22470
  authorized: true,
20833
22471
  path: absolute,