@cortexkit/aft-opencode 0.18.3 → 0.18.4
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/dist/index.d.ts.map +1 -1
- package/dist/index.js +161 -12
- package/dist/pool.d.ts +2 -0
- package/dist/pool.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +2 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/url-fetch.d.ts +16 -0
- package/dist/shared/url-fetch.d.ts.map +1 -1
- package/dist/tools/reading.d.ts +14 -0
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tui.js +35 -11
- package/dist/workflow-hints.d.ts +25 -0
- package/dist/workflow-hints.d.ts.map +1 -0
- package/package.json +7 -6
- package/src/shared/rpc-client.ts +38 -5
- package/src/shared/url-fetch.ts +53 -8
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAiKlD;;;;;;;;;;;;;;;;;;GAkBG;AACH,QAAA,MAAM,MAAM,EAAE,MAskBb,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -25108,6 +25108,9 @@ function isProcessAlive2(pid) {
|
|
|
25108
25108
|
}
|
|
25109
25109
|
}
|
|
25110
25110
|
|
|
25111
|
+
// src/pool.ts
|
|
25112
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
25113
|
+
|
|
25111
25114
|
// src/bridge.ts
|
|
25112
25115
|
import { spawn as spawn3 } from "child_process";
|
|
25113
25116
|
import { homedir as homedir7 } from "os";
|
|
@@ -25630,6 +25633,14 @@ class BridgePool {
|
|
|
25630
25633
|
}
|
|
25631
25634
|
return null;
|
|
25632
25635
|
}
|
|
25636
|
+
getActiveBridgeForRoot(projectRoot) {
|
|
25637
|
+
const key = normalizeKey(projectRoot);
|
|
25638
|
+
const entry = this.bridges.get(key);
|
|
25639
|
+
if (!entry?.bridge.isAlive())
|
|
25640
|
+
return null;
|
|
25641
|
+
entry.lastUsed = Date.now();
|
|
25642
|
+
return entry.bridge;
|
|
25643
|
+
}
|
|
25633
25644
|
getBridge(projectRoot) {
|
|
25634
25645
|
const key = normalizeKey(projectRoot);
|
|
25635
25646
|
const existing = this.bridges.get(key);
|
|
@@ -25689,7 +25700,12 @@ class BridgePool {
|
|
|
25689
25700
|
}
|
|
25690
25701
|
}
|
|
25691
25702
|
function normalizeKey(projectRoot) {
|
|
25692
|
-
|
|
25703
|
+
const stripped = projectRoot.replace(/[/\\]+$/, "");
|
|
25704
|
+
try {
|
|
25705
|
+
return realpathSync3(stripped);
|
|
25706
|
+
} catch {
|
|
25707
|
+
return stripped;
|
|
25708
|
+
}
|
|
25693
25709
|
}
|
|
25694
25710
|
|
|
25695
25711
|
// src/resolver.ts
|
|
@@ -26179,6 +26195,7 @@ import {
|
|
|
26179
26195
|
} from "fs";
|
|
26180
26196
|
import { isIP } from "net";
|
|
26181
26197
|
import { join as join17 } from "path";
|
|
26198
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
26182
26199
|
var MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
26183
26200
|
var CACHE_TTL_MS2 = 24 * 60 * 60 * 1000;
|
|
26184
26201
|
var FETCH_TIMEOUT_MS = 30000;
|
|
@@ -26270,7 +26287,7 @@ function isPrivateIp(address) {
|
|
|
26270
26287
|
}
|
|
26271
26288
|
return true;
|
|
26272
26289
|
}
|
|
26273
|
-
async function assertPublicUrl(url2, allowPrivate) {
|
|
26290
|
+
async function assertPublicUrl(url2, allowPrivate, dnsLookup = lookup) {
|
|
26274
26291
|
if (url2.protocol !== "http:" && url2.protocol !== "https:") {
|
|
26275
26292
|
throw new Error(`Only http:// and https:// URLs are supported, got: ${url2.protocol}`);
|
|
26276
26293
|
}
|
|
@@ -26281,14 +26298,27 @@ async function assertPublicUrl(url2, allowPrivate) {
|
|
|
26281
26298
|
if (isPrivateIp(hostname3)) {
|
|
26282
26299
|
throw new Error(`Blocked private URL host ${url2.hostname} (${hostname3})`);
|
|
26283
26300
|
}
|
|
26284
|
-
return;
|
|
26301
|
+
return hostname3;
|
|
26285
26302
|
}
|
|
26286
|
-
const addresses = await
|
|
26303
|
+
const addresses = await dnsLookup(hostname3, { all: true, verbatim: true });
|
|
26287
26304
|
for (const { address } of addresses) {
|
|
26288
26305
|
if (isPrivateIp(address)) {
|
|
26289
26306
|
throw new Error(`Blocked private URL host ${url2.hostname} (${address})`);
|
|
26290
26307
|
}
|
|
26291
26308
|
}
|
|
26309
|
+
if (addresses.length === 0) {
|
|
26310
|
+
throw new Error(`Failed to resolve URL host ${url2.hostname}`);
|
|
26311
|
+
}
|
|
26312
|
+
return addresses[0].address;
|
|
26313
|
+
}
|
|
26314
|
+
function createPinnedDispatcher(validatedIp) {
|
|
26315
|
+
return new Agent({
|
|
26316
|
+
connect: {
|
|
26317
|
+
lookup: (_hostname, _opts, callback) => {
|
|
26318
|
+
callback(null, validatedIp, validatedIp.includes(":") ? 6 : 4);
|
|
26319
|
+
}
|
|
26320
|
+
}
|
|
26321
|
+
});
|
|
26292
26322
|
}
|
|
26293
26323
|
function resolveRedirectUrl(currentUrl, location) {
|
|
26294
26324
|
if (!location) {
|
|
@@ -26296,17 +26326,21 @@ function resolveRedirectUrl(currentUrl, location) {
|
|
|
26296
26326
|
}
|
|
26297
26327
|
return new URL(location, currentUrl);
|
|
26298
26328
|
}
|
|
26299
|
-
async function fetchWithRedirects(startUrl, allowPrivate) {
|
|
26329
|
+
async function fetchWithRedirects(startUrl, allowPrivate, deps = {}) {
|
|
26300
26330
|
let currentUrl = startUrl;
|
|
26331
|
+
const fetchImpl = deps.fetchImpl ?? undiciFetch;
|
|
26332
|
+
const dnsLookup = deps.lookup ?? lookup;
|
|
26301
26333
|
for (let redirectCount = 0;redirectCount <= MAX_REDIRECTS; redirectCount++) {
|
|
26302
|
-
await assertPublicUrl(currentUrl, allowPrivate);
|
|
26334
|
+
const validatedIp = await assertPublicUrl(currentUrl, allowPrivate, dnsLookup);
|
|
26335
|
+
const dispatcher = validatedIp ? (deps.dispatcherFactory ?? createPinnedDispatcher)(validatedIp) : undefined;
|
|
26303
26336
|
const controller = new AbortController;
|
|
26304
26337
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
26305
26338
|
let response;
|
|
26306
26339
|
try {
|
|
26307
|
-
response = await
|
|
26340
|
+
response = await fetchImpl(currentUrl.href, {
|
|
26308
26341
|
signal: controller.signal,
|
|
26309
26342
|
redirect: "manual",
|
|
26343
|
+
dispatcher,
|
|
26310
26344
|
headers: {
|
|
26311
26345
|
"user-agent": "aft-opencode-plugin",
|
|
26312
26346
|
accept: "application/vnd.github.raw, text/markdown, text/x-markdown, text/html;q=0.9, text/plain;q=0.5"
|
|
@@ -26367,7 +26401,7 @@ async function fetchUrlToTempFile(url2, storageDir, options = {}) {
|
|
|
26367
26401
|
} catch {}
|
|
26368
26402
|
}
|
|
26369
26403
|
log(`Fetching URL: ${url2}`);
|
|
26370
|
-
const response = await fetchWithRedirects(parsed, allowPrivate);
|
|
26404
|
+
const response = await fetchWithRedirects(parsed, allowPrivate, options);
|
|
26371
26405
|
if (!response.ok) {
|
|
26372
26406
|
throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${url2}`);
|
|
26373
26407
|
}
|
|
@@ -28742,9 +28776,12 @@ Provide exactly ONE of 'filePath' or 'url'. Pass either 'symbol' for a single lo
|
|
|
28742
28776
|
const params2 = { file: file2, symbol: sym };
|
|
28743
28777
|
if (args.contextLines !== undefined)
|
|
28744
28778
|
params2.context_lines = args.contextLines;
|
|
28745
|
-
return callBridge(ctx, context, "zoom", params2)
|
|
28779
|
+
return callBridge(ctx, context, "zoom", params2).catch((err) => ({
|
|
28780
|
+
success: false,
|
|
28781
|
+
message: err instanceof Error ? err.message : String(err)
|
|
28782
|
+
}));
|
|
28746
28783
|
}));
|
|
28747
|
-
return JSON.stringify(results);
|
|
28784
|
+
return JSON.stringify(formatZoomBatchResult(args.symbols, results), null, 2);
|
|
28748
28785
|
}
|
|
28749
28786
|
const params = { file: file2 };
|
|
28750
28787
|
if (typeof args.symbol === "string")
|
|
@@ -28760,6 +28797,40 @@ Provide exactly ONE of 'filePath' or 'url'. Pass either 'symbol' for a single lo
|
|
|
28760
28797
|
}
|
|
28761
28798
|
};
|
|
28762
28799
|
}
|
|
28800
|
+
function formatZoomBatchResult(symbols, responses) {
|
|
28801
|
+
const entries = symbols.map((name, index) => {
|
|
28802
|
+
const response = responses[index] ?? { success: false, message: "missing zoom response" };
|
|
28803
|
+
if (response.success === false) {
|
|
28804
|
+
const message = typeof response.message === "string" && response.message.length > 0 ? response.message : "zoom failed";
|
|
28805
|
+
return { name, success: false, error: message };
|
|
28806
|
+
}
|
|
28807
|
+
return { name, success: true, content: zoomResponseContent(response) };
|
|
28808
|
+
});
|
|
28809
|
+
const complete = entries.every((entry) => entry.success);
|
|
28810
|
+
const lines = [];
|
|
28811
|
+
if (!complete) {
|
|
28812
|
+
lines.push("Incomplete zoom results: one or more symbols failed.");
|
|
28813
|
+
}
|
|
28814
|
+
for (const entry of entries) {
|
|
28815
|
+
if (entry.success) {
|
|
28816
|
+
lines.push(`Symbol "${entry.name}":
|
|
28817
|
+
${entry.content ?? ""}`.trimEnd());
|
|
28818
|
+
} else {
|
|
28819
|
+
lines.push(`Symbol "${entry.name}" not found: ${entry.error ?? "zoom failed"}`);
|
|
28820
|
+
}
|
|
28821
|
+
}
|
|
28822
|
+
return { complete, symbols: entries, text: lines.join(`
|
|
28823
|
+
|
|
28824
|
+
`) };
|
|
28825
|
+
}
|
|
28826
|
+
function zoomResponseContent(response) {
|
|
28827
|
+
if (typeof response.content === "string")
|
|
28828
|
+
return response.content;
|
|
28829
|
+
if (typeof response.text === "string")
|
|
28830
|
+
return response.text;
|
|
28831
|
+
const { success: _success2, ...rest } = response;
|
|
28832
|
+
return JSON.stringify(rest, null, 2);
|
|
28833
|
+
}
|
|
28763
28834
|
var SKIP_DIRS = new Set([
|
|
28764
28835
|
"node_modules",
|
|
28765
28836
|
".git",
|
|
@@ -29322,6 +29393,58 @@ function structureTools(ctx) {
|
|
|
29322
29393
|
};
|
|
29323
29394
|
}
|
|
29324
29395
|
|
|
29396
|
+
// src/workflow-hints.ts
|
|
29397
|
+
var HEADING = "## Prefer AFT tools for token efficiency";
|
|
29398
|
+
function buildWorkflowHints(opts) {
|
|
29399
|
+
const sections = [];
|
|
29400
|
+
const grepName = opts.hoistBuiltins ? "grep" : "aft_grep";
|
|
29401
|
+
const bashName = opts.hoistBuiltins ? "bash" : "aft_bash";
|
|
29402
|
+
const bashStatusName = "bash_status";
|
|
29403
|
+
const hasOutline = !opts.disabledTools.has("aft_outline");
|
|
29404
|
+
const hasZoom = !opts.disabledTools.has("aft_zoom");
|
|
29405
|
+
const hasGrep = opts.toolSurface !== "minimal" && !opts.disabledTools.has(grepName);
|
|
29406
|
+
const hasSearch = opts.toolSurface !== "minimal" && opts.semanticEnabled && !opts.disabledTools.has("aft_search");
|
|
29407
|
+
const hasNavigate = opts.toolSurface === "all" && !opts.disabledTools.has("aft_navigate");
|
|
29408
|
+
const hasBgBash = opts.bashBackgroundEnabled && !opts.disabledTools.has(bashName) && !opts.disabledTools.has(bashStatusName);
|
|
29409
|
+
if (hasOutline && hasZoom) {
|
|
29410
|
+
sections.push(`**Web/URL access**: \`aft_outline({ url })\` first for structure, then \`aft_zoom({ url, symbol: "<heading>" })\` for the specific section.`);
|
|
29411
|
+
}
|
|
29412
|
+
if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
|
|
29413
|
+
const locator = hasGrep && hasSearch ? `\`${grepName}\` or \`aft_search\`` : hasGrep ? `\`${grepName}\`` : "`aft_search`";
|
|
29414
|
+
sections.push(`**Code exploration**: ${locator} to locate \u2192 \`aft_outline\` for structure \u2192 \`aft_zoom\` for symbol(s).`);
|
|
29415
|
+
}
|
|
29416
|
+
if (hasNavigate) {
|
|
29417
|
+
sections.push([
|
|
29418
|
+
"Use `aft_navigate` instead of grep + read chains for relationship questions:",
|
|
29419
|
+
"- `callers` \u2014 find all call sites before changing a function signature",
|
|
29420
|
+
"- `impact` \u2014 blast radius (which functions/files will need updates)",
|
|
29421
|
+
"- `trace_to` \u2014 how execution reaches this code from entry points (routes, exports, main)",
|
|
29422
|
+
"- `trace_data` \u2014 follow a value through assignments and parameters across files"
|
|
29423
|
+
].join(`
|
|
29424
|
+
`));
|
|
29425
|
+
}
|
|
29426
|
+
if (hasBgBash) {
|
|
29427
|
+
sections.push(`**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns immediately with a \`taskId\`. Check progress with \`${bashStatusName}({ taskId })\`.`);
|
|
29428
|
+
}
|
|
29429
|
+
if (sections.length === 0) {
|
|
29430
|
+
return null;
|
|
29431
|
+
}
|
|
29432
|
+
return `${HEADING}
|
|
29433
|
+
|
|
29434
|
+
${sections.join(`
|
|
29435
|
+
|
|
29436
|
+
`)}`;
|
|
29437
|
+
}
|
|
29438
|
+
function buildHintsFromConfig(config2, disabledTools) {
|
|
29439
|
+
return buildWorkflowHints({
|
|
29440
|
+
toolSurface: config2.tool_surface ?? "recommended",
|
|
29441
|
+
hoistBuiltins: config2.hoist_builtin_tools !== false,
|
|
29442
|
+
semanticEnabled: config2.semantic_search === true,
|
|
29443
|
+
bashBackgroundEnabled: config2.experimental?.bash?.background === true,
|
|
29444
|
+
disabledTools
|
|
29445
|
+
});
|
|
29446
|
+
}
|
|
29447
|
+
|
|
29325
29448
|
// src/index.ts
|
|
29326
29449
|
var STATUS_COMMAND = "aft-status";
|
|
29327
29450
|
var SENTINEL_PREFIX = "__AFT_STATUS_";
|
|
@@ -29554,7 +29677,7 @@ ${lines}
|
|
|
29554
29677
|
});
|
|
29555
29678
|
rpcServer.handle("status", async (params) => {
|
|
29556
29679
|
const sessionID = params.sessionID || "rpc";
|
|
29557
|
-
const bridge = pool.
|
|
29680
|
+
const bridge = pool.getActiveBridgeForRoot(input.directory) ?? pool.getBridge(input.directory);
|
|
29558
29681
|
return await bridge.send("status", { session_id: sessionID });
|
|
29559
29682
|
});
|
|
29560
29683
|
const storageDir = configOverrides.storage_dir;
|
|
@@ -29664,8 +29787,34 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
29664
29787
|
autoUpdate: aftConfig.auto_update ?? true,
|
|
29665
29788
|
signal: autoUpdateAbort.signal
|
|
29666
29789
|
});
|
|
29790
|
+
const HINTS_TOOL_NAMES = [
|
|
29791
|
+
"aft_outline",
|
|
29792
|
+
"aft_zoom",
|
|
29793
|
+
"aft_search",
|
|
29794
|
+
"aft_navigate",
|
|
29795
|
+
"grep",
|
|
29796
|
+
"aft_grep",
|
|
29797
|
+
"bash",
|
|
29798
|
+
"aft_bash",
|
|
29799
|
+
"bash_status"
|
|
29800
|
+
];
|
|
29801
|
+
const registeredTools = new Set(Object.keys(allTools));
|
|
29802
|
+
const hintsAbsentTools = new Set;
|
|
29803
|
+
for (const name of HINTS_TOOL_NAMES) {
|
|
29804
|
+
if (!registeredTools.has(name))
|
|
29805
|
+
hintsAbsentTools.add(name);
|
|
29806
|
+
}
|
|
29807
|
+
const hintsBlock = buildHintsFromConfig(aftConfig, hintsAbsentTools);
|
|
29808
|
+
if (hintsBlock) {
|
|
29809
|
+
log(`Workflow hints injected (${hintsBlock.length} chars)`);
|
|
29810
|
+
}
|
|
29667
29811
|
return {
|
|
29668
29812
|
tool: allTools,
|
|
29813
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
29814
|
+
if (hintsBlock) {
|
|
29815
|
+
output.system.push(hintsBlock);
|
|
29816
|
+
}
|
|
29817
|
+
},
|
|
29669
29818
|
event: async (eventInput) => {
|
|
29670
29819
|
await autoUpdateEventHook(eventInput);
|
|
29671
29820
|
if (eventInput.event.type !== "session.idle")
|
|
@@ -29687,7 +29836,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
|
|
|
29687
29836
|
if (isTuiMode2() || commandInput.command !== STATUS_COMMAND) {
|
|
29688
29837
|
return;
|
|
29689
29838
|
}
|
|
29690
|
-
const bridge = ctx.pool.
|
|
29839
|
+
const bridge = ctx.pool.getActiveBridgeForRoot(input.directory) ?? ctx.pool.getBridge(input.directory);
|
|
29691
29840
|
const response = await bridge.send("status", { session_id: commandInput.sessionID });
|
|
29692
29841
|
if (response.success === false) {
|
|
29693
29842
|
throw new Error(response.message || "status failed");
|
package/dist/pool.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export declare class BridgePool {
|
|
|
38
38
|
* cold-start cost on a cheap query.
|
|
39
39
|
*/
|
|
40
40
|
getAnyActiveBridge(projectRoot: string): BinaryBridge | null;
|
|
41
|
+
/** Get an alive bridge only when it belongs to the requested project root. */
|
|
42
|
+
getActiveBridgeForRoot(projectRoot: string): BinaryBridge | null;
|
|
41
43
|
/**
|
|
42
44
|
* Get or create the bridge for `projectRoot`.
|
|
43
45
|
*
|
package/dist/pool.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAY5D,MAAM,WAAW,WAAY,SAAQ,aAAa;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,UAAU;IACrB,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA0B;IAC1D,OAAO,CAAC,YAAY,CAA+C;gBAGjE,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,WAAgB,EACzB,eAAe,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;IAqB/C;;;;;;;OAOG;IACH,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAgB5D,8EAA8E;IAC9E,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAQhE;;;;;;;OAOG;IACH,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY;IAmB5C,wEAAwE;IACxE,OAAO,CAAC,OAAO;IAUf,yDAAyD;IACzD,OAAO,CAAC,QAAQ;IAgBhB,wDAAwD;IAClD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAU/B;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcnD,4CAA4C;IAC5C,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
|
@@ -6,6 +6,8 @@ export declare class AftRpcClient {
|
|
|
6
6
|
constructor(storageDir: string, directory: string);
|
|
7
7
|
/** Call an RPC method. Retries port resolution if the server isn't ready yet. */
|
|
8
8
|
call<T = Record<string, unknown>>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
9
|
+
private callResolved;
|
|
10
|
+
private retryAfterReset;
|
|
9
11
|
/** Check if the RPC server is reachable. */
|
|
10
12
|
isAvailable(): Promise<boolean>;
|
|
11
13
|
private resolvePort;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAQA,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,aAAa,CAAS;gBAElB,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAIjD,iFAAiF;IAC3E,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAQA,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,aAAa,CAAS;gBAElB,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAIjD,iFAAiF;IAC3E,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,CAAC,CAAC;YASC,YAAY;YA6BZ,eAAe;IAe7B,4CAA4C;IACtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;YASvB,WAAW;YAIX,eAAe;IAoC7B,OAAO,CAAC,YAAY;YAuBN,WAAW;YAWX,gBAAgB;IAU9B,KAAK,IAAI,IAAI;CAKd"}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { type Dispatcher } from "undici";
|
|
1
3
|
interface FetchUrlOptions {
|
|
2
4
|
allowPrivate?: boolean;
|
|
5
|
+
/** Test-only injection point. Production fetches use undici with DNS pinning. */
|
|
6
|
+
fetchImpl?: FetchImpl;
|
|
7
|
+
/** Test-only injection point. Production DNS resolution uses node:dns/promises.lookup. */
|
|
8
|
+
lookup?: LookupFn;
|
|
9
|
+
/** Test-only injection point for observing the DNS-pinned dispatcher. */
|
|
10
|
+
dispatcherFactory?: (validatedIp: string) => Dispatcher;
|
|
3
11
|
}
|
|
12
|
+
type LookupFn = typeof lookup;
|
|
13
|
+
interface FetchInit {
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
redirect?: "manual";
|
|
16
|
+
dispatcher?: Dispatcher;
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
type FetchImpl = (input: string, init: FetchInit) => Promise<Response>;
|
|
4
20
|
/** Exported for unit tests. */
|
|
5
21
|
export declare function _isPrivateIpv4(address: string): boolean;
|
|
6
22
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url-fetch.d.ts","sourceRoot":"","sources":["../../src/shared/url-fetch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"url-fetch.d.ts","sourceRoot":"","sources":["../../src/shared/url-fetch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAW3C,OAAO,EAAS,KAAK,UAAU,EAAwB,MAAM,QAAQ,CAAC;AAWtE,UAAU,eAAe;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iFAAiF;IACjF,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,yEAAyE;IACzE,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,UAAU,CAAC;CACzD;AAED,KAAK,QAAQ,GAAG,OAAO,MAAM,CAAC;AAC9B,UAAU,SAAS;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAyBvE,+BAA+B;AAC/B,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAavD;AAgPD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAgGjB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAkCxD"}
|
package/dist/tools/reading.d.ts
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
2
|
import type { PluginContext } from "../types.js";
|
|
3
|
+
interface ZoomBatchSymbolResult {
|
|
4
|
+
name: string;
|
|
5
|
+
success: boolean;
|
|
6
|
+
content?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
interface ZoomBatchResult {
|
|
10
|
+
complete: boolean;
|
|
11
|
+
symbols: ZoomBatchSymbolResult[];
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
15
|
* Tool definitions for code reading commands: outline + zoom.
|
|
5
16
|
*/
|
|
6
17
|
export declare function readingTools(ctx: PluginContext): Record<string, ToolDefinition>;
|
|
18
|
+
/** Exported for regression tests. */
|
|
19
|
+
export declare function formatZoomBatchResult(symbols: string[], responses: Record<string, unknown>[]): ZoomBatchResult;
|
|
20
|
+
export {};
|
|
7
21
|
//# sourceMappingURL=reading.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reading.d.ts","sourceRoot":"","sources":["../../src/tools/reading.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA2CjD;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"reading.d.ts","sourceRoot":"","sources":["../../src/tools/reading.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA2CjD,UAAU,qBAAqB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,eAAe;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,qBAAqB,EAAE,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0K/E;AAED,qCAAqC;AACrC,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EAAE,EACjB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACnC,eAAe,CA0BjB"}
|
package/dist/tui.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "@cortexkit/aft-opencode",
|
|
5
|
-
version: "0.18.
|
|
5
|
+
version: "0.18.4",
|
|
6
6
|
type: "module",
|
|
7
7
|
description: "OpenCode plugin for Agent File Tools (AFT) \u2014 tree-sitter and lsp powered code analysis",
|
|
8
8
|
main: "dist/index.js",
|
|
@@ -33,14 +33,15 @@ var package_default = {
|
|
|
33
33
|
"@opencode-ai/plugin": "^1.2.26",
|
|
34
34
|
"@opencode-ai/sdk": "^1.2.26",
|
|
35
35
|
"comment-json": "^4.6.2",
|
|
36
|
+
undici: "^7.25.0",
|
|
36
37
|
zod: "^4.1.8"
|
|
37
38
|
},
|
|
38
39
|
optionalDependencies: {
|
|
39
|
-
"@cortexkit/aft-darwin-arm64": "0.18.
|
|
40
|
-
"@cortexkit/aft-darwin-x64": "0.18.
|
|
41
|
-
"@cortexkit/aft-linux-arm64": "0.18.
|
|
42
|
-
"@cortexkit/aft-linux-x64": "0.18.
|
|
43
|
-
"@cortexkit/aft-win32-x64": "0.18.
|
|
40
|
+
"@cortexkit/aft-darwin-arm64": "0.18.4",
|
|
41
|
+
"@cortexkit/aft-darwin-x64": "0.18.4",
|
|
42
|
+
"@cortexkit/aft-linux-arm64": "0.18.4",
|
|
43
|
+
"@cortexkit/aft-linux-x64": "0.18.4",
|
|
44
|
+
"@cortexkit/aft-win32-x64": "0.18.4"
|
|
44
45
|
},
|
|
45
46
|
devDependencies: {
|
|
46
47
|
"@types/node": "^22.0.0",
|
|
@@ -157,17 +158,40 @@ class AftRpcClient {
|
|
|
157
158
|
if (!info) {
|
|
158
159
|
throw new Error("AFT RPC server not available");
|
|
159
160
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
return this.callResolved(method, params, info, true);
|
|
162
|
+
}
|
|
163
|
+
async callResolved(method, params, info, retryOnConnectionFailure) {
|
|
164
|
+
let response;
|
|
165
|
+
try {
|
|
166
|
+
response = await this.fetchWithTimeout(`http://127.0.0.1:${info.port}/rpc/${method}`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { "Content-Type": "application/json" },
|
|
169
|
+
body: JSON.stringify({ ...params, token: info.token })
|
|
170
|
+
});
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (!retryOnConnectionFailure)
|
|
173
|
+
throw err;
|
|
174
|
+
return this.retryAfterReset(method, params, err);
|
|
175
|
+
}
|
|
165
176
|
if (!response.ok) {
|
|
166
177
|
const text = await response.text();
|
|
178
|
+
if (response.status >= 500 && retryOnConnectionFailure) {
|
|
179
|
+
return this.retryAfterReset(method, params, `HTTP ${response.status}: ${text}`);
|
|
180
|
+
}
|
|
167
181
|
throw new Error(`RPC ${method} failed (${response.status}): ${text}`);
|
|
168
182
|
}
|
|
169
183
|
return await response.json();
|
|
170
184
|
}
|
|
185
|
+
async retryAfterReset(method, params, reason) {
|
|
186
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
187
|
+
warn(`RPC ${method} failed on cached port; retrying after port refresh (${message})`);
|
|
188
|
+
this.reset();
|
|
189
|
+
const refreshed = await this.resolvePortInfo();
|
|
190
|
+
if (!refreshed) {
|
|
191
|
+
throw new Error("AFT RPC server not available after port refresh");
|
|
192
|
+
}
|
|
193
|
+
return this.callResolved(method, params, refreshed, false);
|
|
194
|
+
}
|
|
171
195
|
async isAvailable() {
|
|
172
196
|
try {
|
|
173
197
|
const port = await this.resolvePort();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AftConfig } from "./config.js";
|
|
2
|
+
export interface WorkflowHintsOpts {
|
|
3
|
+
/** `tool_surface` setting — controls which tools are registered. */
|
|
4
|
+
toolSurface: "minimal" | "recommended" | "all";
|
|
5
|
+
/** `hoist_builtin_tools` setting — affects tool name (read vs aft_read). */
|
|
6
|
+
hoistBuiltins: boolean;
|
|
7
|
+
/** `experimental.semantic_search` — gates `aft_search` mention. */
|
|
8
|
+
semanticEnabled: boolean;
|
|
9
|
+
/** `experimental.bash.background` — gates background-bash paragraph. */
|
|
10
|
+
bashBackgroundEnabled: boolean;
|
|
11
|
+
/** Set of disabled tool names (after surface filtering). */
|
|
12
|
+
disabledTools: Set<string>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build the workflow hints block. Returns `null` when no hints are
|
|
16
|
+
* applicable for the configured surface (e.g. `tool_surface: "minimal"`
|
|
17
|
+
* with no aft_outline/aft_zoom available — only safety tool is registered).
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildWorkflowHints(opts: WorkflowHintsOpts): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve workflow-hints opts from a loaded AftConfig and the active
|
|
22
|
+
* disabled-tools set computed at registration time.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildHintsFromConfig(config: AftConfig, disabledTools: Set<string>): string | null;
|
|
25
|
+
//# sourceMappingURL=workflow-hints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-hints.d.ts","sourceRoot":"","sources":["../src/workflow-hints.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,WAAW,EAAE,SAAS,GAAG,aAAa,GAAG,KAAK,CAAC;IAC/C,4EAA4E;IAC5E,aAAa,EAAE,OAAO,CAAC;IACvB,mEAAmE;IACnE,eAAe,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,4DAA4D;IAC5D,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC5B;AAID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAoEzE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAQjG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/aft-opencode",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,14 +31,15 @@
|
|
|
31
31
|
"@opencode-ai/plugin": "^1.2.26",
|
|
32
32
|
"@opencode-ai/sdk": "^1.2.26",
|
|
33
33
|
"comment-json": "^4.6.2",
|
|
34
|
+
"undici": "^7.25.0",
|
|
34
35
|
"zod": "^4.1.8"
|
|
35
36
|
},
|
|
36
37
|
"optionalDependencies": {
|
|
37
|
-
"@cortexkit/aft-darwin-arm64": "0.18.
|
|
38
|
-
"@cortexkit/aft-darwin-x64": "0.18.
|
|
39
|
-
"@cortexkit/aft-linux-arm64": "0.18.
|
|
40
|
-
"@cortexkit/aft-linux-x64": "0.18.
|
|
41
|
-
"@cortexkit/aft-win32-x64": "0.18.
|
|
38
|
+
"@cortexkit/aft-darwin-arm64": "0.18.4",
|
|
39
|
+
"@cortexkit/aft-darwin-x64": "0.18.4",
|
|
40
|
+
"@cortexkit/aft-linux-arm64": "0.18.4",
|
|
41
|
+
"@cortexkit/aft-linux-x64": "0.18.4",
|
|
42
|
+
"@cortexkit/aft-win32-x64": "0.18.4"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/node": "^22.0.0",
|
package/src/shared/rpc-client.ts
CHANGED
|
@@ -26,20 +26,53 @@ export class AftRpcClient {
|
|
|
26
26
|
throw new Error("AFT RPC server not available");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
return this.callResolved<T>(method, params, info, true);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async callResolved<T>(
|
|
33
|
+
method: string,
|
|
34
|
+
params: Record<string, unknown>,
|
|
35
|
+
info: { port: number; token: string | null },
|
|
36
|
+
retryOnConnectionFailure: boolean,
|
|
37
|
+
): Promise<T> {
|
|
38
|
+
let response: Response;
|
|
39
|
+
try {
|
|
40
|
+
response = await this.fetchWithTimeout(`http://127.0.0.1:${info.port}/rpc/${method}`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "Content-Type": "application/json" },
|
|
43
|
+
body: JSON.stringify({ ...params, token: info.token }),
|
|
44
|
+
});
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (!retryOnConnectionFailure) throw err;
|
|
47
|
+
return this.retryAfterReset<T>(method, params, err);
|
|
48
|
+
}
|
|
34
49
|
|
|
35
50
|
if (!response.ok) {
|
|
36
51
|
const text = await response.text();
|
|
52
|
+
if (response.status >= 500 && retryOnConnectionFailure) {
|
|
53
|
+
return this.retryAfterReset<T>(method, params, `HTTP ${response.status}: ${text}`);
|
|
54
|
+
}
|
|
37
55
|
throw new Error(`RPC ${method} failed (${response.status}): ${text}`);
|
|
38
56
|
}
|
|
39
57
|
|
|
40
58
|
return (await response.json()) as T;
|
|
41
59
|
}
|
|
42
60
|
|
|
61
|
+
private async retryAfterReset<T>(
|
|
62
|
+
method: string,
|
|
63
|
+
params: Record<string, unknown>,
|
|
64
|
+
reason: unknown,
|
|
65
|
+
): Promise<T> {
|
|
66
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
67
|
+
warn(`RPC ${method} failed on cached port; retrying after port refresh (${message})`);
|
|
68
|
+
this.reset();
|
|
69
|
+
const refreshed = await this.resolvePortInfo();
|
|
70
|
+
if (!refreshed) {
|
|
71
|
+
throw new Error("AFT RPC server not available after port refresh");
|
|
72
|
+
}
|
|
73
|
+
return this.callResolved<T>(method, params, refreshed, false);
|
|
74
|
+
}
|
|
75
|
+
|
|
43
76
|
/** Check if the RPC server is reachable. */
|
|
44
77
|
async isAvailable(): Promise<boolean> {
|
|
45
78
|
try {
|
package/src/shared/url-fetch.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
import { isIP } from "node:net";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
+
import { Agent, type Dispatcher, fetch as undiciFetch } from "undici";
|
|
13
14
|
import { log, warn } from "../logger";
|
|
14
15
|
|
|
15
16
|
/** Max response body size (10 MB) */
|
|
@@ -22,8 +23,24 @@ const MAX_REDIRECTS = 5;
|
|
|
22
23
|
|
|
23
24
|
interface FetchUrlOptions {
|
|
24
25
|
allowPrivate?: boolean;
|
|
26
|
+
/** Test-only injection point. Production fetches use undici with DNS pinning. */
|
|
27
|
+
fetchImpl?: FetchImpl;
|
|
28
|
+
/** Test-only injection point. Production DNS resolution uses node:dns/promises.lookup. */
|
|
29
|
+
lookup?: LookupFn;
|
|
30
|
+
/** Test-only injection point for observing the DNS-pinned dispatcher. */
|
|
31
|
+
dispatcherFactory?: (validatedIp: string) => Dispatcher;
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
type LookupFn = typeof lookup;
|
|
35
|
+
interface FetchInit {
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
redirect?: "manual";
|
|
38
|
+
dispatcher?: Dispatcher;
|
|
39
|
+
headers?: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type FetchImpl = (input: string, init: FetchInit) => Promise<Response>;
|
|
43
|
+
|
|
27
44
|
interface CacheMeta {
|
|
28
45
|
url: string;
|
|
29
46
|
contentType: string;
|
|
@@ -163,11 +180,15 @@ function isPrivateIp(address: string): boolean {
|
|
|
163
180
|
return true;
|
|
164
181
|
}
|
|
165
182
|
|
|
166
|
-
async function assertPublicUrl(
|
|
183
|
+
async function assertPublicUrl(
|
|
184
|
+
url: URL,
|
|
185
|
+
allowPrivate: boolean,
|
|
186
|
+
dnsLookup: LookupFn = lookup,
|
|
187
|
+
): Promise<string | undefined> {
|
|
167
188
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
168
189
|
throw new Error(`Only http:// and https:// URLs are supported, got: ${url.protocol}`);
|
|
169
190
|
}
|
|
170
|
-
if (allowPrivate) return;
|
|
191
|
+
if (allowPrivate) return undefined;
|
|
171
192
|
|
|
172
193
|
// URL.hostname returns IPv6 literals wrapped in brackets ("[::1]") per WHATWG.
|
|
173
194
|
// Strip them before checking the address.
|
|
@@ -180,15 +201,29 @@ async function assertPublicUrl(url: URL, allowPrivate: boolean): Promise<void> {
|
|
|
180
201
|
if (isPrivateIp(hostname)) {
|
|
181
202
|
throw new Error(`Blocked private URL host ${url.hostname} (${hostname})`);
|
|
182
203
|
}
|
|
183
|
-
return;
|
|
204
|
+
return hostname;
|
|
184
205
|
}
|
|
185
206
|
|
|
186
|
-
const addresses = await
|
|
207
|
+
const addresses = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
187
208
|
for (const { address } of addresses) {
|
|
188
209
|
if (isPrivateIp(address)) {
|
|
189
210
|
throw new Error(`Blocked private URL host ${url.hostname} (${address})`);
|
|
190
211
|
}
|
|
191
212
|
}
|
|
213
|
+
if (addresses.length === 0) {
|
|
214
|
+
throw new Error(`Failed to resolve URL host ${url.hostname}`);
|
|
215
|
+
}
|
|
216
|
+
return addresses[0].address;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function createPinnedDispatcher(validatedIp: string): Dispatcher {
|
|
220
|
+
return new Agent({
|
|
221
|
+
connect: {
|
|
222
|
+
lookup: (_hostname, _opts, callback) => {
|
|
223
|
+
callback(null, validatedIp, validatedIp.includes(":") ? 6 : 4);
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
});
|
|
192
227
|
}
|
|
193
228
|
|
|
194
229
|
function resolveRedirectUrl(currentUrl: URL, location: string | null): URL {
|
|
@@ -198,19 +233,29 @@ function resolveRedirectUrl(currentUrl: URL, location: string | null): URL {
|
|
|
198
233
|
return new URL(location, currentUrl);
|
|
199
234
|
}
|
|
200
235
|
|
|
201
|
-
async function fetchWithRedirects(
|
|
236
|
+
async function fetchWithRedirects(
|
|
237
|
+
startUrl: URL,
|
|
238
|
+
allowPrivate: boolean,
|
|
239
|
+
deps: Pick<FetchUrlOptions, "dispatcherFactory" | "fetchImpl" | "lookup"> = {},
|
|
240
|
+
): Promise<Response> {
|
|
202
241
|
let currentUrl = startUrl;
|
|
242
|
+
const fetchImpl = deps.fetchImpl ?? (undiciFetch as FetchImpl);
|
|
243
|
+
const dnsLookup = deps.lookup ?? lookup;
|
|
203
244
|
|
|
204
245
|
for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount++) {
|
|
205
|
-
await assertPublicUrl(currentUrl, allowPrivate);
|
|
246
|
+
const validatedIp = await assertPublicUrl(currentUrl, allowPrivate, dnsLookup);
|
|
247
|
+
const dispatcher = validatedIp
|
|
248
|
+
? (deps.dispatcherFactory ?? createPinnedDispatcher)(validatedIp)
|
|
249
|
+
: undefined;
|
|
206
250
|
|
|
207
251
|
const controller = new AbortController();
|
|
208
252
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
209
253
|
let response: Response;
|
|
210
254
|
try {
|
|
211
|
-
response = await
|
|
255
|
+
response = await fetchImpl(currentUrl.href, {
|
|
212
256
|
signal: controller.signal,
|
|
213
257
|
redirect: "manual",
|
|
258
|
+
dispatcher,
|
|
214
259
|
headers: {
|
|
215
260
|
"user-agent": "aft-opencode-plugin",
|
|
216
261
|
// Prioritize markdown for content-negotiating servers (GitHub API, many docs sites).
|
|
@@ -316,7 +361,7 @@ export async function fetchUrlToTempFile(
|
|
|
316
361
|
}
|
|
317
362
|
|
|
318
363
|
log(`Fetching URL: ${url}`);
|
|
319
|
-
const response = await fetchWithRedirects(parsed, allowPrivate);
|
|
364
|
+
const response = await fetchWithRedirects(parsed, allowPrivate, options);
|
|
320
365
|
|
|
321
366
|
if (!response.ok) {
|
|
322
367
|
throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${url}`);
|