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
@@ -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
+ };