trace-to-skill 0.1.26 → 0.1.35

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.
@@ -1,7 +1,10 @@
1
- import { promises as fs } from "node:fs";
1
+ import { constants as fsConstants, promises as fs } from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
4
+ import { TextDecoder } from "node:util";
3
5
  import { doctorRepo } from "./doctor.js";
4
6
  const SKIP_DIRS = new Set([".git", "node_modules", "dist", "coverage", ".next", "build"]);
7
+ const INSTRUCTION_SIZE_WARN_BYTES = 24000;
5
8
  export async function lintAgents(target = process.cwd()) {
6
9
  const root = path.resolve(target);
7
10
  const files = await listFiles(root);
@@ -15,6 +18,9 @@ export async function lintAgents(target = process.cwd()) {
15
18
  finding.kind === "secret_exposure" ||
16
19
  finding.kind === "hidden_unicode" ||
17
20
  finding.kind === "prompt_injection");
21
+ findings.push(...(await detectInstructionFileRisks(root, instructionFiles)));
22
+ findings.push(...(await detectMcpConfigDiagnostics(root, mcpConfigs)));
23
+ findings.push(...(await detectCodexConfigDiagnostics(root, mcpConfigs)));
18
24
  const score = calculateAgentsLintScore(checks, findings);
19
25
  const status = statusFrom(score, checks, findings);
20
26
  return {
@@ -148,9 +154,698 @@ function isInstructionFile(file) {
148
154
  }
149
155
  function isMcpConfigCandidate(file) {
150
156
  return /(^|\/)(mcp|\.mcp|mcp-config|model-context)\.(json|jsonc)$/i.test(file) ||
151
- /(^|\/)\.cursor\/mcp\.json$/i.test(file);
157
+ /(^|\/)\.cursor\/mcp\.json$/i.test(file) ||
158
+ /(^|\/)\.codex\/config\.toml$/i.test(file);
152
159
  }
153
160
  function isEvidenceArchive(file) {
154
161
  return /^(fixtures|examples|runs)\//i.test(file);
155
162
  }
163
+ async function detectInstructionFileRisks(root, instructionFiles) {
164
+ const findings = [];
165
+ const missingPathEvidence = [];
166
+ const largeFileEvidence = [];
167
+ const missingIncludeEvidence = [];
168
+ const nestedInstructionEvidence = [];
169
+ const encodingEvidence = [];
170
+ const instructionContents = new Map();
171
+ for (const file of instructionFiles) {
172
+ const absolute = path.join(root, file);
173
+ const raw = await fs.readFile(absolute);
174
+ const decoded = decodeInstructionFile(raw);
175
+ const content = decoded.content;
176
+ instructionContents.set(file, content);
177
+ const lines = content.split(/\r?\n/);
178
+ if (decoded.invalidUtf8) {
179
+ encodingEvidence.push({
180
+ file,
181
+ line: 1,
182
+ excerpt: `${file} contains bytes that are not valid UTF-8; Codex may skip or misread this instruction file.`
183
+ });
184
+ }
185
+ if (Buffer.byteLength(content, "utf8") > INSTRUCTION_SIZE_WARN_BYTES) {
186
+ largeFileEvidence.push({
187
+ file,
188
+ line: 1,
189
+ excerpt: `${file} is ${Buffer.byteLength(content, "utf8")} bytes; split long instructions into smaller scoped files or keep critical rules near the top.`
190
+ });
191
+ }
192
+ for (const reference of collectPathReferences(lines)) {
193
+ if (!(await pathExists(root, reference.value))) {
194
+ missingPathEvidence.push({
195
+ file,
196
+ line: reference.line,
197
+ excerpt: `referenced path does not exist: ${reference.value}`
198
+ });
199
+ }
200
+ }
201
+ for (const reference of collectIncludeReferences(lines)) {
202
+ if (reference.invalid) {
203
+ missingIncludeEvidence.push({
204
+ file,
205
+ line: reference.line,
206
+ excerpt: `include target is not a safe repo-relative markdown path: ${reference.value}`
207
+ });
208
+ }
209
+ else if (!(await pathExists(root, reference.value))) {
210
+ missingIncludeEvidence.push({
211
+ file,
212
+ line: reference.line,
213
+ excerpt: `include target does not exist: ${reference.value}`
214
+ });
215
+ }
216
+ }
217
+ }
218
+ const rootInstructions = instructionContents.get("AGENTS.md") ?? "";
219
+ if (rootInstructions) {
220
+ for (const file of instructionFiles) {
221
+ if (file !== "AGENTS.md" && /(^|\/)AGENTS\.md$/i.test(file) && !rootInstructions.includes(file) && !rootInstructions.includes(path.dirname(file))) {
222
+ nestedInstructionEvidence.push({
223
+ file,
224
+ line: 1,
225
+ excerpt: `nested AGENTS.md may not be loaded unless Codex starts in ${path.dirname(file)} or the root AGENTS.md explicitly points to it`
226
+ });
227
+ }
228
+ }
229
+ }
230
+ if (missingPathEvidence.length > 0) {
231
+ findings.push({
232
+ kind: "hallucinated_file",
233
+ severity: "medium",
234
+ title: "Agent instruction references missing paths",
235
+ why: "Codex and other agents lose time or follow stale guidance when AGENTS.md or tool instructions point at files that no longer exist.",
236
+ evidence: missingPathEvidence.slice(0, 8),
237
+ suggestedRule: "Keep paths in agent instruction files verified; run trace-to-skill lint-agents after moving or deleting referenced files."
238
+ });
239
+ }
240
+ if (missingIncludeEvidence.length > 0) {
241
+ findings.push({
242
+ kind: "hallucinated_file",
243
+ severity: "medium",
244
+ title: "Agent instruction include references missing paths",
245
+ why: "Teams often split AGENTS.md or CLAUDE.md into reusable markdown files. Missing or unsafe include targets make instruction assembly unverifiable and create cross-tool drift.",
246
+ evidence: missingIncludeEvidence.slice(0, 8),
247
+ suggestedRule: "Keep @include-style instruction references repo-relative, present, and auditable; run trace-to-skill lint-agents after moving shared instruction files."
248
+ });
249
+ }
250
+ if (largeFileEvidence.length > 0) {
251
+ findings.push({
252
+ kind: "ignored_instruction",
253
+ severity: "medium",
254
+ title: "Large agent instruction file may be truncated or ignored",
255
+ why: "Very large instruction files make it harder for agents to preserve high-priority rules and can hide important guidance near the end.",
256
+ evidence: largeFileEvidence,
257
+ suggestedRule: "Keep critical repository instructions short, put must-follow validation rules near the top, and split long reference material into linked docs."
258
+ });
259
+ }
260
+ if (nestedInstructionEvidence.length > 0) {
261
+ findings.push({
262
+ kind: "ignored_instruction",
263
+ severity: "medium",
264
+ title: "Nested AGENTS.md may not be loaded automatically",
265
+ why: "Codex users report that nested AGENTS.md files in monorepos are easy to miss unless the agent starts in the right directory or the root instructions explicitly mention them.",
266
+ evidence: nestedInstructionEvidence.slice(0, 8),
267
+ suggestedRule: "List nested AGENTS.md files in the root AGENTS.md or add explicit package-scope rules so maintainers can verify which instruction files should be loaded for each path."
268
+ });
269
+ }
270
+ if (encodingEvidence.length > 0) {
271
+ findings.push({
272
+ kind: "ignored_instruction",
273
+ severity: "medium",
274
+ title: "Agent instruction file may fail UTF-8 loading",
275
+ why: "Instruction files with invalid encoding can be silently skipped or misread by coding agents, making policy failures hard to diagnose.",
276
+ evidence: encodingEvidence.slice(0, 8),
277
+ suggestedRule: "Save AGENTS.md, CLAUDE.md, and other agent instruction files as valid UTF-8 and reject files with replacement characters or unsupported encodings."
278
+ });
279
+ }
280
+ return findings;
281
+ }
282
+ function decodeInstructionFile(raw) {
283
+ try {
284
+ return {
285
+ content: new TextDecoder("utf-8", { fatal: true }).decode(raw),
286
+ invalidUtf8: false
287
+ };
288
+ }
289
+ catch {
290
+ return {
291
+ content: raw.toString("utf8"),
292
+ invalidUtf8: true
293
+ };
294
+ }
295
+ }
296
+ function collectPathReferences(lines) {
297
+ const references = [];
298
+ const seen = new Set();
299
+ lines.forEach((line, index) => {
300
+ for (const match of line.matchAll(/`([^`\n]+)`/g)) {
301
+ addPathReference(references, seen, index + 1, match[1]);
302
+ }
303
+ for (const match of line.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
304
+ addPathReference(references, seen, index + 1, match[1]);
305
+ }
306
+ });
307
+ return references;
308
+ }
309
+ function addPathReference(references, seen, line, raw) {
310
+ const value = normalizePathReference(raw);
311
+ if (!value || seen.has(value)) {
312
+ return;
313
+ }
314
+ seen.add(value);
315
+ references.push({ line, value });
316
+ }
317
+ function collectIncludeReferences(lines) {
318
+ const references = [];
319
+ const seen = new Set();
320
+ lines.forEach((line, index) => {
321
+ for (const match of line.matchAll(/(?:^|\s)@([A-Za-z0-9._/-]+\.md)\b/g)) {
322
+ const raw = match[1];
323
+ const value = normalizeIncludeReference(raw);
324
+ const key = value ?? raw;
325
+ if (seen.has(key)) {
326
+ continue;
327
+ }
328
+ seen.add(key);
329
+ references.push({
330
+ line: index + 1,
331
+ value: key,
332
+ invalid: !value
333
+ });
334
+ }
335
+ });
336
+ return references;
337
+ }
338
+ function normalizeIncludeReference(raw) {
339
+ const value = raw.trim().replace(/^[.]\//, "").replace(/[),.;:]+$/, "");
340
+ if (!value ||
341
+ value.startsWith("/") ||
342
+ value.startsWith("~") ||
343
+ value.includes("..") ||
344
+ value.includes("$") ||
345
+ value.includes("*") ||
346
+ value.includes(" ")) {
347
+ return undefined;
348
+ }
349
+ return /^[A-Za-z0-9._/-]+\.md$/i.test(value) ? value : undefined;
350
+ }
351
+ function normalizePathReference(raw) {
352
+ const value = raw.trim().replace(/^file:\/\//, "").replace(/[),.;:]+$/, "");
353
+ if (!value ||
354
+ value.startsWith("http://") ||
355
+ value.startsWith("https://") ||
356
+ value.startsWith("mailto:") ||
357
+ value.startsWith("#") ||
358
+ value.includes("*") ||
359
+ value.includes("$") ||
360
+ value.includes(" ") ||
361
+ value.startsWith("@") ||
362
+ value.startsWith("~") ||
363
+ path.isAbsolute(value)) {
364
+ return undefined;
365
+ }
366
+ if (!/^[A-Za-z0-9._/@-]+(?:\/[A-Za-z0-9._@-]+)*$/.test(value)) {
367
+ return undefined;
368
+ }
369
+ const hasPathSignal = value.includes("/") || /\.[A-Za-z0-9]{1,8}$/.test(value);
370
+ return hasPathSignal ? value.replace(/^\.\//, "") : undefined;
371
+ }
372
+ async function pathExists(root, reference) {
373
+ try {
374
+ await fs.access(path.join(root, reference));
375
+ return true;
376
+ }
377
+ catch {
378
+ return false;
379
+ }
380
+ }
381
+ async function detectMcpConfigDiagnostics(root, mcpConfigs) {
382
+ const evidence = [];
383
+ for (const file of mcpConfigs) {
384
+ const absolute = path.join(root, file);
385
+ const content = await fs.readFile(absolute, "utf8");
386
+ const parsed = parseMcpConfig(file, content);
387
+ if (!parsed) {
388
+ evidence.push({
389
+ file,
390
+ line: 1,
391
+ excerpt: `MCP config could not be parsed as ${file.endsWith(".toml") ? "TOML" : "JSON/JSONC"}.`
392
+ });
393
+ continue;
394
+ }
395
+ if (isTomlConfig(file) && !asObject(parsed.mcp_servers)) {
396
+ continue;
397
+ }
398
+ if (!isTomlConfig(file) && asObject(parsed.mcp_servers) && !asObject(parsed.mcpServers)) {
399
+ evidence.push({
400
+ file,
401
+ line: findLine(content, "mcp_servers"),
402
+ excerpt: "uses mcp_servers; Codex plugin MCP configs commonly expect mcpServers or a direct top-level server map"
403
+ });
404
+ }
405
+ const servers = discoverMcpServers(parsed);
406
+ if (!servers) {
407
+ evidence.push({
408
+ file,
409
+ line: 1,
410
+ excerpt: "no MCP server map found; expected mcpServers, servers, or a direct top-level server map"
411
+ });
412
+ continue;
413
+ }
414
+ for (const [name, rawServer] of Object.entries(servers)) {
415
+ const server = asObject(rawServer);
416
+ if (!server) {
417
+ evidence.push({
418
+ file,
419
+ line: findLine(content, name),
420
+ excerpt: `server "${name}" is not an object`
421
+ });
422
+ continue;
423
+ }
424
+ if (isTomlConfig(file) && commandLooksLocal(server.command) && typeof server.cwd !== "string") {
425
+ evidence.push({
426
+ file,
427
+ line: findLine(content, name),
428
+ excerpt: `server "${name}" in project .codex/config.toml uses a local stdio command without explicit cwd`
429
+ });
430
+ }
431
+ evidence.push(...(await inspectMcpServer(root, file, content, name, server)));
432
+ }
433
+ }
434
+ if (evidence.length === 0) {
435
+ return [];
436
+ }
437
+ return [{
438
+ kind: "mcp_risk",
439
+ severity: "medium",
440
+ title: "MCP config has unresolved startup inputs",
441
+ why: "Codex MCP failures are hard to debug when config shape, command paths, cwd values, or required environment variables are invalid before the server even starts.",
442
+ evidence: evidence.slice(0, 10),
443
+ suggestedRule: "Before enabling an MCP server for coding agents, verify config key casing, command availability, cwd existence, and required environment variables with trace-to-skill lint-agents."
444
+ }];
445
+ }
446
+ async function detectCodexConfigDiagnostics(root, configFiles) {
447
+ const evidence = [];
448
+ for (const file of configFiles.filter((item) => isCodexTomlConfig(item))) {
449
+ const absolute = path.join(root, file);
450
+ const content = await fs.readFile(absolute, "utf8");
451
+ const lines = parseTomlLines(content);
452
+ const permissions = collectPermissionProfiles(lines);
453
+ const defaultPermissions = findTomlAssignment(lines, "default_permissions");
454
+ if (defaultPermissions && typeof defaultPermissions.value === "string" && !permissions.has(defaultPermissions.value)) {
455
+ evidence.push({
456
+ file,
457
+ line: defaultPermissions.line,
458
+ excerpt: `default_permissions references missing permissions profile: ${defaultPermissions.value}`
459
+ });
460
+ }
461
+ for (const item of lines) {
462
+ if (item.section === "features" && item.key === "codex_hooks") {
463
+ evidence.push({
464
+ file,
465
+ line: item.line,
466
+ excerpt: "deprecated [features].codex_hooks key is present; use [features].hooks instead"
467
+ });
468
+ }
469
+ if (item.section?.startsWith("projects.") && item.key === "trusted_level") {
470
+ evidence.push({
471
+ file,
472
+ line: item.line,
473
+ excerpt: "projects.* trusted_level is stored in config.toml; this can pollute synced dotfiles with machine-specific project metadata"
474
+ });
475
+ }
476
+ }
477
+ for (const section of collectSections(lines)) {
478
+ if (section.startsWith("projects.") && /\/|\\|:/.test(section)) {
479
+ evidence.push({
480
+ file,
481
+ line: findLine(content, section),
482
+ excerpt: `project-specific config section appears to contain a machine-local path: [${section}]`
483
+ });
484
+ }
485
+ }
486
+ }
487
+ if (evidence.length === 0) {
488
+ return [];
489
+ }
490
+ return [{
491
+ kind: "ignored_instruction",
492
+ severity: "medium",
493
+ title: "Codex config has drift-prone settings",
494
+ why: "Codex config.toml issues are hard to debug when deprecated feature flags, missing permission profiles, or machine-local project trust metadata are mixed into repository or dotfiles config.",
495
+ evidence: evidence.slice(0, 10),
496
+ suggestedRule: "Keep Codex config portable: migrate [features].codex_hooks to [features].hooks, define every default_permissions profile, and avoid syncing projects.* trusted_level entries."
497
+ }];
498
+ }
499
+ async function inspectMcpServer(root, file, content, name, server) {
500
+ const evidence = [];
501
+ const hasUrl = typeof server.url === "string" || typeof server.httpUrl === "string";
502
+ const command = typeof server.command === "string" ? server.command.trim() : "";
503
+ if (!command && !hasUrl) {
504
+ evidence.push({
505
+ file,
506
+ line: findLine(content, name),
507
+ excerpt: `server "${name}" has neither command nor url`
508
+ });
509
+ }
510
+ const commandIssue = startupStringIssue(command);
511
+ if (commandIssue) {
512
+ evidence.push({
513
+ file,
514
+ line: findLine(content, command),
515
+ excerpt: `server "${name}" command ${commandIssue}: ${command}`
516
+ });
517
+ }
518
+ else if (command && !(await commandExists(root, command))) {
519
+ evidence.push({
520
+ file,
521
+ line: findLine(content, command),
522
+ excerpt: `server "${name}" command is not resolvable without starting it: ${command}`
523
+ });
524
+ }
525
+ if (typeof server.cwd === "string" && server.cwd.trim()) {
526
+ const cwd = server.cwd.trim();
527
+ const cwdIssue = startupStringIssue(cwd);
528
+ if (cwdIssue) {
529
+ evidence.push({
530
+ file,
531
+ line: findLine(content, cwd),
532
+ excerpt: `server "${name}" cwd ${cwdIssue}: ${cwd}`
533
+ });
534
+ }
535
+ else if (!(await localPathExists(root, cwd))) {
536
+ evidence.push({
537
+ file,
538
+ line: findLine(content, cwd),
539
+ excerpt: `server "${name}" cwd does not exist: ${cwd}`
540
+ });
541
+ }
542
+ }
543
+ if (Array.isArray(server.args)) {
544
+ server.args.forEach((value) => {
545
+ if (typeof value !== "string") {
546
+ return;
547
+ }
548
+ const issue = startupStringIssue(value);
549
+ if (issue) {
550
+ evidence.push({
551
+ file,
552
+ line: findLine(content, value),
553
+ excerpt: `server "${name}" arg ${issue}: ${value}`
554
+ });
555
+ }
556
+ });
557
+ }
558
+ const env = asObject(server.env);
559
+ if (env) {
560
+ for (const [key, value] of Object.entries(env)) {
561
+ const issue = envValueIssue(key, value);
562
+ if (issue) {
563
+ evidence.push({
564
+ file,
565
+ line: findLine(content, key),
566
+ excerpt: `server "${name}" env ${key}: ${issue}`
567
+ });
568
+ }
569
+ }
570
+ }
571
+ return evidence;
572
+ }
573
+ function discoverMcpServers(parsed) {
574
+ const wrapped = asObject(parsed.mcpServers) ?? asObject(parsed.servers) ?? asObject(parsed.mcp_servers);
575
+ if (wrapped) {
576
+ return wrapped;
577
+ }
578
+ const entries = Object.entries(parsed).filter(([key, value]) => !key.startsWith("$") && asObject(value));
579
+ if (entries.length > 0 && entries.every(([, value]) => looksLikeMcpServer(value))) {
580
+ return Object.fromEntries(entries);
581
+ }
582
+ return undefined;
583
+ }
584
+ function looksLikeMcpServer(value) {
585
+ const server = asObject(value);
586
+ return Boolean(server && (typeof server.command === "string" ||
587
+ typeof server.url === "string" ||
588
+ typeof server.httpUrl === "string" ||
589
+ Array.isArray(server.args) ||
590
+ asObject(server.env)));
591
+ }
592
+ function commandLooksLocal(value) {
593
+ return typeof value === "string" && (value.includes("/") || value.includes("\\") || value === "php" || value === "node" || value === "python" || value === "python3");
594
+ }
595
+ async function commandExists(root, command) {
596
+ if (command.includes("/") || command.includes("\\")) {
597
+ return localPathExists(root, command);
598
+ }
599
+ const paths = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
600
+ const extensions = process.platform === "win32"
601
+ ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
602
+ : [""];
603
+ for (const directory of paths) {
604
+ for (const extension of extensions) {
605
+ const candidate = path.join(directory, `${command}${extension}`);
606
+ if (await isExecutableFile(candidate)) {
607
+ return true;
608
+ }
609
+ }
610
+ }
611
+ return false;
612
+ }
613
+ async function localPathExists(root, value) {
614
+ const expanded = value === "~" || value.startsWith("~/")
615
+ ? path.join(os.homedir(), value.slice(2))
616
+ : value;
617
+ const candidate = path.isAbsolute(expanded) ? expanded : path.join(root, expanded);
618
+ try {
619
+ await fs.access(candidate);
620
+ return true;
621
+ }
622
+ catch {
623
+ return false;
624
+ }
625
+ }
626
+ async function isExecutableFile(candidate) {
627
+ try {
628
+ const stat = await fs.stat(candidate);
629
+ if (!stat.isFile()) {
630
+ return false;
631
+ }
632
+ if (process.platform === "win32") {
633
+ return true;
634
+ }
635
+ await fs.access(candidate, fsConstants.X_OK);
636
+ return true;
637
+ }
638
+ catch {
639
+ return false;
640
+ }
641
+ }
642
+ function envValueIssue(key, value) {
643
+ if (typeof value !== "string") {
644
+ return "value should be a string so the launcher receives the intended environment variable";
645
+ }
646
+ const trimmed = value.trim();
647
+ if (!trimmed) {
648
+ return "empty value";
649
+ }
650
+ if (/^(todo|tbd|changeme|change-me|your[-_ ]?(token|key|secret|password)|example)$/i.test(trimmed)) {
651
+ return "placeholder value";
652
+ }
653
+ const variableReference = /^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/.exec(trimmed);
654
+ if (variableReference && !process.env[variableReference[1]]) {
655
+ return `references unset environment variable ${variableReference[1]}`;
656
+ }
657
+ if (/token|secret|key|password/i.test(key) && /^[A-Za-z0-9_./+=-]{16,}$/.test(trimmed) && !variableReference) {
658
+ return "appears to contain a literal secret; prefer referencing an external environment variable";
659
+ }
660
+ return undefined;
661
+ }
662
+ function startupStringIssue(value) {
663
+ const variableReferences = Array.from(value.matchAll(/\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g)).map((match) => match[1]);
664
+ const unresolved = variableReferences.filter((name) => !process.env[name]);
665
+ if (unresolved.length > 0) {
666
+ return `references unset environment variable ${unresolved.join(", ")}`;
667
+ }
668
+ return undefined;
669
+ }
670
+ function parseMcpConfig(file, content) {
671
+ if (isTomlConfig(file)) {
672
+ return parseMcpToml(content);
673
+ }
674
+ return parseJsonObject(stripJsonComments(content));
675
+ }
676
+ function isTomlConfig(file) {
677
+ return /\.toml$/i.test(file);
678
+ }
679
+ function isCodexTomlConfig(file) {
680
+ return /(^|\/)\.codex\/config\.toml$/i.test(file);
681
+ }
682
+ function parseMcpToml(content) {
683
+ const mcpServers = {};
684
+ const lines = content.split(/\r?\n/);
685
+ let currentServer;
686
+ for (const rawLine of lines) {
687
+ const line = stripTomlComment(rawLine).trim();
688
+ if (!line) {
689
+ continue;
690
+ }
691
+ const section = /^\[mcp_servers\.([A-Za-z0-9_.-]+)\]$/.exec(line) ?? /^\[mcpServers\.([A-Za-z0-9_.-]+)\]$/.exec(line);
692
+ if (section) {
693
+ currentServer = {};
694
+ mcpServers[section[1]] = currentServer;
695
+ continue;
696
+ }
697
+ if (line.startsWith("[") && line.endsWith("]")) {
698
+ currentServer = undefined;
699
+ continue;
700
+ }
701
+ if (!currentServer) {
702
+ continue;
703
+ }
704
+ const assignment = /^([A-Za-z0-9_-]+)\s*=\s*(.+)$/.exec(line);
705
+ if (!assignment) {
706
+ continue;
707
+ }
708
+ currentServer[assignment[1]] = parseTomlValue(assignment[2].trim());
709
+ }
710
+ return Object.keys(mcpServers).length > 0 ? { mcp_servers: mcpServers } : {};
711
+ }
712
+ function parseTomlValue(value) {
713
+ if (value.startsWith("\"") || value.startsWith("'")) {
714
+ return parseTomlString(value);
715
+ }
716
+ if (value.startsWith("[")) {
717
+ return parseTomlArray(value);
718
+ }
719
+ if (value === "true") {
720
+ return true;
721
+ }
722
+ if (value === "false") {
723
+ return false;
724
+ }
725
+ return value;
726
+ }
727
+ function parseTomlArray(value) {
728
+ const trimmed = value.trim();
729
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
730
+ return [];
731
+ }
732
+ const items = [];
733
+ const pattern = /"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^,\s][^,]*)/g;
734
+ const inner = trimmed.slice(1, -1);
735
+ for (const match of inner.matchAll(pattern)) {
736
+ if (match[1] !== undefined) {
737
+ items.push(unescapeTomlString(match[1]));
738
+ }
739
+ else if (match[2] !== undefined) {
740
+ items.push(match[2]);
741
+ }
742
+ else if (match[3] !== undefined) {
743
+ items.push(parseTomlValue(match[3].trim()));
744
+ }
745
+ }
746
+ return items;
747
+ }
748
+ function parseTomlString(value) {
749
+ if (value.startsWith("\"")) {
750
+ const match = /^"((?:\\.|[^"\\])*)"/.exec(value);
751
+ return match ? unescapeTomlString(match[1]) : value;
752
+ }
753
+ const match = /^'([^']*)'/.exec(value);
754
+ return match ? match[1] : value;
755
+ }
756
+ function unescapeTomlString(value) {
757
+ return value
758
+ .replace(/\\"/g, "\"")
759
+ .replace(/\\n/g, "\n")
760
+ .replace(/\\t/g, "\t")
761
+ .replace(/\\\\/g, "\\");
762
+ }
763
+ function stripTomlComment(line) {
764
+ let quote;
765
+ for (let index = 0; index < line.length; index += 1) {
766
+ const char = line[index];
767
+ const previous = line[index - 1];
768
+ if ((char === "\"" || char === "'") && previous !== "\\") {
769
+ quote = quote === char ? undefined : quote ?? char;
770
+ continue;
771
+ }
772
+ if (char === "#" && !quote) {
773
+ return line.slice(0, index);
774
+ }
775
+ }
776
+ return line;
777
+ }
778
+ function parseTomlLines(content) {
779
+ const result = [];
780
+ let currentSection;
781
+ content.split(/\r?\n/).forEach((rawLine, index) => {
782
+ const line = stripTomlComment(rawLine).trim();
783
+ if (!line) {
784
+ return;
785
+ }
786
+ const section = /^\[([^\]]+)\]$/.exec(line);
787
+ if (section) {
788
+ currentSection = section[1];
789
+ result.push({ line: index + 1, section: currentSection });
790
+ return;
791
+ }
792
+ const assignment = /^([A-Za-z0-9_-]+)\s*=\s*(.+)$/.exec(line);
793
+ if (assignment) {
794
+ result.push({
795
+ line: index + 1,
796
+ section: currentSection,
797
+ key: assignment[1],
798
+ value: parseTomlValue(assignment[2].trim())
799
+ });
800
+ }
801
+ });
802
+ return result;
803
+ }
804
+ function collectPermissionProfiles(lines) {
805
+ const profiles = new Set();
806
+ for (const item of lines) {
807
+ if (!item.section) {
808
+ continue;
809
+ }
810
+ const match = /^permissions\.((?:"[^"]+"|'[^']+'|[^.]+))/.exec(item.section);
811
+ if (match) {
812
+ profiles.add(unquoteTomlBarePart(match[1]));
813
+ }
814
+ }
815
+ return profiles;
816
+ }
817
+ function findTomlAssignment(lines, key) {
818
+ return lines.find((item) => item.key === key);
819
+ }
820
+ function collectSections(lines) {
821
+ return lines.filter((item) => item.section && !item.key).map((item) => item.section);
822
+ }
823
+ function unquoteTomlBarePart(value) {
824
+ if (value.startsWith("\"") || value.startsWith("'")) {
825
+ return parseTomlString(value);
826
+ }
827
+ return value;
828
+ }
829
+ function stripJsonComments(content) {
830
+ return content
831
+ .replace(/\/\*[\s\S]*?\*\//g, "")
832
+ .replace(/^\s*\/\/.*$/gm, "");
833
+ }
834
+ function parseJsonObject(content) {
835
+ try {
836
+ const parsed = JSON.parse(content);
837
+ return asObject(parsed);
838
+ }
839
+ catch {
840
+ return undefined;
841
+ }
842
+ }
843
+ function asObject(value) {
844
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
845
+ }
846
+ function findLine(content, needle) {
847
+ const lines = content.split(/\r?\n/);
848
+ const index = lines.findIndex((line) => line.includes(needle));
849
+ return index >= 0 ? index + 1 : 1;
850
+ }
156
851
  //# sourceMappingURL=agentsLint.js.map