Skip to content

Commit e6cfdcb

Browse files
author
agent-core-bot
committed
chore: sync core lib and CLAUDE.md from agent-core
1 parent abfdf8a commit e6cfdcb

15 files changed

Lines changed: 2095 additions & 274 deletions

File tree

lib/binary/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ const { promisify } = require('util');
4848

4949
const execFileAsync = promisify(cp.execFile);
5050

51+
// repo-intel artifacts grow with history: a mature repo's JSON can exceed 20 MB
52+
// (agnix measured ~21 MB). Node's execFile default maxBuffer is 1 MB, which
53+
// silently fails init/update/query on any real repo with "stdout maxBuffer length
54+
// exceeded". Cap generously; callers can override via options.maxBuffer.
55+
const ANALYZER_MAX_BUFFER = 256 * 1024 * 1024;
56+
5157
const { ANALYZER_MIN_VERSION, BINARY_NAME, GITHUB_REPO } = require('./version');
5258

5359
const PLATFORM_MAP = {
@@ -957,7 +963,7 @@ function ensureBinarySync(options) {
957963
*/
958964
function runAnalyzer(args, options) {
959965
const binPath = ensureBinarySync();
960-
const opts = Object.assign({ encoding: 'utf8', windowsHide: true }, options);
966+
const opts = Object.assign({ encoding: 'utf8', windowsHide: true, maxBuffer: ANALYZER_MAX_BUFFER }, options);
961967
if (!opts.stdio) opts.stdio = ['pipe', 'pipe', 'pipe'];
962968
const result = cp.execFileSync(binPath, args, opts);
963969
return typeof result === 'string' ? result : result.toString('utf8');
@@ -971,7 +977,7 @@ function runAnalyzer(args, options) {
971977
*/
972978
async function runAnalyzerAsync(args, options) {
973979
const binPath = await ensureBinary();
974-
const opts = Object.assign({ encoding: 'utf8', windowsHide: true }, options);
980+
const opts = Object.assign({ encoding: 'utf8', windowsHide: true, maxBuffer: ANALYZER_MAX_BUFFER }, options);
975981
const result = await execFileAsync(binPath, args, opts);
976982
return result.stdout;
977983
}

lib/binary/shared-helpers.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
'use strict';
2+
3+
/**
4+
* Shared HTTP + archive helpers used by both binary resolvers
5+
* (`lib/binary/index.js` for `agent-analyzer`, `lib/embed/binary.js`
6+
* for `agent-analyzer-embed`).
7+
*
8+
* Extracted to keep the two resolvers from drifting on HTTP redirect
9+
* handling, GitHub auth, and archive extraction details — a single
10+
* fix to e.g. the timeout policy or the redirect cap lands once and
11+
* applies to both binaries.
12+
*
13+
* @module lib/binary/shared-helpers
14+
*/
15+
16+
const fs = require('fs');
17+
const path = require('path');
18+
const os = require('os');
19+
const https = require('https');
20+
const cp = require('child_process');
21+
22+
const DEFAULT_DOWNLOAD_TIMEOUT_MS = 30000;
23+
const MAX_REDIRECTS = 5;
24+
25+
/**
26+
* Fetch a URL into an in-memory Buffer following up to 5 redirects.
27+
*
28+
* Honors `GITHUB_TOKEN` / `GH_TOKEN` for authenticated requests
29+
* (raises rate limit, lets private-repo asset URLs work). Stalled
30+
* connections are killed by the per-request timeout — without this
31+
* a stuck socket would hang the process indefinitely.
32+
*
33+
* @param {string} url
34+
* @param {Object} [options]
35+
* @param {string} [options.userAgent='agent-sh/binary-resolver']
36+
* @param {number} [options.timeoutMs=30000] - per-request timeout
37+
* @returns {Promise<Buffer>}
38+
*/
39+
function downloadToBuffer(url, options) {
40+
const opts = options || {};
41+
const userAgent = opts.userAgent || 'agent-sh/binary-resolver';
42+
const timeoutMs = opts.timeoutMs || DEFAULT_DOWNLOAD_TIMEOUT_MS;
43+
44+
return new Promise(function (resolve, reject) {
45+
const ghToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
46+
47+
function request(reqUrl, redirectCount) {
48+
if (redirectCount > MAX_REDIRECTS) {
49+
reject(new Error('Too many redirects fetching from ' + url));
50+
return;
51+
}
52+
const headers = {
53+
'User-Agent': userAgent,
54+
'Accept': 'application/octet-stream'
55+
};
56+
if (ghToken) headers['Authorization'] = 'Bearer ' + ghToken;
57+
58+
const req = https.get(reqUrl, { headers: headers, timeout: timeoutMs }, function (res) {
59+
const sc = res.statusCode;
60+
if (sc === 301 || sc === 302 || sc === 307 || sc === 308) {
61+
res.resume();
62+
var loc = res.headers.location;
63+
if (loc && !loc.startsWith('https://')) {
64+
reject(new Error('Refusing non-HTTPS redirect to ' + loc));
65+
return;
66+
}
67+
request(loc, redirectCount + 1);
68+
return;
69+
}
70+
if (sc !== 200) {
71+
res.resume();
72+
const hint = sc === 403 ? ' (rate limited - set GITHUB_TOKEN env var)' : '';
73+
reject(new Error('HTTP ' + sc + hint + ' fetching ' + reqUrl));
74+
return;
75+
}
76+
const chunks = [];
77+
res.on('data', function (chunk) { chunks.push(chunk); });
78+
res.on('end', function () { resolve(Buffer.concat(chunks)); });
79+
res.on('error', reject);
80+
});
81+
req.on('error', reject);
82+
req.on('timeout', function () {
83+
req.destroy();
84+
reject(new Error('Timeout (' + timeoutMs + 'ms) fetching ' + reqUrl));
85+
});
86+
}
87+
88+
request(url, 0);
89+
});
90+
}
91+
92+
/**
93+
* Extract a `.tar.gz` Buffer into `destDir` using the system `tar`.
94+
* Available on Linux, macOS, and Windows (built into recent Win10/11).
95+
*
96+
* @param {Buffer} buf
97+
* @param {string} destDir
98+
* @returns {Promise<void>}
99+
*/
100+
function extractTarGz(buf, destDir) {
101+
return new Promise(function (resolve, reject) {
102+
const tarDest = process.platform === 'win32' ? destDir.replace(/\\/g, '/') : destDir;
103+
const tar = cp.spawn('tar', ['xz', '-C', tarDest], {
104+
stdio: ['pipe', 'pipe', 'pipe']
105+
});
106+
let stderr = '';
107+
tar.stderr.on('data', function (d) { stderr += d; });
108+
tar.stdin.write(buf);
109+
tar.stdin.end();
110+
tar.on('close', function (code) {
111+
if (code !== 0) {
112+
reject(new Error('tar extraction failed (code ' + code + '): ' + stderr));
113+
} else {
114+
resolve();
115+
}
116+
});
117+
tar.on('error', reject);
118+
});
119+
}
120+
121+
/**
122+
* Extract a `.zip` Buffer into `destDir` using PowerShell's
123+
* `Expand-Archive` (Windows-only).
124+
*
125+
* @param {Buffer} buf
126+
* @param {string} destDir
127+
* @param {string} binaryName - used as the temp-dir prefix
128+
* @returns {Promise<void>}
129+
*/
130+
function extractZip(buf, destDir, binaryName) {
131+
return new Promise(function (resolve, reject) {
132+
var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), binaryName + '-'));
133+
var tmpZip = path.join(tmpDir, 'archive.zip');
134+
fs.writeFileSync(tmpZip, buf);
135+
var ps = cp.spawn(
136+
'powershell',
137+
['-NoProfile', '-NonInteractive', '-Command',
138+
'Expand-Archive', '-Path', tmpZip, '-DestinationPath', destDir, '-Force'],
139+
{ stdio: ['ignore', 'pipe', 'pipe'] }
140+
);
141+
var stderr = '';
142+
ps.stderr.on('data', function (d) { stderr += d; });
143+
ps.on('close', function (code) {
144+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) { /* ignore */ }
145+
if (code !== 0) {
146+
reject(new Error('zip extraction failed (code ' + code + '): ' + stderr));
147+
} else {
148+
resolve();
149+
}
150+
});
151+
ps.on('error', reject);
152+
});
153+
}
154+
155+
module.exports = {
156+
downloadToBuffer,
157+
extractTarGz,
158+
extractZip,
159+
DEFAULT_DOWNLOAD_TIMEOUT_MS
160+
};

lib/enhance/skill-patterns.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,7 @@ const skillPatterns = {
7676
(content && p.test(content));
7777
});
7878

79-
const disableModelInvocation = frontmatter['disable-model-invocation'];
80-
const isManualOnly = disableModelInvocation === true ||
81-
(typeof disableModelInvocation === 'string' && disableModelInvocation.toLowerCase() === 'true');
82-
83-
if (hasSideEffects && !isManualOnly) {
79+
if (hasSideEffects && frontmatter['disable-model-invocation'] !== true) {
8480
return {
8581
issue: 'Skill with side effects should have disable-model-invocation: true',
8682
fix: 'Add "disable-model-invocation: true" to frontmatter for manual-only invocation'

lib/repo-intel/cache.js

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)