agentsys 5.13.4 → 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.
- package/.claude-plugin/marketplace.json +28 -28
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -3
- package/AGENTS.md +8 -8
- package/CHANGELOG.md +34 -0
- package/README.md +11 -116
- package/lib/binary/index.js +8 -2
- package/lib/binary/shared-helpers.js +160 -0
- package/lib/collectors/codebase.js +7 -2
- package/lib/collectors/documentation.js +8 -2
- package/lib/enhance/agent-patterns.js +17 -4
- package/lib/enhance/auto-suppression.js +19 -7
- package/lib/enhance/cross-file-analyzer.js +11 -4
- package/lib/enhance/docs-patterns.js +6 -2
- package/lib/enhance/fixer.js +22 -5
- package/lib/enhance/skill-patterns.js +5 -5
- package/lib/index.js +2 -0
- package/lib/repo-intel/cache.js +171 -0
- package/lib/repo-intel/converter.js +130 -0
- package/lib/repo-intel/embed/binary.js +242 -0
- package/lib/repo-intel/embed/index.js +26 -0
- package/lib/repo-intel/embed/orchestrator.js +239 -0
- package/lib/repo-intel/embed/preference.js +136 -0
- package/lib/repo-intel/enrich.js +198 -0
- package/lib/repo-intel/index.js +370 -0
- package/lib/repo-intel/installer.js +78 -0
- package/lib/repo-intel/queries.js +213 -13
- package/lib/repo-intel/updater.js +104 -0
- package/lib/repo-map/index.js +19 -254
- package/package.json +1 -1
- package/scripts/generate-docs.js +13 -18
- package/scripts/plugins.txt +2 -2
- package/site/assets/js/main.js +5 -13
- package/site/content.json +6 -23
- package/site/index.html +29 -77
- package/site/ux-spec.md +7 -7
- package/.kiro/agents/web-session.json +0 -12
- package/.kiro/skills/web-auth/SKILL.md +0 -177
- package/.kiro/skills/web-browse/SKILL.md +0 -516
|
@@ -50,7 +50,9 @@ function safeReadFile(filePath, basePath) {
|
|
|
50
50
|
* Analyze a single markdown file
|
|
51
51
|
*/
|
|
52
52
|
function analyzeMarkdownFile(content, filePath) {
|
|
53
|
-
|
|
53
|
+
// ReDoS fix: bound the \s+ run after the ## marker; line-anchored (.+) cannot
|
|
54
|
+
// cross newlines so this matches the same headings as before.
|
|
55
|
+
const sectionMatches = content.match(/^##\s{1,1000}(.+)$/gm) || [];
|
|
54
56
|
const sections = sectionMatches.slice(0, 10).map(s => s.replace(/^##\s+/, ''));
|
|
55
57
|
const sectionLower = sections.map(s => s.toLowerCase()).join(' ');
|
|
56
58
|
|
|
@@ -83,7 +85,11 @@ function extractCheckboxes(result, content) {
|
|
|
83
85
|
* Extract documented features
|
|
84
86
|
*/
|
|
85
87
|
function extractFeatures(result, content) {
|
|
86
|
-
|
|
88
|
+
// ReDoS fix: bound the \s+ run and the line-content quantifiers so the lazy
|
|
89
|
+
// (.+?) / optional trailing (.+) pair cannot backtrack polynomially. Using
|
|
90
|
+
// [^\n] is equivalent to . here (. never matches newline), and the bounds far
|
|
91
|
+
// exceed the 80-char feature cap applied below, so matches are unchanged.
|
|
92
|
+
const featurePattern = /^[-*]\s{1,100}\*{0,2}([^\n]{1,2000}?)\*{0,2}(?:\s{0,100}[-–]\s{0,100}([^\n]{1,2000}))?$/gm;
|
|
87
93
|
let match;
|
|
88
94
|
|
|
89
95
|
while ((match = featurePattern.exec(content)) !== null && result.features.length < 20) {
|
|
@@ -439,8 +439,17 @@ const agentPatterns = {
|
|
|
439
439
|
|
|
440
440
|
// Look for hardcoded .claude/ references
|
|
441
441
|
const hasHardcoded = /\.claude\//.test(content);
|
|
442
|
-
// Exclude if using AI_STATE_DIR
|
|
443
|
-
|
|
442
|
+
// Exclude if using AI_STATE_DIR or a ${...STATE...} env expression.
|
|
443
|
+
// ReDoS fix: the old /\$\{.*STATE.*\}/ (and the [^}]*STATE[^}]* rewrite)
|
|
444
|
+
// has two ambiguous quantifier runs -> polynomial backtrack. Instead
|
|
445
|
+
// scan each ${...} group with a single bounded [^}] run, then substring-
|
|
446
|
+
// test for STATE. Linear, and matches STATE in ANY ${...} like before.
|
|
447
|
+
let usesEnvVar = /AI_STATE_DIR/i.test(content);
|
|
448
|
+
if (!usesEnvVar) {
|
|
449
|
+
for (const m of content.matchAll(/\$\{([^}]{0,1000})\}/g)) {
|
|
450
|
+
if (/STATE/i.test(m[1])) { usesEnvVar = true; break; }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
444
453
|
|
|
445
454
|
if (hasHardcoded && !usesEnvVar) {
|
|
446
455
|
return {
|
|
@@ -494,8 +503,12 @@ const agentPatterns = {
|
|
|
494
503
|
|
|
495
504
|
// Check if has code blocks or lists but no XML
|
|
496
505
|
const hasCodeBlocks = /```[\s\S]+?```/.test(content);
|
|
497
|
-
|
|
498
|
-
|
|
506
|
+
// ReDoS fix: bound the \s+ and line-content runs; line-anchored so this still
|
|
507
|
+
// detects any "- item" / "* item" list line as before.
|
|
508
|
+
const hasLists = /^[-*]\s{1,1000}[^\n]{1,2000}$/m.test(content);
|
|
509
|
+
// ReDoS fix: bound the unbounded [\s\S]*? so an unterminated <tag> cannot
|
|
510
|
+
// drive polynomial backtracking; 50k chars covers any realistic XML block.
|
|
511
|
+
const hasXML = /<\w+>[\s\S]{0,50000}?<\/\w+>/.test(content);
|
|
499
512
|
const sectionCount = (content.match(/^##\s+/gm) || []).length;
|
|
500
513
|
|
|
501
514
|
// Complex content without XML
|
|
@@ -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
|
|
59
|
-
/vague
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
164
|
-
/enhance
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
package/lib/enhance/fixer.js
CHANGED
|
@@ -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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (hasSideEffects &&
|
|
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 };
|