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,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Binary resolver for `agent-analyzer-embed`.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the structure of `lib/binary/index.js` but for the separate
|
|
7
|
+
* embedder binary. Kept as its own module rather than parameterizing
|
|
8
|
+
* the existing resolver — the current single-binary helper is heavily
|
|
9
|
+
* specialized and a refactor would touch every call site for one
|
|
10
|
+
* use case.
|
|
11
|
+
*
|
|
12
|
+
* Both binaries share:
|
|
13
|
+
* - the same install dir (`~/.agent-sh/bin/`)
|
|
14
|
+
* - the same release-tag-aware download (latest tag with TTL cache)
|
|
15
|
+
* - the same platform map
|
|
16
|
+
*
|
|
17
|
+
* They differ in:
|
|
18
|
+
* - binary name (`agent-analyzer-embed` here)
|
|
19
|
+
* - GitHub release asset path uses the embed binary name
|
|
20
|
+
*
|
|
21
|
+
* @module lib/embed/binary
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const https = require('https');
|
|
28
|
+
const cp = require('child_process');
|
|
29
|
+
|
|
30
|
+
// Reuse PLATFORM_MAP from the main resolver to avoid drift if a new
|
|
31
|
+
// platform is added.
|
|
32
|
+
const mainBinary = require('../../binary');
|
|
33
|
+
// Reuse HTTP + archive helpers so a single bug fix to e.g. timeout
|
|
34
|
+
// behavior or redirect handling lands once and applies to both
|
|
35
|
+
// binaries (`agent-analyzer` and `agent-analyzer-embed`).
|
|
36
|
+
const sharedHelpers = require('../../binary/shared-helpers');
|
|
37
|
+
|
|
38
|
+
const EMBED_BINARY_NAME = 'agent-analyzer-embed';
|
|
39
|
+
const EMBED_GITHUB_REPO = 'agent-sh/agent-analyzer';
|
|
40
|
+
const LATEST_VERSION_TTL_MS = 60 * 60 * 1000;
|
|
41
|
+
const PLATFORM_MAP = mainBinary.PLATFORM_MAP;
|
|
42
|
+
|
|
43
|
+
function getBinaryPath() {
|
|
44
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
45
|
+
return path.join(os.homedir(), '.agent-sh', 'bin', EMBED_BINARY_NAME + ext);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Platform-specific ONNX Runtime dylib bundled beside the embed binary in the
|
|
49
|
+
// release tarball (see release.yml). The Rust binary's runtime resolver
|
|
50
|
+
// (resolve_and_preflight_ort) looks for exactly this name next to the
|
|
51
|
+
// executable. Mirrored here so the JS side can tell whether an existing
|
|
52
|
+
// install predates the bundling and needs a re-download.
|
|
53
|
+
function getBundledOrtName() {
|
|
54
|
+
if (process.platform === 'win32') return 'onnxruntime.dll';
|
|
55
|
+
if (process.platform === 'darwin') return 'libonnxruntime.dylib';
|
|
56
|
+
return 'libonnxruntime.so';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getBundledOrtPath() {
|
|
60
|
+
return path.join(path.dirname(getBinaryPath()), getBundledOrtName());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// musl has no bundled ORT (no MS musl build; a glibc dylib cannot dlopen under
|
|
64
|
+
// musl). On musl we never expect a sibling lib, so its absence must not trigger
|
|
65
|
+
// a perpetual re-download loop.
|
|
66
|
+
function platformBundlesOrt() {
|
|
67
|
+
const key = getPlatformKey();
|
|
68
|
+
return !!key && !key.includes('musl');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getPlatformKey() {
|
|
72
|
+
const key = process.platform + '-' + process.arch;
|
|
73
|
+
return PLATFORM_MAP[key] || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getVersion() {
|
|
77
|
+
const binPath = getBinaryPath();
|
|
78
|
+
if (!fs.existsSync(binPath)) return null;
|
|
79
|
+
try {
|
|
80
|
+
const out = cp.execFileSync(binPath, ['--version'], {
|
|
81
|
+
timeout: 5000,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
84
|
+
windowsHide: true
|
|
85
|
+
});
|
|
86
|
+
const match = out.trim().match(/(\d+\.\d+\.\d+)/);
|
|
87
|
+
return match ? match[1] : out.trim();
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isAvailable() {
|
|
94
|
+
return fs.existsSync(getBinaryPath());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let _latestVersionCache = null;
|
|
98
|
+
|
|
99
|
+
async function getLatestReleaseVersion() {
|
|
100
|
+
if (
|
|
101
|
+
_latestVersionCache &&
|
|
102
|
+
Date.now() - _latestVersionCache.fetchedAt < LATEST_VERSION_TTL_MS
|
|
103
|
+
) {
|
|
104
|
+
return _latestVersionCache.version;
|
|
105
|
+
}
|
|
106
|
+
return new Promise(function (resolve, reject) {
|
|
107
|
+
const ghToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
108
|
+
const headers = {
|
|
109
|
+
'User-Agent': 'agent-sh/embed-resolver',
|
|
110
|
+
'Accept': 'application/vnd.github+json'
|
|
111
|
+
};
|
|
112
|
+
if (ghToken) headers['Authorization'] = 'Bearer ' + ghToken;
|
|
113
|
+
|
|
114
|
+
const url = 'https://api.github.com/repos/' + EMBED_GITHUB_REPO + '/releases/latest';
|
|
115
|
+
const fail = function (msg) {
|
|
116
|
+
reject(new Error(msg + ' fetching ' + url));
|
|
117
|
+
};
|
|
118
|
+
const req = https.get(url, { headers: headers, timeout: 5000 }, function (res) {
|
|
119
|
+
if (res.statusCode !== 200) {
|
|
120
|
+
res.resume();
|
|
121
|
+
fail('HTTP ' + res.statusCode);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const chunks = [];
|
|
125
|
+
res.on('data', function (chunk) { chunks.push(chunk); });
|
|
126
|
+
res.on('end', function () {
|
|
127
|
+
try {
|
|
128
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
129
|
+
const tag = (body && body.tag_name) || '';
|
|
130
|
+
const version = tag.replace(/^v/, '');
|
|
131
|
+
if (/^\d+\.\d+\.\d+/.test(version)) {
|
|
132
|
+
_latestVersionCache = { version: version, fetchedAt: Date.now() };
|
|
133
|
+
resolve(version);
|
|
134
|
+
} else {
|
|
135
|
+
fail('No valid release tag');
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
fail('Failed to parse release JSON: ' + e.message);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
res.on('error', function (e) { fail(e.message); });
|
|
142
|
+
});
|
|
143
|
+
req.on('error', function (e) { fail(e.message); });
|
|
144
|
+
req.on('timeout', function () { req.destroy(); fail('Timeout'); });
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildDownloadUrl(ver, platformKey) {
|
|
149
|
+
const ext = process.platform === 'win32' ? '.zip' : '.tar.gz';
|
|
150
|
+
return (
|
|
151
|
+
'https://github.com/' +
|
|
152
|
+
EMBED_GITHUB_REPO +
|
|
153
|
+
'/releases/download/v' +
|
|
154
|
+
ver +
|
|
155
|
+
'/' +
|
|
156
|
+
EMBED_BINARY_NAME +
|
|
157
|
+
'-' +
|
|
158
|
+
platformKey +
|
|
159
|
+
ext
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Per-call shim so the embed resolver passes its own User-Agent
|
|
164
|
+
// header without duplicating the rest of the HTTP plumbing.
|
|
165
|
+
function downloadToBuffer(url) {
|
|
166
|
+
return sharedHelpers.downloadToBuffer(url, { userAgent: 'agent-sh/embed-resolver' });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const extractTarGz = sharedHelpers.extractTarGz;
|
|
170
|
+
const extractZip = sharedHelpers.extractZip;
|
|
171
|
+
|
|
172
|
+
async function downloadBinary(ver) {
|
|
173
|
+
const platformKey = getPlatformKey();
|
|
174
|
+
if (!platformKey) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
'Unsupported platform: ' + process.platform + '-' + process.arch + '. ' +
|
|
177
|
+
'Supported: ' + Object.keys(PLATFORM_MAP).join(', ')
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const url = buildDownloadUrl(ver, platformKey);
|
|
181
|
+
process.stderr.write('Downloading ' + EMBED_BINARY_NAME + ' v' + ver + ' for ' + platformKey + '...\n');
|
|
182
|
+
|
|
183
|
+
const binPath = getBinaryPath();
|
|
184
|
+
const binDir = path.dirname(binPath);
|
|
185
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
186
|
+
|
|
187
|
+
let buf;
|
|
188
|
+
try {
|
|
189
|
+
buf = await downloadToBuffer(url);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
'Failed to download ' + EMBED_BINARY_NAME + ':\n' +
|
|
193
|
+
' URL: ' + url + '\n' +
|
|
194
|
+
' Error: ' + err.message + '\n\n' +
|
|
195
|
+
'To install manually:\n' +
|
|
196
|
+
' 1. Download: ' + url + '\n' +
|
|
197
|
+
' 2. Extract the binary to: ' + binDir + '\n' +
|
|
198
|
+
' 3. Ensure it is named: ' + path.basename(binPath)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (process.platform === 'win32') {
|
|
203
|
+
await extractZip(buf, binDir, path.basename(binPath));
|
|
204
|
+
} else {
|
|
205
|
+
await extractTarGz(buf, binDir);
|
|
206
|
+
}
|
|
207
|
+
if (process.platform !== 'win32') {
|
|
208
|
+
fs.chmodSync(binPath, 0o755);
|
|
209
|
+
}
|
|
210
|
+
return binPath;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function ensureBinary(options) {
|
|
214
|
+
const opts = options || {};
|
|
215
|
+
const binPath = getBinaryPath();
|
|
216
|
+
if (fs.existsSync(binPath)) {
|
|
217
|
+
// An install from before ORT bundling has the binary but no sibling
|
|
218
|
+
// dylib; without it the embedder fails at runtime. Re-download once to
|
|
219
|
+
// pick up the bundled lib. Skip on musl (never bundled) so this can't loop.
|
|
220
|
+
if (platformBundlesOrt() && !fs.existsSync(getBundledOrtPath())) {
|
|
221
|
+
const targetVer = opts.version || (await getLatestReleaseVersion());
|
|
222
|
+
return downloadBinary(targetVer);
|
|
223
|
+
}
|
|
224
|
+
return binPath;
|
|
225
|
+
}
|
|
226
|
+
const targetVer = opts.version || (await getLatestReleaseVersion());
|
|
227
|
+
return downloadBinary(targetVer);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
EMBED_BINARY_NAME,
|
|
232
|
+
getBinaryPath,
|
|
233
|
+
getBundledOrtName,
|
|
234
|
+
getBundledOrtPath,
|
|
235
|
+
platformBundlesOrt,
|
|
236
|
+
getVersion,
|
|
237
|
+
getPlatformKey,
|
|
238
|
+
getLatestReleaseVersion,
|
|
239
|
+
isAvailable,
|
|
240
|
+
ensureBinary,
|
|
241
|
+
buildDownloadUrl
|
|
242
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public surface for the embed module — preference cache, binary
|
|
5
|
+
* resolver, and high-level orchestrator (scan / update / status).
|
|
6
|
+
*
|
|
7
|
+
* Skill code should import from here, not the internal modules,
|
|
8
|
+
* so the wiring stays swappable.
|
|
9
|
+
*
|
|
10
|
+
* @module lib/embed
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const preference = require('./preference');
|
|
14
|
+
const binary = require('./binary');
|
|
15
|
+
const orchestrator = require('./orchestrator');
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
preference,
|
|
19
|
+
binary,
|
|
20
|
+
orchestrator,
|
|
21
|
+
// Convenience re-exports for the common cases.
|
|
22
|
+
isEnabled: orchestrator.isEnabled,
|
|
23
|
+
runScan: orchestrator.runScan,
|
|
24
|
+
runUpdate: orchestrator.runUpdate,
|
|
25
|
+
status: orchestrator.status
|
|
26
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* High-level embed orchestration. Glues together:
|
|
5
|
+
*
|
|
6
|
+
* user preference → binary download → scan/update → set-embeddings
|
|
7
|
+
*
|
|
8
|
+
* Called from the `/repo-intel enrich` command after the existing
|
|
9
|
+
* weighter and summarizer Haiku agents finish; degrades to a no-op
|
|
10
|
+
* when the user has chosen `embedder: "none"`.
|
|
11
|
+
*
|
|
12
|
+
* Also exposes `runUpdate()` for the standalone `/repo-intel embed
|
|
13
|
+
* update` action group (and the `npx ... embed update` CI hook).
|
|
14
|
+
*
|
|
15
|
+
* @module lib/embed/orchestrator
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const cp = require('child_process');
|
|
21
|
+
|
|
22
|
+
const preference = require('./preference');
|
|
23
|
+
const embedBinary = require('./binary');
|
|
24
|
+
const mainBinary = require('../../binary');
|
|
25
|
+
const cache = require('../cache');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Should the orchestrator run for this repo? Returns false when the
|
|
29
|
+
* user has not opted in (preference unset or 'none'), which lets
|
|
30
|
+
* callers safely no-op without wrapping every call site in a guard.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} cwd
|
|
33
|
+
* @returns {boolean}
|
|
34
|
+
*/
|
|
35
|
+
function isEnabled(cwd) {
|
|
36
|
+
const pref = preference.read(cwd);
|
|
37
|
+
return pref.embedder === 'small' || pref.embedder === 'big';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run a full scan: ensures the embed binary is downloaded, runs the
|
|
42
|
+
* scan subcommand, pipes the JSON document into `agent-analyzer
|
|
43
|
+
* repo-intel set-embeddings`. Returns a small status object so callers
|
|
44
|
+
* can report what happened.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} cwd
|
|
47
|
+
* @returns {Promise<{ran: boolean, files?: number, durationMs?: number, reason?: string}>}
|
|
48
|
+
*/
|
|
49
|
+
async function runScan(cwd) {
|
|
50
|
+
if (!isEnabled(cwd)) {
|
|
51
|
+
return { ran: false, reason: 'embedder preference is "none" or unset' };
|
|
52
|
+
}
|
|
53
|
+
const pref = preference.read(cwd);
|
|
54
|
+
const detail = preference.detailToCliArg(pref.embedderDetail || 'balanced');
|
|
55
|
+
|
|
56
|
+
const mapFile = cache.getPath(cwd);
|
|
57
|
+
if (!fs.existsSync(mapFile)) {
|
|
58
|
+
return { ran: false, reason: 'no repo-intel map found; run `/repo-intel init` first' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const start = Date.now();
|
|
62
|
+
const embedBin = await embedBinary.ensureBinary();
|
|
63
|
+
const mainBin = await mainBinary.ensureBinary();
|
|
64
|
+
|
|
65
|
+
const result = await streamEmbedToSetEmbeddings(
|
|
66
|
+
embedBin,
|
|
67
|
+
['scan', cwd, '--variant', pref.embedder, '--detail', detail],
|
|
68
|
+
mainBin,
|
|
69
|
+
mapFile
|
|
70
|
+
);
|
|
71
|
+
return Object.assign({ ran: true, durationMs: Date.now() - start }, result);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Run a delta update: only re-embeds files whose content hash differs
|
|
76
|
+
* from the existing sidecar. Falls back to a full scan when no
|
|
77
|
+
* sidecar exists yet.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} cwd
|
|
80
|
+
* @returns {Promise<{ran: boolean, files?: number, durationMs?: number, reason?: string}>}
|
|
81
|
+
*/
|
|
82
|
+
async function runUpdate(cwd) {
|
|
83
|
+
if (!isEnabled(cwd)) {
|
|
84
|
+
return { ran: false, reason: 'embedder preference is "none" or unset' };
|
|
85
|
+
}
|
|
86
|
+
const pref = preference.read(cwd);
|
|
87
|
+
const detail = preference.detailToCliArg(pref.embedderDetail || 'balanced');
|
|
88
|
+
|
|
89
|
+
const mapFile = cache.getPath(cwd);
|
|
90
|
+
if (!fs.existsSync(mapFile)) {
|
|
91
|
+
return { ran: false, reason: 'no repo-intel map; run `/repo-intel init` then `enrich`' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const start = Date.now();
|
|
95
|
+
const embedBin = await embedBinary.ensureBinary();
|
|
96
|
+
const mainBin = await mainBinary.ensureBinary();
|
|
97
|
+
|
|
98
|
+
const result = await streamEmbedToSetEmbeddings(
|
|
99
|
+
embedBin,
|
|
100
|
+
['update', cwd, '--map-file', mapFile, '--variant', pref.embedder, '--detail', detail],
|
|
101
|
+
mainBin,
|
|
102
|
+
mapFile
|
|
103
|
+
);
|
|
104
|
+
return Object.assign({ ran: true, durationMs: Date.now() - start }, result);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Status snapshot: which preference is set, whether the binary is
|
|
109
|
+
* installed, whether the bundled ONNX Runtime is present, whether the
|
|
110
|
+
* sidecar exists.
|
|
111
|
+
*
|
|
112
|
+
* `ortBundled` surfaces the prerequisite the embed binary needs at runtime:
|
|
113
|
+
* the binary loads libonnxruntime from beside itself (shipped in the release
|
|
114
|
+
* tarball). On a platform that bundles ORT, a present binary with a missing
|
|
115
|
+
* lib means an upgrade re-download is pending - ensureBinary handles it, but
|
|
116
|
+
* status reports it so callers can explain a one-time re-fetch.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} cwd
|
|
119
|
+
* @returns {{enabled: boolean, embedder?: string, embedderDetail?: string, binaryInstalled: boolean, ortBundled: boolean, sidecarExists: boolean, sidecarPath?: string}}
|
|
120
|
+
*/
|
|
121
|
+
function status(cwd) {
|
|
122
|
+
const pref = preference.read(cwd);
|
|
123
|
+
const mapFile = cache.getPath(cwd);
|
|
124
|
+
const sidecarPath = deriveSidecarPath(mapFile);
|
|
125
|
+
return {
|
|
126
|
+
enabled: isEnabled(cwd),
|
|
127
|
+
embedder: pref.embedder,
|
|
128
|
+
embedderDetail: pref.embedderDetail,
|
|
129
|
+
binaryInstalled: embedBinary.isAvailable(),
|
|
130
|
+
ortBundled: !embedBinary.platformBundlesOrt() || fs.existsSync(embedBinary.getBundledOrtPath()),
|
|
131
|
+
sidecarExists: fs.existsSync(sidecarPath),
|
|
132
|
+
sidecarPath: sidecarPath
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Stream the embed binary's stdout directly into the main binary's
|
|
138
|
+
* `set-embeddings --input -` stdin. The intermediate JSON document
|
|
139
|
+
* can run into the megabytes for big repos at high detail; piping
|
|
140
|
+
* keeps memory flat instead of buffering the whole document.
|
|
141
|
+
*
|
|
142
|
+
* The promise resolves with `{ files }` when both children exit cleanly;
|
|
143
|
+
* rejects with the failing child's exit code + captured stderr otherwise.
|
|
144
|
+
*
|
|
145
|
+
* Hardened against the failure modes a bare `pipe()` leaves open: a stream
|
|
146
|
+
* error (e.g. set-embeddings dies mid-write) used to leave the promise
|
|
147
|
+
* unsettled forever and leak the surviving process. Now any failure path
|
|
148
|
+
* — spawn error, stream error, or non-zero exit — kills the sibling and
|
|
149
|
+
* rejects once. Embed stderr is captured (not inherited) so the error
|
|
150
|
+
* message carries the diagnostic instead of just an exit code.
|
|
151
|
+
*/
|
|
152
|
+
function streamEmbedToSetEmbeddings(embedBinPath, embedArgs, mainBinPath, mapFile) {
|
|
153
|
+
return new Promise(function (resolve, reject) {
|
|
154
|
+
const embedChild = cp.spawn(embedBinPath, embedArgs, {
|
|
155
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
156
|
+
windowsHide: true
|
|
157
|
+
});
|
|
158
|
+
const setChild = cp.spawn(
|
|
159
|
+
mainBinPath,
|
|
160
|
+
['repo-intel', 'set-embeddings', '--map-file', mapFile, '--input', '-'],
|
|
161
|
+
{ stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
let embedExit = null;
|
|
165
|
+
let setExit = null;
|
|
166
|
+
let settled = false;
|
|
167
|
+
let setStdout = '';
|
|
168
|
+
let embedStderr = '';
|
|
169
|
+
let setStderr = '';
|
|
170
|
+
|
|
171
|
+
// Single exit path. Kills the sibling on any failure so neither process
|
|
172
|
+
// is left running against a closed pipe (FD leak / zombie).
|
|
173
|
+
function done(err, value) {
|
|
174
|
+
if (settled) return;
|
|
175
|
+
settled = true;
|
|
176
|
+
if (err) {
|
|
177
|
+
try { embedChild.kill('SIGTERM'); } catch (e) { /* already gone */ }
|
|
178
|
+
try { setChild.kill('SIGTERM'); } catch (e) { /* already gone */ }
|
|
179
|
+
reject(err);
|
|
180
|
+
} else {
|
|
181
|
+
resolve(value);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function maybeFinish() {
|
|
186
|
+
if (settled || embedExit === null || setExit === null) return;
|
|
187
|
+
if (embedExit !== 0) {
|
|
188
|
+
return done(new Error(
|
|
189
|
+
embedBinary.EMBED_BINARY_NAME + ' exited ' + embedExit +
|
|
190
|
+
(embedStderr.trim() ? ': ' + embedStderr.trim().slice(0, 500) : '')
|
|
191
|
+
));
|
|
192
|
+
}
|
|
193
|
+
if (setExit !== 0) {
|
|
194
|
+
return done(new Error(
|
|
195
|
+
'agent-analyzer set-embeddings exited ' + setExit +
|
|
196
|
+
(setStderr.trim() ? ': ' + setStderr.trim().slice(0, 500) : '')
|
|
197
|
+
));
|
|
198
|
+
}
|
|
199
|
+
const m = setStdout.match(/(\d+)\s+files?/);
|
|
200
|
+
done(null, { files: m ? parseInt(m[1], 10) : undefined });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// stderr piped (not inherited) so failures carry a message.
|
|
204
|
+
embedChild.stderr.on('data', function (d) { embedStderr += d.toString('utf8'); });
|
|
205
|
+
setChild.stderr.on('data', function (d) { setStderr += d.toString('utf8'); });
|
|
206
|
+
setChild.stdout.on('data', function (d) { setStdout += d.toString('utf8'); });
|
|
207
|
+
|
|
208
|
+
// Stream wiring with error handling on BOTH ends of the pipe — a bare
|
|
209
|
+
// .pipe() swallows these and hangs.
|
|
210
|
+
embedChild.stdout.on('error', function (e) { done(e); });
|
|
211
|
+
setChild.stdin.on('error', function (e) {
|
|
212
|
+
// EPIPE when set-embeddings has already exited is benign — its close
|
|
213
|
+
// handler reports the real cause. Only surface other stdin errors.
|
|
214
|
+
if (e && e.code !== 'EPIPE') done(e);
|
|
215
|
+
});
|
|
216
|
+
embedChild.stdout.pipe(setChild.stdin);
|
|
217
|
+
|
|
218
|
+
embedChild.on('error', function (e) { done(e); });
|
|
219
|
+
setChild.on('error', function (e) { done(e); });
|
|
220
|
+
embedChild.on('close', function (code) { embedExit = code; maybeFinish(); });
|
|
221
|
+
setChild.on('close', function (code) { setExit = code; maybeFinish(); });
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function deriveSidecarPath(mapFile) {
|
|
226
|
+
if (!mapFile) return '';
|
|
227
|
+
const dir = path.dirname(mapFile);
|
|
228
|
+
const stem = path.basename(mapFile, path.extname(mapFile));
|
|
229
|
+
return path.join(dir, stem + '.embeddings.bin');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
isEnabled,
|
|
234
|
+
runScan,
|
|
235
|
+
runUpdate,
|
|
236
|
+
status,
|
|
237
|
+
// exported for testing the dual-process pipe in isolation
|
|
238
|
+
streamEmbedToSetEmbeddings
|
|
239
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* User preference for the embedder. Persists the answer to two
|
|
5
|
+
* one-time prompts so subsequent enrich runs don't re-ask:
|
|
6
|
+
*
|
|
7
|
+
* embedder : "none" | "small" | "big"
|
|
8
|
+
* embedderDetail : "compact" | "balanced" | "maximum"
|
|
9
|
+
*
|
|
10
|
+
* Stored in `<stateDir>/sources/preference.json` alongside the existing
|
|
11
|
+
* task source preference (so the user has a single place to clear all
|
|
12
|
+
* cached choices via `/repo-intel embed reset`).
|
|
13
|
+
*
|
|
14
|
+
* @module lib/embed/preference
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const cache = require('../cache');
|
|
20
|
+
|
|
21
|
+
const VALID_EMBEDDER = ['none', 'small', 'big'];
|
|
22
|
+
const VALID_DETAIL = ['compact', 'balanced', 'maximum'];
|
|
23
|
+
|
|
24
|
+
// State-directory resolution delegates to `cache.getStateDirPath` so
|
|
25
|
+
// the embedder reads/writes the same directory the artifact loader
|
|
26
|
+
// uses. Reviewer-flagged split-brain risk: an inline copy here would
|
|
27
|
+
// drift on candidate order, env-var support, or other policy and
|
|
28
|
+
// silently land preferences in a different place than the map file.
|
|
29
|
+
function preferencePath(cwd) {
|
|
30
|
+
return path.join(cache.getStateDirPath(cwd), 'sources', 'preference.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load preference from disk. Returns an empty object when absent or
|
|
35
|
+
* unreadable so callers can treat "unset" uniformly.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} cwd
|
|
38
|
+
* @returns {{embedder?: string, embedderDetail?: string}}
|
|
39
|
+
*/
|
|
40
|
+
function read(cwd) {
|
|
41
|
+
const p = preferencePath(cwd);
|
|
42
|
+
if (!fs.existsSync(p)) return {};
|
|
43
|
+
try {
|
|
44
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
45
|
+
return raw && typeof raw === 'object' ? raw : {};
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Merge updates into the on-disk preference file. Creates the
|
|
53
|
+
* containing directory if needed.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} cwd
|
|
56
|
+
* @param {Object} patch fields to set/overwrite
|
|
57
|
+
* @returns {Object} the merged preference
|
|
58
|
+
*/
|
|
59
|
+
function update(cwd, patch) {
|
|
60
|
+
const current = read(cwd);
|
|
61
|
+
const next = Object.assign({}, current, patch || {});
|
|
62
|
+
const p = preferencePath(cwd);
|
|
63
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
64
|
+
fs.writeFileSync(p, JSON.stringify(next, null, 2));
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Strip embedder fields from preference. Used by `embed reset` to
|
|
70
|
+
* trigger fresh prompts on the next enrich.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} cwd
|
|
73
|
+
*/
|
|
74
|
+
function reset(cwd) {
|
|
75
|
+
const current = read(cwd);
|
|
76
|
+
delete current.embedder;
|
|
77
|
+
delete current.embedderDetail;
|
|
78
|
+
const p = preferencePath(cwd);
|
|
79
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
80
|
+
fs.writeFileSync(p, JSON.stringify(current, null, 2));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Did the user already answer the embedder install prompt?
|
|
85
|
+
*
|
|
86
|
+
* @param {string} cwd
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
function hasEmbedderChoice(cwd) {
|
|
90
|
+
const pref = read(cwd);
|
|
91
|
+
return VALID_EMBEDDER.includes(pref.embedder);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Did the user already answer the detail prompt? Only meaningful when
|
|
96
|
+
* embedder !== 'none'.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} cwd
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
function hasDetailChoice(cwd) {
|
|
102
|
+
const pref = read(cwd);
|
|
103
|
+
return VALID_DETAIL.includes(pref.embedderDetail);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Translate the user-facing detail label into the analyzer-embed CLI
|
|
108
|
+
* value. Centralizes the mapping so the CLI flag never appears as a
|
|
109
|
+
* string literal scattered across modules.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} detail
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function detailToCliArg(detail) {
|
|
115
|
+
switch (detail) {
|
|
116
|
+
case 'compact':
|
|
117
|
+
return 'compact';
|
|
118
|
+
case 'maximum':
|
|
119
|
+
return 'maximum';
|
|
120
|
+
case 'balanced':
|
|
121
|
+
default:
|
|
122
|
+
return 'balanced';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
read,
|
|
128
|
+
update,
|
|
129
|
+
reset,
|
|
130
|
+
hasEmbedderChoice,
|
|
131
|
+
hasDetailChoice,
|
|
132
|
+
detailToCliArg,
|
|
133
|
+
preferencePath,
|
|
134
|
+
VALID_EMBEDDER,
|
|
135
|
+
VALID_DETAIL
|
|
136
|
+
};
|