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.
- package/.claude-plugin/marketplace.json +1 -27
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -3
- package/AGENTS.md +4 -6
- package/CHANGELOG.md +13 -0
- package/README.md +5 -115
- 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 +2 -13
- package/scripts/plugins.txt +0 -2
- package/site/assets/js/main.js +5 -13
- package/site/content.json +7 -24
- package/site/index.html +26 -74
- package/site/ux-spec.md +6 -6
- 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
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-init enrichment helpers.
|
|
3
|
+
*
|
|
4
|
+
* The repo-intel skill spawns two Haiku-backed Task subagents after
|
|
5
|
+
* the deterministic init/update pass:
|
|
6
|
+
*
|
|
7
|
+
* 1. `repo-intel-summarizer` reads README + manifests + hotspot
|
|
8
|
+
* headers and writes a 3-depth narrative summary.
|
|
9
|
+
* 2. `repo-intel-weighter` writes 1-2 sentence descriptors for the
|
|
10
|
+
* top-N most-active files, used by `find` to add semantic recall.
|
|
11
|
+
*
|
|
12
|
+
* The orchestration calls into this module to gather the agents'
|
|
13
|
+
* inputs, parse their JSON outputs (which arrive between marker
|
|
14
|
+
* blocks because subagent stdout is otherwise free-form), and pipe
|
|
15
|
+
* the result back through `repoIntel.applyDescriptors` /
|
|
16
|
+
* `applySummary`. The Rust binary stores the data; this module
|
|
17
|
+
* never touches the LLM directly.
|
|
18
|
+
*
|
|
19
|
+
* @module lib/repo-intel/enrich
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const crypto = require('crypto');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read the README, returning empty string when absent so the
|
|
30
|
+
* downstream JSON.stringify doesn't break.
|
|
31
|
+
*/
|
|
32
|
+
function readReadme(repoPath) {
|
|
33
|
+
for (const name of ['README.md', 'README.MD', 'readme.md', 'README.rst', 'README.txt', 'README']) {
|
|
34
|
+
const candidate = path.join(repoPath, name);
|
|
35
|
+
if (fs.existsSync(candidate)) {
|
|
36
|
+
try { return fs.readFileSync(candidate, 'utf8'); } catch { /* fall through */ }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read whichever manifests are present and return them as a parsed
|
|
44
|
+
* object keyed by manifest filename. Used by the summarizer to
|
|
45
|
+
* understand what kind of project it's looking at.
|
|
46
|
+
*/
|
|
47
|
+
function readManifests(repoPath) {
|
|
48
|
+
const manifests = {};
|
|
49
|
+
for (const name of ['package.json', 'Cargo.toml', 'pyproject.toml', 'go.mod', 'pom.xml', 'build.gradle']) {
|
|
50
|
+
const p = path.join(repoPath, name);
|
|
51
|
+
if (fs.existsSync(p)) {
|
|
52
|
+
try {
|
|
53
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
54
|
+
// Don't attempt to parse non-JSON manifests - the summarizer
|
|
55
|
+
// can read them as plain strings.
|
|
56
|
+
if (name.endsWith('.json')) {
|
|
57
|
+
try { manifests[name] = JSON.parse(text); }
|
|
58
|
+
catch { manifests[name] = text.slice(0, 4000); }
|
|
59
|
+
} else {
|
|
60
|
+
manifests[name] = text.slice(0, 4000);
|
|
61
|
+
}
|
|
62
|
+
} catch { /* skip on read error */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return manifests;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pick the top-N files by activity (changes + 2*recent_changes), and
|
|
70
|
+
* return `{path, head}` for each where `head` is the first 500 chars
|
|
71
|
+
* of file content. Used as `hotspots` input to the summarizer.
|
|
72
|
+
*
|
|
73
|
+
* For the weighter we just want the path list - call `topPaths()`.
|
|
74
|
+
*/
|
|
75
|
+
function topHotspots(repoPath, repoIntelData, n = 10) {
|
|
76
|
+
const paths = topPaths(repoIntelData, n);
|
|
77
|
+
return paths.map((p) => {
|
|
78
|
+
const abs = path.join(repoPath, p);
|
|
79
|
+
let head = '';
|
|
80
|
+
try {
|
|
81
|
+
const buf = fs.readFileSync(abs);
|
|
82
|
+
head = buf.subarray(0, Math.min(buf.length, 500)).toString('utf8');
|
|
83
|
+
} catch { /* file missing on disk, skip */ }
|
|
84
|
+
return { path: p, head };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Rank file_activity entries by activity score and return the top N
|
|
90
|
+
* paths. Recent changes are weighted 2x because the agent should
|
|
91
|
+
* prioritize files that are still being actively touched.
|
|
92
|
+
*/
|
|
93
|
+
function topPaths(repoIntelData, n) {
|
|
94
|
+
const fa = repoIntelData.fileActivity || {};
|
|
95
|
+
const scored = Object.entries(fa)
|
|
96
|
+
.map(([p, a]) => ({
|
|
97
|
+
path: p,
|
|
98
|
+
score: (a.changes || 0) + 2 * (a.recentChanges || 0)
|
|
99
|
+
}))
|
|
100
|
+
.filter((e) => e.score > 0);
|
|
101
|
+
scored.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
102
|
+
return scored.slice(0, n).map((e) => e.path);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Stable hash of the inputs that fed into a summary, so the skill
|
|
107
|
+
* can decide whether to regenerate. Mirrors what the Rust set-summary
|
|
108
|
+
* subcommand stores under summary.inputHash.
|
|
109
|
+
*/
|
|
110
|
+
function summaryInputHash(readme, manifests, hotspots) {
|
|
111
|
+
const h = crypto.createHash('sha256');
|
|
112
|
+
h.update(readme);
|
|
113
|
+
h.update(JSON.stringify(manifests));
|
|
114
|
+
h.update(JSON.stringify(hotspots));
|
|
115
|
+
return 'sha256:' + h.digest('hex').slice(0, 16);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract the JSON object between `=== <name>_START ===` and
|
|
120
|
+
* `=== <name>_END ===` markers in the agent's output.
|
|
121
|
+
*
|
|
122
|
+
* Returns the parsed object, or null if either marker is missing or
|
|
123
|
+
* the inner text doesn't parse as JSON. Tolerates extra whitespace
|
|
124
|
+
* and surrounding agent commentary.
|
|
125
|
+
*/
|
|
126
|
+
function parseMarkers(agentOutput, name) {
|
|
127
|
+
if (typeof agentOutput !== 'string') return null;
|
|
128
|
+
const startMarker = `=== ${name}_START ===`;
|
|
129
|
+
const endMarker = `=== ${name}_END ===`;
|
|
130
|
+
const startIdx = agentOutput.indexOf(startMarker);
|
|
131
|
+
const endIdx = agentOutput.indexOf(endMarker);
|
|
132
|
+
if (startIdx < 0 || endIdx < 0 || endIdx <= startIdx) return null;
|
|
133
|
+
const inner = agentOutput.slice(startIdx + startMarker.length, endIdx).trim();
|
|
134
|
+
// Inner is usually fenced inside ```json ... ``` or just raw JSON.
|
|
135
|
+
// Strip code fences if present, then parse.
|
|
136
|
+
const stripped = inner
|
|
137
|
+
.replace(/^```(?:json)?\s*/i, '')
|
|
138
|
+
.replace(/\s*```\s*$/i, '')
|
|
139
|
+
.trim();
|
|
140
|
+
try { return JSON.parse(stripped); }
|
|
141
|
+
catch { return null; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build the summarizer prompt - the literal string sent as the Task
|
|
146
|
+
* `prompt` argument. Kept here so the command markdown stays compact.
|
|
147
|
+
*/
|
|
148
|
+
function buildSummarizerPrompt(repoPath, readme, manifests, hotspots) {
|
|
149
|
+
return [
|
|
150
|
+
`Generate a 3-depth summary for the repo at ${repoPath}.`,
|
|
151
|
+
'',
|
|
152
|
+
'Inputs:',
|
|
153
|
+
'```json',
|
|
154
|
+
JSON.stringify({ repoPath, readme, manifests, hotspots }, null, 2),
|
|
155
|
+
'```',
|
|
156
|
+
'',
|
|
157
|
+
'Return JSON between `=== SUMMARY_START ===` and `=== SUMMARY_END ===` markers as instructed in your system prompt.'
|
|
158
|
+
].join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build the weighter prompt for one batch of paths.
|
|
163
|
+
*/
|
|
164
|
+
function buildWeighterPrompt(repoPath, paths) {
|
|
165
|
+
return [
|
|
166
|
+
`Generate descriptors for the following files in ${repoPath}.`,
|
|
167
|
+
'',
|
|
168
|
+
'Inputs:',
|
|
169
|
+
'```json',
|
|
170
|
+
JSON.stringify({ repoPath, paths }, null, 2),
|
|
171
|
+
'```',
|
|
172
|
+
'',
|
|
173
|
+
'Return JSON between `=== DESCRIPTORS_START ===` and `=== DESCRIPTORS_END ===` markers as instructed in your system prompt.'
|
|
174
|
+
].join('\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Split a list into chunks of `size`. Used to keep weighter Task
|
|
179
|
+
* calls bounded - one big Task with 500 paths would burn context;
|
|
180
|
+
* 30/batch keeps each call cheap and lets the orchestrator parallelize.
|
|
181
|
+
*/
|
|
182
|
+
function chunk(arr, size) {
|
|
183
|
+
const out = [];
|
|
184
|
+
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
readReadme,
|
|
190
|
+
readManifests,
|
|
191
|
+
topHotspots,
|
|
192
|
+
topPaths,
|
|
193
|
+
summaryInputHash,
|
|
194
|
+
parseMarkers,
|
|
195
|
+
buildSummarizerPrompt,
|
|
196
|
+
buildWeighterPrompt,
|
|
197
|
+
chunk
|
|
198
|
+
};
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Repo Intel - unified repository intelligence over agent-analyzer.
|
|
5
|
+
*
|
|
6
|
+
* One surface for the whole pipeline:
|
|
7
|
+
* - lifecycle: init / update / status / load / exists (was lib/repo-map)
|
|
8
|
+
* - queries: typed wrappers over the binary's query subcommands (queries.js)
|
|
9
|
+
* - install: binary availability checks
|
|
10
|
+
*
|
|
11
|
+
* The agent-analyzer binary is the engine and is auto-downloaded on first use.
|
|
12
|
+
* init/update run the binary, persist repo-intel.json (raw), convert to the
|
|
13
|
+
* repo-map.json view, and cache it. queries.* read the raw repo-intel.json.
|
|
14
|
+
*
|
|
15
|
+
* @module lib/repo-intel
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const cp = require('child_process');
|
|
21
|
+
const { execFileSync } = cp;
|
|
22
|
+
|
|
23
|
+
const installer = require('./installer');
|
|
24
|
+
const cache = require('./cache');
|
|
25
|
+
const updater = require('./updater');
|
|
26
|
+
const converter = require('./converter');
|
|
27
|
+
const queries = require('./queries');
|
|
28
|
+
const binary = require('../binary');
|
|
29
|
+
const { getStateDirPath } = require('../platform/state-dir');
|
|
30
|
+
const { writeJsonAtomic } = require('../utils/atomic-write');
|
|
31
|
+
|
|
32
|
+
const REPO_INTEL_FILENAME = 'repo-intel.json';
|
|
33
|
+
|
|
34
|
+
function getIntelMapPath(basePath) {
|
|
35
|
+
return path.join(getStateDirPath(basePath), REPO_INTEL_FILENAME);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initialize a new repo map (full scan).
|
|
40
|
+
* @param {string} basePath - Repository root path
|
|
41
|
+
* @param {Object} options - Options
|
|
42
|
+
* @param {boolean} options.force - Force rebuild even if map exists
|
|
43
|
+
* @returns {Promise<{success: boolean, map?: Object, error?: string}>}
|
|
44
|
+
*/
|
|
45
|
+
async function init(basePath, options = {}) {
|
|
46
|
+
const installed = await installer.checkInstalled();
|
|
47
|
+
if (!installed.found) {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
error: 'agent-analyzer binary unavailable: ' + (installed.error || 'unknown error'),
|
|
51
|
+
installSuggestion: installer.getInstallInstructions()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const existing = cache.load(basePath);
|
|
56
|
+
if (existing && !options.force) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: 'Repo map already exists. Use --force to rebuild or update to refresh.',
|
|
60
|
+
existing: cache.getStatus(basePath)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
|
|
66
|
+
let intelJson;
|
|
67
|
+
try {
|
|
68
|
+
intelJson = await binary.runAnalyzerAsync(['repo-intel', 'init', basePath]);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return { success: false, error: 'agent-analyzer repo-intel init failed: ' + e.message };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let intel;
|
|
74
|
+
try {
|
|
75
|
+
intel = JSON.parse(intelJson);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return { success: false, error: 'Failed to parse repo-intel output: ' + e.message };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Persist repo-intel.json for future incremental updates
|
|
81
|
+
const intelPath = getIntelMapPath(basePath);
|
|
82
|
+
try {
|
|
83
|
+
writeJsonAtomic(intelPath, intel);
|
|
84
|
+
} catch {
|
|
85
|
+
// Non-fatal: update() will fall back to full init
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const map = converter.convertIntelToRepoMap(intel);
|
|
89
|
+
map.stats.scanDurationMs = Date.now() - startTime;
|
|
90
|
+
cache.save(basePath, map);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
map,
|
|
95
|
+
summary: {
|
|
96
|
+
files: Object.keys(map.files).length,
|
|
97
|
+
symbols: map.stats.totalSymbols,
|
|
98
|
+
languages: map.project.languages,
|
|
99
|
+
duration: map.stats.scanDurationMs
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Update an existing repo map (incremental via agent-analyzer).
|
|
106
|
+
* @param {string} basePath - Repository root path
|
|
107
|
+
* @param {Object} options - Options
|
|
108
|
+
* @param {boolean} options.full - Force full rebuild instead of incremental
|
|
109
|
+
* @returns {Promise<{success: boolean, summary?: Object, error?: string}>}
|
|
110
|
+
*/
|
|
111
|
+
async function update(basePath, options = {}) {
|
|
112
|
+
const installed = await installer.checkInstalled();
|
|
113
|
+
if (!installed.found) {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
error: 'agent-analyzer binary unavailable: ' + (installed.error || 'unknown error'),
|
|
117
|
+
installSuggestion: installer.getInstallInstructions()
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!cache.exists(basePath)) {
|
|
122
|
+
return { success: false, error: 'No repo map found. Run init first.' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (options.full) {
|
|
126
|
+
return init(basePath, { force: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const intelPath = getIntelMapPath(basePath);
|
|
130
|
+
if (!fs.existsSync(intelPath)) {
|
|
131
|
+
return init(basePath, { force: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const startTime = Date.now();
|
|
135
|
+
|
|
136
|
+
let intelJson;
|
|
137
|
+
try {
|
|
138
|
+
intelJson = await binary.runAnalyzerAsync([
|
|
139
|
+
'repo-intel', 'update',
|
|
140
|
+
'--map-file', intelPath,
|
|
141
|
+
basePath
|
|
142
|
+
]);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return { success: false, error: 'agent-analyzer repo-intel update failed: ' + e.message };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let intel;
|
|
148
|
+
try {
|
|
149
|
+
intel = JSON.parse(intelJson);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
return { success: false, error: 'Failed to parse repo-intel update output: ' + e.message };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
writeJsonAtomic(intelPath, intel);
|
|
156
|
+
} catch {
|
|
157
|
+
// Non-fatal
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const map = converter.convertIntelToRepoMap(intel);
|
|
161
|
+
map.stats.scanDurationMs = Date.now() - startTime;
|
|
162
|
+
cache.save(basePath, map);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
success: true,
|
|
166
|
+
map,
|
|
167
|
+
summary: {
|
|
168
|
+
files: Object.keys(map.files).length,
|
|
169
|
+
symbols: map.stats.totalSymbols,
|
|
170
|
+
duration: map.stats.scanDurationMs
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get repo map status.
|
|
177
|
+
* @param {string} basePath - Repository root path
|
|
178
|
+
* @returns {{exists: boolean, status?: Object}}
|
|
179
|
+
*/
|
|
180
|
+
function status(basePath) {
|
|
181
|
+
const map = cache.load(basePath);
|
|
182
|
+
if (!map) {
|
|
183
|
+
return { exists: false };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const staleness = updater.checkStaleness(basePath, map);
|
|
187
|
+
|
|
188
|
+
let branch;
|
|
189
|
+
try {
|
|
190
|
+
branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: basePath, encoding: 'utf8' }).trim();
|
|
191
|
+
} catch {
|
|
192
|
+
// Non-fatal
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
exists: true,
|
|
197
|
+
status: {
|
|
198
|
+
generated: map.generated,
|
|
199
|
+
updated: map.updated,
|
|
200
|
+
commit: map.git?.commit,
|
|
201
|
+
branch,
|
|
202
|
+
files: Object.keys(map.files).length,
|
|
203
|
+
symbols: map.stats?.totalSymbols || 0,
|
|
204
|
+
languages: map.project?.languages || [],
|
|
205
|
+
staleness
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Load repo map (if exists).
|
|
212
|
+
* @param {string} basePath - Repository root path
|
|
213
|
+
* @returns {Object|null}
|
|
214
|
+
*/
|
|
215
|
+
function load(basePath) {
|
|
216
|
+
return cache.load(basePath);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Check if repo map exists.
|
|
221
|
+
* @param {string} basePath - Repository root path
|
|
222
|
+
* @returns {boolean}
|
|
223
|
+
*/
|
|
224
|
+
function exists(basePath) {
|
|
225
|
+
return cache.exists(basePath);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Load the RAW repo-intel.json (the binary's native output: fileActivity,
|
|
230
|
+
* coupling, symbols, importGraph, ...) — distinct from load(), which returns
|
|
231
|
+
* the converted repo-map.json view. enrich + any consumer needing raw git/AST
|
|
232
|
+
* structure uses this; queries.* also read raw under the hood.
|
|
233
|
+
* @param {string} basePath - Repository root path
|
|
234
|
+
* @returns {Object|null} parsed raw artifact, or null if absent/unreadable
|
|
235
|
+
*/
|
|
236
|
+
function loadRaw(basePath) {
|
|
237
|
+
const p = getIntelMapPath(basePath);
|
|
238
|
+
if (!fs.existsSync(p)) return null;
|
|
239
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Spawn the analyzer with a JSON payload on stdin and capture stdout/stderr.
|
|
244
|
+
*
|
|
245
|
+
* Used by the post-init agent orchestration: the Haiku weighter and
|
|
246
|
+
* summarizer write JSON to stdout, the orchestrating skill captures it,
|
|
247
|
+
* then pipes it into the analyzer via this helper. Replaces what would
|
|
248
|
+
* otherwise be a tempfile dance.
|
|
249
|
+
*
|
|
250
|
+
* @param {string[]} args - subcommand args (must end with `--input -`)
|
|
251
|
+
* @param {string} stdinJson - the JSON payload to feed to stdin
|
|
252
|
+
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
253
|
+
*/
|
|
254
|
+
async function runAnalyzerWithStdin(args, stdinJson) {
|
|
255
|
+
const binPath = await binary.ensureBinary();
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
const proc = cp.spawn(binPath, args, {
|
|
258
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
259
|
+
windowsHide: true
|
|
260
|
+
});
|
|
261
|
+
let stdout = '';
|
|
262
|
+
let stderr = '';
|
|
263
|
+
proc.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
264
|
+
proc.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
|
265
|
+
proc.on('error', reject);
|
|
266
|
+
proc.on('close', (code) => {
|
|
267
|
+
if (code === 0) {
|
|
268
|
+
resolve({ stdout, stderr });
|
|
269
|
+
} else {
|
|
270
|
+
reject(new Error(
|
|
271
|
+
`agent-analyzer ${args.join(' ')} exited ${code}: ${stderr.trim() || stdout.trim()}`
|
|
272
|
+
));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
proc.stdin.write(stdinJson);
|
|
276
|
+
proc.stdin.end();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Merge per-file descriptors (from the `repo-intel-weighter` agent) into the
|
|
282
|
+
* cached artifact via the binary's set-descriptors subcommand. Partial updates
|
|
283
|
+
* are safe — entries the agent didn't refresh this run are preserved.
|
|
284
|
+
*
|
|
285
|
+
* @param {string} basePath - Repository root path
|
|
286
|
+
* @param {Object<string, string>} descriptors - {path: descriptor, ...}
|
|
287
|
+
* @returns {Promise<void>}
|
|
288
|
+
*/
|
|
289
|
+
async function applyDescriptors(basePath, descriptors) {
|
|
290
|
+
if (!descriptors || typeof descriptors !== 'object') {
|
|
291
|
+
throw new Error('applyDescriptors requires an object {path: descriptor}');
|
|
292
|
+
}
|
|
293
|
+
const mapFile = getIntelMapPath(basePath);
|
|
294
|
+
if (!fs.existsSync(mapFile)) {
|
|
295
|
+
throw new Error('No repo-intel artifact for ' + basePath + '; run init first.');
|
|
296
|
+
}
|
|
297
|
+
await runAnalyzerWithStdin(
|
|
298
|
+
['repo-intel', 'set-descriptors', '--map-file', mapFile, '--input', '-'],
|
|
299
|
+
JSON.stringify(descriptors)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Set the 3-depth narrative summary (from the `repo-intel-summarizer` agent)
|
|
305
|
+
* via the binary's set-summary subcommand. Fully replaces any previous summary.
|
|
306
|
+
*
|
|
307
|
+
* @param {string} basePath - Repository root path
|
|
308
|
+
* @param {{depth1: string, depth3: string, depth10: string, inputHash: string}} summary
|
|
309
|
+
* @returns {Promise<void>}
|
|
310
|
+
*/
|
|
311
|
+
async function applySummary(basePath, summary) {
|
|
312
|
+
if (!summary || !summary.depth1 || !summary.depth3 || !summary.depth10) {
|
|
313
|
+
throw new Error('applySummary requires {depth1, depth3, depth10, inputHash}');
|
|
314
|
+
}
|
|
315
|
+
const mapFile = getIntelMapPath(basePath);
|
|
316
|
+
if (!fs.existsSync(mapFile)) {
|
|
317
|
+
throw new Error('No repo-intel artifact for ' + basePath + '; run init first.');
|
|
318
|
+
}
|
|
319
|
+
await runAnalyzerWithStdin(
|
|
320
|
+
['repo-intel', 'set-summary', '--map-file', mapFile, '--input', '-'],
|
|
321
|
+
JSON.stringify(summary)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Check if agent-analyzer is available.
|
|
327
|
+
* @returns {Promise<{found: boolean, version?: string, tool: string}>}
|
|
328
|
+
*/
|
|
329
|
+
async function checkAstGrepInstalled() {
|
|
330
|
+
return installer.checkInstalled();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get install instructions.
|
|
335
|
+
* @returns {string}
|
|
336
|
+
*/
|
|
337
|
+
function getInstallInstructions() {
|
|
338
|
+
return installer.getInstallInstructions();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
module.exports = {
|
|
342
|
+
// Lifecycle (was lib/repo-map)
|
|
343
|
+
init,
|
|
344
|
+
update,
|
|
345
|
+
status,
|
|
346
|
+
load,
|
|
347
|
+
loadRaw,
|
|
348
|
+
exists,
|
|
349
|
+
applyDescriptors,
|
|
350
|
+
applySummary,
|
|
351
|
+
checkAstGrepInstalled,
|
|
352
|
+
getInstallInstructions,
|
|
353
|
+
|
|
354
|
+
// Typed query wrappers (read the cached repo-intel.json)
|
|
355
|
+
queries,
|
|
356
|
+
|
|
357
|
+
// Submodules for advanced use
|
|
358
|
+
installer,
|
|
359
|
+
cache,
|
|
360
|
+
updater,
|
|
361
|
+
converter
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Embedder (opt-in, separate agent-analyzer-embed binary). Lazy getter so
|
|
365
|
+
// `require('repo-intel')` for query/lifecycle use never loads the embed binary
|
|
366
|
+
// resolver or its download path — it only materializes when embed is accessed.
|
|
367
|
+
Object.defineProperty(module.exports, 'embed', {
|
|
368
|
+
enumerable: true,
|
|
369
|
+
get() { return require('./embed'); }
|
|
370
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* agent-analyzer binary availability check.
|
|
5
|
+
*
|
|
6
|
+
* Replaces the former ast-grep installer. The agent-analyzer binary is
|
|
7
|
+
* auto-downloaded by agent-core on first use - no manual install required.
|
|
8
|
+
*
|
|
9
|
+
* @module lib/repo-intel/installer
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const binary = require('../binary');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if agent-analyzer is available (async). Downloads if missing.
|
|
16
|
+
* @returns {Promise<{found: boolean, version?: string, tool: string}>}
|
|
17
|
+
*/
|
|
18
|
+
async function checkInstalled() {
|
|
19
|
+
if (binary.isAvailable()) {
|
|
20
|
+
return { found: true, version: binary.getVersion(), tool: 'agent-analyzer' };
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await binary.ensureBinary();
|
|
24
|
+
return { found: true, version: binary.getVersion(), tool: 'agent-analyzer' };
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return { found: false, error: e.message, tool: 'agent-analyzer' };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if agent-analyzer is available (sync). Downloads if missing.
|
|
32
|
+
* @returns {{found: boolean, version?: string, tool: string}}
|
|
33
|
+
*/
|
|
34
|
+
function checkInstalledSync() {
|
|
35
|
+
if (binary.isAvailable()) {
|
|
36
|
+
return { found: true, version: binary.getVersion(), tool: 'agent-analyzer' };
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
binary.ensureBinarySync();
|
|
40
|
+
return { found: true, version: binary.getVersion(), tool: 'agent-analyzer' };
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { found: false, error: e.message, tool: 'agent-analyzer' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Version check is handled by the binary module - always true when found.
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
function meetsMinimumVersion() {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get install instructions (binary is auto-downloaded, but here for compat).
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function getInstallInstructions() {
|
|
59
|
+
return 'agent-analyzer is downloaded automatically on first use from https://github.com/agent-sh/agent-analyzer/releases';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get minimum version string.
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
function getMinimumVersion() {
|
|
67
|
+
return '0.3.0';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
checkInstalled,
|
|
72
|
+
checkInstalledSync,
|
|
73
|
+
meetsMinimumVersion,
|
|
74
|
+
getInstallInstructions,
|
|
75
|
+
getMinimumVersion,
|
|
76
|
+
// Stub: runner.js references this but is no longer the scan path
|
|
77
|
+
getCommand: () => null
|
|
78
|
+
};
|