agentsys 5.14.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude-plugin/marketplace.json +1 -27
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +2 -3
  4. package/AGENTS.md +4 -6
  5. package/CHANGELOG.md +13 -0
  6. package/README.md +5 -115
  7. package/lib/binary/index.js +8 -2
  8. package/lib/binary/shared-helpers.js +160 -0
  9. package/lib/collectors/codebase.js +7 -2
  10. package/lib/collectors/documentation.js +8 -2
  11. package/lib/enhance/agent-patterns.js +17 -4
  12. package/lib/enhance/auto-suppression.js +19 -7
  13. package/lib/enhance/cross-file-analyzer.js +11 -4
  14. package/lib/enhance/docs-patterns.js +6 -2
  15. package/lib/enhance/fixer.js +22 -5
  16. package/lib/enhance/skill-patterns.js +5 -5
  17. package/lib/index.js +2 -0
  18. package/lib/repo-intel/cache.js +171 -0
  19. package/lib/repo-intel/converter.js +130 -0
  20. package/lib/repo-intel/embed/binary.js +242 -0
  21. package/lib/repo-intel/embed/index.js +26 -0
  22. package/lib/repo-intel/embed/orchestrator.js +239 -0
  23. package/lib/repo-intel/embed/preference.js +136 -0
  24. package/lib/repo-intel/enrich.js +198 -0
  25. package/lib/repo-intel/index.js +370 -0
  26. package/lib/repo-intel/installer.js +78 -0
  27. package/lib/repo-intel/queries.js +213 -13
  28. package/lib/repo-intel/updater.js +104 -0
  29. package/lib/repo-map/index.js +19 -254
  30. package/package.json +1 -1
  31. package/scripts/generate-docs.js +2 -13
  32. package/scripts/plugins.txt +0 -2
  33. package/site/assets/js/main.js +5 -13
  34. package/site/content.json +7 -24
  35. package/site/index.html +26 -74
  36. package/site/ux-spec.md +6 -6
  37. package/.kiro/agents/web-session.json +0 -12
  38. package/.kiro/skills/web-auth/SKILL.md +0 -177
  39. package/.kiro/skills/web-browse/SKILL.md +0 -516
@@ -54,9 +54,12 @@ const PATTERN_HEURISTICS = {
54
54
  const contentLower = content.toLowerCase();
55
55
 
56
56
  // Check if file is pattern documentation describing vague language detection
57
+ // ReDoS fix: the .* runs never matched across newlines (. excludes \n), so
58
+ // bounding them to [^\n]{0,N} keeps the same "within one line, in order"
59
+ // semantics while removing the polynomial multi-.* backtracking.
57
60
  const isPatternDoc =
58
- /pattern.*detect.*usually|example.*vague|fuzzy.*language.*like/i.test(content) ||
59
- /vague.*terms.*like|"usually".*"sometimes"/i.test(content);
61
+ /pattern[^\n]{0,500}detect[^\n]{0,500}usually|example[^\n]{0,500}vague|fuzzy[^\n]{0,500}language[^\n]{0,500}like/i.test(content) ||
62
+ /vague[^\n]{0,500}terms[^\n]{0,500}like|"usually"[^\n]{0,500}"sometimes"/i.test(content);
60
63
 
61
64
  if (isPatternDoc) {
62
65
  return {
@@ -129,7 +132,10 @@ const PATTERN_HEURISTICS = {
129
132
  const isOrchestrator =
130
133
  fileNameLower.includes('orchestrator') ||
131
134
  fileNameLower.includes('coordinator') ||
132
- /Task\s*\(\s*\{[\s\S]*subagent_type/i.test(content);
135
+ // ReDoS fix: bound the unbounded [\s\S]* so a "Task({" with no following
136
+ // subagent_type cannot drive polynomial backtracking; 50k chars covers any
137
+ // realistic Task(...) call body.
138
+ /Task\s{0,100}\(\s{0,100}\{[\s\S]{0,50000}subagent_type/i.test(content);
133
139
 
134
140
  if (isOrchestrator) {
135
141
  return {
@@ -140,7 +146,9 @@ const PATTERN_HEURISTICS = {
140
146
 
141
147
  // Check if workflow command that invokes agents
142
148
  const isWorkflowCommand =
143
- /spawn.*agent|invoke.*agent|Task\s*\(\s*\{/i.test(content) &&
149
+ // ReDoS fix: bound the within-line .* runs and \s* runs ([^\n] == . here)
150
+ // to keep the same matches without polynomial backtracking.
151
+ /spawn[^\n]{0,500}agent|invoke[^\n]{0,500}agent|Task\s{0,100}\(\s{0,100}\{/i.test(content) &&
144
152
  fileNameLower.endsWith('.md');
145
153
 
146
154
  if (isWorkflowCommand) {
@@ -159,9 +167,11 @@ const PATTERN_HEURISTICS = {
159
167
  */
160
168
  missing_output_format: (finding, content, context) => {
161
169
  // Check if content spawns subagents with their own output specs
170
+ // ReDoS fix: bound the within-line .* and \s* runs ([^\n] == . here) so the
171
+ // same membership matches hold without polynomial backtracking.
162
172
  const spawnsSubagent =
163
- /subagent_type|spawn.*agent|Task\s*\(\s*\{/i.test(content) ||
164
- /enhance:.*-enhancer|enhance:.*-reporter/i.test(content);
173
+ /subagent_type|spawn[^\n]{0,500}agent|Task\s{0,100}\(\s{0,100}\{/i.test(content) ||
174
+ /enhance:[^\n]{0,500}-enhancer|enhance:[^\n]{0,500}-reporter/i.test(content);
165
175
 
166
176
  if (spawnsSubagent) {
167
177
  return {
@@ -180,7 +190,9 @@ const PATTERN_HEURISTICS = {
180
190
  missing_constraints: (finding, content, context) => {
181
191
  // Check for constraint section presence
182
192
  const hasConstraintSection =
183
- /##\s*What\s+.*MUST\s+NOT\s+Do/i.test(content) ||
193
+ // ReDoS fix: bound the within-line .* and \s runs ([^\n] == . here) so the
194
+ // "## What ... MUST NOT Do" heading still matches without backtracking.
195
+ /##\s{0,100}What\s{1,100}[^\n]{0,500}MUST\s{1,100}NOT\s{1,100}Do/i.test(content) ||
184
196
  /##\s*Constraints/i.test(content) ||
185
197
  /<constraints>/i.test(content) ||
186
198
  /##\s*Critical\s+Constraints/i.test(content) ||
@@ -114,8 +114,11 @@ const CRITICAL_PATTERNS = [
114
114
  const SUBAGENT_PATTERN = /subagent_type\s*[=:]\s*["']([^"']+)["']/g;
115
115
 
116
116
  /** Pre-compiled patterns for cleaning content */
117
- const BAD_EXAMPLE_TAG_PATTERN = /<bad[_\- ]?example>[\s\S]*?<\/bad[_\- ]?example>/gi;
118
- const BAD_EXAMPLE_CODE_PATTERN = /```[^\n]*bad[^\n]*\n[\s\S]*?```/gi;
117
+ // ReDoS fix: bound the lazy [\s\S]*? bodies so an unterminated <bad-example> or
118
+ // ``` fence cannot drive polynomial backtracking; 50k chars covers any realistic
119
+ // example block, so the stripped regions are unchanged for real content.
120
+ const BAD_EXAMPLE_TAG_PATTERN = /<bad[_\- ]?example>[\s\S]{0,50000}?<\/bad[_\- ]?example>/gi;
121
+ const BAD_EXAMPLE_CODE_PATTERN = /```[^\n]{0,500}bad[^\n]{0,500}\n[\s\S]{0,50000}?```/gi;
119
122
 
120
123
  // ============================================
121
124
  // TOOL PATTERN CACHE
@@ -649,10 +652,14 @@ function analyzePromptConsistency(agents) {
649
652
 
650
653
  // Extract action keywords
651
654
  let action;
655
+ // ReDoS fix: bound the greedy prefix to non-newline chars. `line` is a single
656
+ // trimmed line (no newlines), so [^\n]{0,N} is equivalent to the prior `.*`:
657
+ // greedy match strips everything up to and including the LAST keyword plus its
658
+ // trailing whitespace, preserving the word-boundary semantics exactly.
652
659
  if (isAlways) {
653
- action = line.replace(/.*\bALWAYS\b\s*/i, '').substring(0, ACTION_COMPARISON_LENGTH);
660
+ action = line.replace(/[^\n]{0,2000}\bALWAYS\b\s{0,200}/i, '').substring(0, ACTION_COMPARISON_LENGTH);
654
661
  } else {
655
- action = line.replace(/.*\b(?:NEVER|DO NOT)\b\s*/i, '').substring(0, ACTION_COMPARISON_LENGTH);
662
+ action = line.replace(/[^\n]{0,2000}\b(?:NEVER|DO NOT)\b\s{0,200}/i, '').substring(0, ACTION_COMPARISON_LENGTH);
656
663
  }
657
664
 
658
665
  // Extract significant keywords from action
@@ -24,7 +24,9 @@ const docsPatterns = {
24
24
  if (!content || typeof content !== 'string') return null;
25
25
 
26
26
  // Find markdown links
27
- const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
27
+ // ReDoS fix: bound the negated-class captures so the matcher is linear;
28
+ // bounds far exceed any realistic markdown link, so matches are unchanged.
29
+ const linkRegex = /\[([^\]]{1,2000})\]\(([^)]{1,4000})\)/g;
28
30
  const brokenLinks = [];
29
31
  let match;
30
32
 
@@ -40,7 +42,9 @@ const docsPatterns = {
40
42
  if (linkTarget.startsWith('#')) {
41
43
  const anchorId = linkTarget.slice(1).toLowerCase();
42
44
  // Generate expected heading anchors from content
43
- const headings = content.match(/^#{1,6}\s+(.+)$/gm) || [];
45
+ // ReDoS fix: bound the \s+ run; line-anchored (.+) cannot cross newlines
46
+ // so the same headings match as before.
47
+ const headings = content.match(/^#{1,6}\s{1,1000}(.+)$/gm) || [];
44
48
  const anchors = headings.map(h => {
45
49
  return h.replace(/^#{1,6}\s+/, '')
46
50
  .toLowerCase()
@@ -225,6 +225,16 @@ function applyFixes(issues, options = {}) {
225
225
  return results;
226
226
  }
227
227
 
228
+ // Prototype-pollution guard: reject path segments that would reach
229
+ // Object.prototype before they index/assign into `current`
230
+ // (CodeQL js/prototype-polluting-assignment). The check is written as inline
231
+ // `!==` comparisons against the literal dangerous keys - CodeQL recognizes
232
+ // that exact shape as a sanitizing barrier, whereas a Set.has() indirection
233
+ // is not traced through and leaves the assignment flagged.
234
+ function isSafeKey(key) {
235
+ return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
236
+ }
237
+
228
238
  function applyAtPath(obj, pathStr, fixFn) {
229
239
  const parts = pathStr.split('.');
230
240
  const result = structuredClone(obj);
@@ -235,10 +245,11 @@ function applyAtPath(obj, pathStr, fixFn) {
235
245
  if (part.includes('[')) {
236
246
  // Array access
237
247
  const match = part.match(/^((?!__proto__|constructor|prototype)[a-zA-Z_]\w*)\[(\d{1,10})\]$/);
238
- if (match) {
248
+ if (match && match[1] !== '__proto__' && match[1] !== 'constructor' && match[1] !== 'prototype') {
239
249
  current = current[match[1]][parseInt(match[2], 10)];
240
250
  }
241
251
  } else {
252
+ if (!isSafeKey(part)) return result; // refuse prototype-polluting traversal
242
253
  current = current[part];
243
254
  }
244
255
  }
@@ -246,10 +257,14 @@ function applyAtPath(obj, pathStr, fixFn) {
246
257
  const lastPart = parts[parts.length - 1];
247
258
  if (lastPart.includes('[')) {
248
259
  const match = lastPart.match(/^((?!__proto__|constructor|prototype)[a-zA-Z_]\w*)\[(\d{1,10})\]$/);
249
- if (match) {
250
- current[match[1]][parseInt(match[2], 10)] = fixFn(current[match[1]][parseInt(match[2], 10)]);
260
+ // Inline literal guard (not the isSafeKey helper) so CodeQL traces the
261
+ // sanitizing barrier on the assignment below.
262
+ if (match && match[1] !== '__proto__' && match[1] !== 'constructor' && match[1] !== 'prototype') {
263
+ const key = match[1];
264
+ const idx = parseInt(match[2], 10);
265
+ current[key][idx] = fixFn(current[key][idx]);
251
266
  }
252
- } else {
267
+ } else if (lastPart !== '__proto__' && lastPart !== 'constructor' && lastPart !== 'prototype') {
253
268
  current[lastPart] = fixFn(current[lastPart]);
254
269
  }
255
270
 
@@ -780,5 +795,7 @@ module.exports = {
780
795
  previewFixes,
781
796
  restoreFromBackup,
782
797
  cleanupBackups,
783
- assertNotSymlink
798
+ assertNotSymlink,
799
+ // Exported for prototype-pollution regression tests.
800
+ applyAtPath
784
801
  };
@@ -76,11 +76,11 @@ const skillPatterns = {
76
76
  (content && p.test(content));
77
77
  });
78
78
 
79
- const disableModelInvocation = frontmatter['disable-model-invocation'];
80
- const isManualOnly = disableModelInvocation === true ||
81
- (typeof disableModelInvocation === 'string' && disableModelInvocation.toLowerCase() === 'true');
82
-
83
- if (hasSideEffects && !isManualOnly) {
79
+ // Accept both the YAML boolean `true` and the quoted string "true" -
80
+ // users commonly write `disable-model-invocation: "true"`, which YAML
81
+ // parses as a string; a strict `!== true` would wrongly re-flag it.
82
+ const dmi = frontmatter['disable-model-invocation'];
83
+ if (hasSideEffects && dmi !== true && dmi !== 'true') {
84
84
  return {
85
85
  issue: 'Skill with side effects should have disable-model-invocation: true',
86
86
  fix: 'Add "disable-model-invocation: true" to frontmatter for manual-only invocation'
package/lib/index.js CHANGED
@@ -25,6 +25,7 @@ const customHandler = require('./sources/custom-handler');
25
25
  const policyQuestions = require('./sources/policy-questions');
26
26
  const crossPlatform = require('./cross-platform');
27
27
  const enhance = require('./enhance');
28
+ const repoIntel = require('./repo-intel');
28
29
  const repoMap = require('./repo-map');
29
30
  const perf = require('./perf');
30
31
  const collectors = require('./collectors');
@@ -250,6 +251,7 @@ module.exports = {
250
251
  sources,
251
252
  xplat,
252
253
  enhance,
254
+ repoIntel,
253
255
  repoMap,
254
256
  perf,
255
257
  collectors,
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Repo map cache management
3
+ *
4
+ * @module lib/repo-intel/cache
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { getStateDirPath } = require('../platform/state-dir');
12
+ const { writeJsonAtomic, writeFileAtomic } = require('../utils/atomic-write');
13
+
14
+ const MAP_FILENAME = 'repo-map.json';
15
+ const STALE_FILENAME = 'repo-map.stale';
16
+ const INTEL_FILENAME = 'repo-intel.json';
17
+
18
+ /**
19
+ * Get repo-map path (the converted view artifact).
20
+ * @param {string} basePath - Repository root
21
+ * @returns {string}
22
+ */
23
+ function getMapPath(basePath) {
24
+ return path.join(getStateDirPath(basePath), MAP_FILENAME);
25
+ }
26
+
27
+ /**
28
+ * Get the RAW repo-intel.json path (the binary's native artifact).
29
+ * The embed submodule (orchestrator.js) feeds this to the embed binary's
30
+ * --map-file. In the standalone layout cache.getPath returned repo-intel.json;
31
+ * the fold renamed the converted-view accessor to getMapPath, so getPath keeps
32
+ * its original raw-artifact meaning. Mirrors index.js getIntelMapPath.
33
+ * @param {string} basePath - Repository root
34
+ * @returns {string}
35
+ */
36
+ function getPath(basePath) {
37
+ return path.join(getStateDirPath(basePath), INTEL_FILENAME);
38
+ }
39
+
40
+ /**
41
+ * Get stale marker path
42
+ * @param {string} basePath - Repository root
43
+ * @returns {string}
44
+ */
45
+ function getStalePath(basePath) {
46
+ return path.join(getStateDirPath(basePath), STALE_FILENAME);
47
+ }
48
+
49
+ /**
50
+ * Ensure state directory exists
51
+ * @param {string} basePath - Repository root
52
+ * @returns {string}
53
+ */
54
+ function ensureStateDir(basePath) {
55
+ const stateDir = getStateDirPath(basePath);
56
+ if (!fs.existsSync(stateDir)) {
57
+ fs.mkdirSync(stateDir, { recursive: true });
58
+ }
59
+ return stateDir;
60
+ }
61
+
62
+ /**
63
+ * Load repo-map from cache
64
+ * @param {string} basePath - Repository root
65
+ * @returns {Object|null}
66
+ */
67
+ function load(basePath) {
68
+ const mapPath = getMapPath(basePath);
69
+ if (!fs.existsSync(mapPath)) return null;
70
+
71
+ try {
72
+ const raw = fs.readFileSync(mapPath, 'utf8');
73
+ return JSON.parse(raw);
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Save repo-map to cache
81
+ * @param {string} basePath - Repository root
82
+ * @param {Object} map - Map object
83
+ */
84
+ function save(basePath, map) {
85
+ ensureStateDir(basePath);
86
+ const mapPath = getMapPath(basePath);
87
+
88
+ const output = {
89
+ ...map,
90
+ updated: new Date().toISOString()
91
+ };
92
+
93
+ writeJsonAtomic(mapPath, output);
94
+
95
+ // Clear stale marker if present
96
+ clearStale(basePath);
97
+ }
98
+
99
+ /**
100
+ * Check if repo-map exists
101
+ * @param {string} basePath - Repository root
102
+ * @returns {boolean}
103
+ */
104
+ function exists(basePath) {
105
+ return fs.existsSync(getMapPath(basePath));
106
+ }
107
+
108
+ /**
109
+ * Mark repo-map as stale
110
+ * @param {string} basePath - Repository root
111
+ */
112
+ function markStale(basePath) {
113
+ ensureStateDir(basePath);
114
+ writeFileAtomic(getStalePath(basePath), new Date().toISOString());
115
+ }
116
+
117
+ /**
118
+ * Clear stale marker
119
+ * @param {string} basePath - Repository root
120
+ */
121
+ function clearStale(basePath) {
122
+ const stalePath = getStalePath(basePath);
123
+ if (fs.existsSync(stalePath)) {
124
+ fs.unlinkSync(stalePath);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Check if stale marker exists
130
+ * @param {string} basePath - Repository root
131
+ * @returns {boolean}
132
+ */
133
+ function isMarkedStale(basePath) {
134
+ return fs.existsSync(getStalePath(basePath));
135
+ }
136
+
137
+ /**
138
+ * Get basic status summary
139
+ * @param {string} basePath - Repository root
140
+ * @returns {Object|null}
141
+ */
142
+ function getStatus(basePath) {
143
+ const map = load(basePath);
144
+ if (!map) return null;
145
+
146
+ return {
147
+ generated: map.generated,
148
+ updated: map.updated,
149
+ commit: map.git?.commit,
150
+ branch: map.git?.branch,
151
+ files: Object.keys(map.files || {}).length,
152
+ symbols: map.stats?.totalSymbols || 0,
153
+ languages: map.project?.languages || []
154
+ };
155
+ }
156
+
157
+ module.exports = {
158
+ load,
159
+ save,
160
+ exists,
161
+ getStatus,
162
+ getMapPath,
163
+ // getPath -> raw repo-intel.json (embed orchestrator); getStateDirPath
164
+ // re-exported for embed/preference.js. Both delegate to platform/state-dir
165
+ // and match the names the standalone cache exposed.
166
+ getPath,
167
+ getStateDirPath,
168
+ markStale,
169
+ clearStale,
170
+ isMarkedStale
171
+ };
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Convert agent-analyzer repo-intel.json format to repo-map.json format.
5
+ *
6
+ * agent-analyzer outputs: { symbols: { [filePath]: { exports, imports, definitions } } }
7
+ * repo-map expects: { files: { [filePath]: { language, symbols, imports } } }
8
+ *
9
+ * @module lib/repo-intel/converter
10
+ */
11
+
12
+ const path = require('path');
13
+
14
+ const LANGUAGE_BY_EXTENSION = {
15
+ '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
16
+ '.ts': 'typescript', '.tsx': 'typescript', '.mts': 'typescript', '.cts': 'typescript',
17
+ '.py': 'python', '.pyw': 'python',
18
+ '.rs': 'rust',
19
+ '.go': 'go',
20
+ '.java': 'java'
21
+ };
22
+
23
+ // SymbolKind values from agent-analyzer (kebab-case serialized)
24
+ const CLASS_KINDS = new Set(['class', 'struct', 'interface', 'enum', 'impl']);
25
+ const TYPE_KINDS = new Set(['trait', 'type-alias']);
26
+ const FUNCTION_LIKE_KINDS = new Set(['method', 'arrow', 'closure']);
27
+ const CONSTANT_KINDS = new Set(['constant', 'variable', 'const', 'field', 'property']);
28
+
29
+ function detectLanguage(filePath) {
30
+ return LANGUAGE_BY_EXTENSION[path.extname(filePath).toLowerCase()] || 'unknown';
31
+ }
32
+
33
+ function detectLanguagesFromFiles(filePaths) {
34
+ const langs = new Set();
35
+ for (const fp of filePaths) {
36
+ const lang = detectLanguage(fp);
37
+ if (lang !== 'unknown') langs.add(lang);
38
+ }
39
+ return Array.from(langs);
40
+ }
41
+
42
+ /**
43
+ * Convert a single file's symbols from repo-intel format to repo-map format.
44
+ * @param {string} filePath
45
+ * @param {Object} fileSym - { exports, imports, definitions }
46
+ * @returns {Object} repo-map file entry
47
+ */
48
+ function convertFile(filePath, fileSym) {
49
+ const exportNames = new Set((fileSym.exports || []).map(e => e.name));
50
+
51
+ const exports = (fileSym.exports || []).map(e => ({
52
+ name: e.name,
53
+ kind: e.kind,
54
+ line: e.line
55
+ }));
56
+
57
+ const functions = [];
58
+ const classes = [];
59
+ const types = [];
60
+ const constants = [];
61
+
62
+ for (const def of fileSym.definitions || []) {
63
+ const entry = {
64
+ name: def.name,
65
+ kind: def.kind,
66
+ line: def.line,
67
+ exported: exportNames.has(def.name)
68
+ };
69
+ if (def.kind === 'function' || FUNCTION_LIKE_KINDS.has(def.kind)) {
70
+ functions.push(entry);
71
+ } else if (CLASS_KINDS.has(def.kind)) {
72
+ classes.push(entry);
73
+ } else if (TYPE_KINDS.has(def.kind)) {
74
+ types.push(entry);
75
+ } else if (CONSTANT_KINDS.has(def.kind)) {
76
+ constants.push(entry);
77
+ } else {
78
+ // Unknown kind - default to constants for backward compat
79
+ constants.push(entry);
80
+ }
81
+ }
82
+
83
+ // agent-analyzer imports: [{ from, names }] → repo-map imports: [{ source, kind, names }]
84
+ const imports = (fileSym.imports || []).map(imp => ({
85
+ source: imp.from,
86
+ kind: 'import',
87
+ names: imp.names || []
88
+ }));
89
+
90
+ return {
91
+ language: detectLanguage(filePath),
92
+ symbols: { exports, functions, classes, types, constants },
93
+ imports
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Convert a full repo-intel data object to repo-map format.
99
+ * @param {Object} intel - RepoIntelData from agent-analyzer
100
+ * @returns {Object} repo-map.json compatible object
101
+ */
102
+ function convertIntelToRepoMap(intel) {
103
+ const files = {};
104
+ let totalSymbols = 0;
105
+ let totalImports = 0;
106
+
107
+ for (const [filePath, fileSym] of Object.entries(intel.symbols || {})) {
108
+ files[filePath] = convertFile(filePath, fileSym);
109
+ const s = files[filePath].symbols;
110
+ totalSymbols += s.functions.length + s.classes.length +
111
+ s.types.length + s.constants.length;
112
+ totalImports += files[filePath].imports.length;
113
+ }
114
+
115
+ return {
116
+ version: '2.0',
117
+ generated: intel.generated || new Date().toISOString(),
118
+ git: intel.git ? { commit: intel.git.analyzedUpTo } : undefined,
119
+ project: { languages: detectLanguagesFromFiles(Object.keys(files)) },
120
+ stats: {
121
+ totalFiles: Object.keys(files).length,
122
+ totalSymbols,
123
+ totalImports,
124
+ errors: []
125
+ },
126
+ files
127
+ };
128
+ }
129
+
130
+ module.exports = { convertIntelToRepoMap, convertFile, detectLanguage };