@nexrall/code-core 1.4.44 → 1.4.45
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/agent/loop.js +1 -1
- package/dist/mcp/manager.d.ts +20 -2
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +36 -3
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +101 -21
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -3123,7 +3123,7 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
3123
3123
|
// 2. Try MCP tools (serverName__toolName)
|
|
3124
3124
|
}
|
|
3125
3125
|
else if (options.mcpManager?.isMcpTool(name)) {
|
|
3126
|
-
const mcpOutput = await options.mcpManager.callTool(name, input);
|
|
3126
|
+
const mcpOutput = await options.mcpManager.callTool(name, input, options.abortSignal);
|
|
3127
3127
|
result = { output: mcpOutput ?? '' };
|
|
3128
3128
|
// 3. Fall through to built-in executor
|
|
3129
3129
|
}
|
package/dist/mcp/manager.d.ts
CHANGED
|
@@ -25,8 +25,26 @@ export declare class McpManager {
|
|
|
25
25
|
connectAll(config: McpConfig): Promise<void>;
|
|
26
26
|
/** Returns all MCP tools as Anthropic-compatible tool definitions. */
|
|
27
27
|
getAnthropicTools(): AnthropicTool[];
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Execute an MCP tool call. Returns null if this tool is not an MCP tool.
|
|
30
|
+
*
|
|
31
|
+
* `abortSignal` is optional and best-effort: the underlying transports (stdio
|
|
32
|
+
* JSON-RPC / HTTP / SSE) have no cancellation message in this client, so an
|
|
33
|
+
* abort here does NOT stop the server from finishing its work — it only stops
|
|
34
|
+
* THIS CALL from making the caller wait for it. Before this was added, Ctrl+C
|
|
35
|
+
* during an MCP tool call was silently swallowed: loop.ts set
|
|
36
|
+
* `abortSignal.aborted = true` but nothing downstream of `options.mcpManager
|
|
37
|
+
* .callTool(name, input)` ever looked at it, so the user's only way out was
|
|
38
|
+
* to wait out the full 60s MCP_TOOL_TIMEOUT_MS (or however long the server
|
|
39
|
+
* actually took). Racing the same abortSignal the 60s timeout already races
|
|
40
|
+
* against makes Ctrl+C return control immediately instead of up to a minute
|
|
41
|
+
* later — matching how the `bash` tool already behaves (its own 200ms abort
|
|
42
|
+
* poll in executor.ts), just without the ability to also kill a remote
|
|
43
|
+
* server's in-flight work the way `bash` can kill a local child process.
|
|
44
|
+
*/
|
|
45
|
+
callTool(prefixedName: string, input: Record<string, unknown>, abortSignal?: {
|
|
46
|
+
aborted: boolean;
|
|
47
|
+
}): Promise<string | null>;
|
|
30
48
|
/** True if this tool name belongs to an MCP server. */
|
|
31
49
|
isMcpTool(name: string): boolean;
|
|
32
50
|
get serverNames(): string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/mcp/manager.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,eAAe,EAAE,iBAAiB,EAAgB,MAAM,UAAU,CAAC;AAOvF,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAC7C;AAGD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE;QACZ,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAID,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAmC;IAEnD,OAAO,CAAC,QAAQ,CAA6B;IAE7C,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,OAAO,CAA6B;IAE5C,OAAO,CAAC,UAAU,CAA6B;IAE/C,+FAA+F;IAC/F,OAAO,CAAC,MAAM,CAAC,UAAU;IAMzB,oGAAoG;IACpG,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS;IA2B7C,mFAAmF;IAC7E,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BlD,sEAAsE;IACtE,iBAAiB,IAAI,aAAa,EAAE;IAkBpC
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/mcp/manager.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,eAAe,EAAE,iBAAiB,EAAgB,MAAM,UAAU,CAAC;AAOvF,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAC7C;AAGD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE;QACZ,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAID,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAmC;IAEnD,OAAO,CAAC,QAAQ,CAA6B;IAE7C,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,OAAO,CAA6B;IAE5C,OAAO,CAAC,UAAU,CAA6B;IAE/C,+FAA+F;IAC/F,OAAO,CAAC,MAAM,CAAC,UAAU;IAMzB,oGAAoG;IACpG,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS;IA2B7C,mFAAmF;IAC7E,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BlD,sEAAsE;IACtE,iBAAiB,IAAI,aAAa,EAAE;IAkBpC;;;;;;;;;;;;;;;;OAgBG;IACG,QAAQ,CACZ,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,GACjC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAoCzB,uDAAuD;IACvD,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIhC,IAAI,WAAW,IAAI,MAAM,EAAE,CAE1B;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,OAAO,CAAC,MAAM,CAAC,WAAW;IAK1B,8FAA8F;IAC9F,SAAS,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;QAAC,KAAK,EAAE,iBAAiB,EAAE,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAY9J,uFAAuF;IACvF,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAMnC,+CAA+C;IAC/C,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM;IAMrE,OAAO,CAAC,MAAM,CAAC,cAAc;IAS7B,OAAO,CAAC,MAAM,CAAC,eAAe;IAK9B,iEAAiE;IACjE,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,GAAE,SAAS,GAAG,MAAkB,GAAG,IAAI;IAOrH,+FAA+F;IAC/F,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAc3D,aAAa,IAAI,IAAI;CAQtB"}
|
package/dist/mcp/manager.js
CHANGED
|
@@ -132,8 +132,24 @@ class McpManager {
|
|
|
132
132
|
}
|
|
133
133
|
return tools;
|
|
134
134
|
}
|
|
135
|
-
/**
|
|
136
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Execute an MCP tool call. Returns null if this tool is not an MCP tool.
|
|
137
|
+
*
|
|
138
|
+
* `abortSignal` is optional and best-effort: the underlying transports (stdio
|
|
139
|
+
* JSON-RPC / HTTP / SSE) have no cancellation message in this client, so an
|
|
140
|
+
* abort here does NOT stop the server from finishing its work — it only stops
|
|
141
|
+
* THIS CALL from making the caller wait for it. Before this was added, Ctrl+C
|
|
142
|
+
* during an MCP tool call was silently swallowed: loop.ts set
|
|
143
|
+
* `abortSignal.aborted = true` but nothing downstream of `options.mcpManager
|
|
144
|
+
* .callTool(name, input)` ever looked at it, so the user's only way out was
|
|
145
|
+
* to wait out the full 60s MCP_TOOL_TIMEOUT_MS (or however long the server
|
|
146
|
+
* actually took). Racing the same abortSignal the 60s timeout already races
|
|
147
|
+
* against makes Ctrl+C return control immediately instead of up to a minute
|
|
148
|
+
* later — matching how the `bash` tool already behaves (its own 200ms abort
|
|
149
|
+
* poll in executor.ts), just without the ability to also kill a remote
|
|
150
|
+
* server's in-flight work the way `bash` can kill a local child process.
|
|
151
|
+
*/
|
|
152
|
+
async callTool(prefixedName, input, abortSignal) {
|
|
137
153
|
const serverName = this._toolMap.get(prefixedName);
|
|
138
154
|
if (!serverName)
|
|
139
155
|
return null;
|
|
@@ -145,7 +161,24 @@ class McpManager {
|
|
|
145
161
|
// Enforce a timeout — a hung MCP server would otherwise block the agent loop forever.
|
|
146
162
|
const MCP_TOOL_TIMEOUT_MS = 60000;
|
|
147
163
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP tool "${prefixedName}" timed out after ${MCP_TOOL_TIMEOUT_MS / 1000}s`)), MCP_TOOL_TIMEOUT_MS));
|
|
148
|
-
|
|
164
|
+
const racers = [client.callTool(toolName, input), timeoutPromise];
|
|
165
|
+
if (abortSignal) {
|
|
166
|
+
let pollId;
|
|
167
|
+
const abortPromise = new Promise((_, reject) => {
|
|
168
|
+
pollId = setInterval(() => {
|
|
169
|
+
if (abortSignal.aborted) {
|
|
170
|
+
clearInterval(pollId);
|
|
171
|
+
reject(Object.assign(new Error('Aborted'), { name: 'AbortError' }));
|
|
172
|
+
}
|
|
173
|
+
}, 100);
|
|
174
|
+
});
|
|
175
|
+
racers.push(abortPromise);
|
|
176
|
+
// Clearing the poll on whichever branch wins avoids leaving a live interval
|
|
177
|
+
// (and the closure it holds) running for the rest of the process lifetime —
|
|
178
|
+
// the same leak-prevention pattern client.ts uses for its own abort poll.
|
|
179
|
+
return Promise.race(racers).finally(() => clearInterval(pollId));
|
|
180
|
+
}
|
|
181
|
+
return Promise.race(racers);
|
|
149
182
|
}
|
|
150
183
|
/** True if this tool name belongs to an MCP server. */
|
|
151
184
|
isMcpTool(name) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAg3EtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,UAAU,CAAC,CA6BrB"}
|
package/dist/tools/executor.js
CHANGED
|
@@ -1617,7 +1617,7 @@ function fetchBlocklistCheck(parsedUrl) {
|
|
|
1617
1617
|
}
|
|
1618
1618
|
return null;
|
|
1619
1619
|
}
|
|
1620
|
-
async function fetchUrl(input, _workDir, _redirectCount = 0) {
|
|
1620
|
+
async function fetchUrl(input, _workDir, _redirectCount = 0, abortSignal) {
|
|
1621
1621
|
const url = typeof input.url === 'string' ? input.url : '';
|
|
1622
1622
|
if (!url)
|
|
1623
1623
|
return { error: 'Missing required parameter: url' };
|
|
@@ -1674,8 +1674,28 @@ async function fetchUrl(input, _workDir, _redirectCount = 0) {
|
|
|
1674
1674
|
if (settled)
|
|
1675
1675
|
return;
|
|
1676
1676
|
settled = true;
|
|
1677
|
+
if (abortPoll !== undefined)
|
|
1678
|
+
clearInterval(abortPoll);
|
|
1677
1679
|
resolve(result);
|
|
1678
1680
|
};
|
|
1681
|
+
// Poll for Ctrl+C the same way the `bash` tool does (executor.ts's own
|
|
1682
|
+
// 200ms abort poll below this function) — fetch_url had NO abort wiring at
|
|
1683
|
+
// all before this, so a user hitting Ctrl+C while the model waited on a slow
|
|
1684
|
+
// page had to sit through the full DEFAULT_TIMEOUT_MS (60s) with no way out.
|
|
1685
|
+
// `selfAborted` (already used for our own intentional destroy() below) is
|
|
1686
|
+
// reused here so an abort-triggered destroy() doesn't ALSO surface as a
|
|
1687
|
+
// generic "socket hang up" error — it reports the clearer "stopped by user"
|
|
1688
|
+
// message instead.
|
|
1689
|
+
let abortPoll;
|
|
1690
|
+
if (abortSignal) {
|
|
1691
|
+
abortPoll = setInterval(() => {
|
|
1692
|
+
if (abortSignal.aborted && !settled) {
|
|
1693
|
+
selfAborted = true;
|
|
1694
|
+
req.destroy();
|
|
1695
|
+
finish({ error: 'Request stopped by user', interrupted: true });
|
|
1696
|
+
}
|
|
1697
|
+
}, 100);
|
|
1698
|
+
}
|
|
1679
1699
|
const req = transport.get(url, {
|
|
1680
1700
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
1681
1701
|
lookup: guardedLookup,
|
|
@@ -1699,7 +1719,7 @@ async function fetchUrl(input, _workDir, _redirectCount = 0) {
|
|
|
1699
1719
|
finish({ error: `Invalid redirect location: ${res.headers.location}` });
|
|
1700
1720
|
return;
|
|
1701
1721
|
}
|
|
1702
|
-
fetchUrl({ url: nextUrl }, undefined, _redirectCount + 1).then(finish);
|
|
1722
|
+
fetchUrl({ url: nextUrl }, undefined, _redirectCount + 1, abortSignal).then(finish);
|
|
1703
1723
|
return;
|
|
1704
1724
|
}
|
|
1705
1725
|
const contentType = res.headers['content-type'] ?? '';
|
|
@@ -2082,37 +2102,70 @@ const VALID_ASPECT_RATIOS = new Set(['1:1', '9:16', '16:9', '3:4', '4:3', '3:2',
|
|
|
2082
2102
|
// Minimal HTTPS helpers (mirrors the Node built-in pattern used by fetchUrl above).
|
|
2083
2103
|
// We avoid `node-fetch` here because executor.ts targets both Node 18+ (global fetch)
|
|
2084
2104
|
// and older runtimes; using `https` directly is always safe.
|
|
2085
|
-
|
|
2105
|
+
//
|
|
2106
|
+
// `abortSignal` is optional and, like fetchUrl's, best-effort: it stops the CALLER
|
|
2107
|
+
// from waiting on the request (destroys the socket, rejects immediately) rather than
|
|
2108
|
+
// waiting out the full timeoutMs. Before this was added, generate_image (170s),
|
|
2109
|
+
// stock_photo, and web_search had NO abort wiring at all — Ctrl+C during any of them
|
|
2110
|
+
// set `abortSignal.aborted = true` but nothing here ever looked at it, so the only way
|
|
2111
|
+
// out was to wait for the request to finish or time out on its own.
|
|
2112
|
+
function _httpsPost(url, body, headers, timeoutMs = 30000, abortSignal) {
|
|
2086
2113
|
return new Promise((resolve, reject) => {
|
|
2087
2114
|
const u = new URL(url);
|
|
2088
2115
|
const chunks = [];
|
|
2116
|
+
let settled = false;
|
|
2117
|
+
let abortPoll;
|
|
2118
|
+
const done = (fn) => { if (settled)
|
|
2119
|
+
return; settled = true; if (abortPoll)
|
|
2120
|
+
clearInterval(abortPoll); fn(); };
|
|
2089
2121
|
const req = https.request({ hostname: u.hostname, path: u.pathname + u.search, method: 'POST', headers: { ...headers, 'Content-Length': Buffer.byteLength(body) }, timeout: timeoutMs }, (res) => {
|
|
2090
2122
|
res.on('data', (c) => chunks.push(c));
|
|
2091
|
-
res.on('end', () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString('utf-8') }));
|
|
2123
|
+
res.on('end', () => done(() => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString('utf-8') })));
|
|
2092
2124
|
});
|
|
2093
|
-
req.on('timeout', () => { req.destroy(); reject(new Error(`Request timed out after ${timeoutMs}ms`)); });
|
|
2094
|
-
req.on('error', reject);
|
|
2125
|
+
req.on('timeout', () => { req.destroy(); done(() => reject(new Error(`Request timed out after ${timeoutMs}ms`))); });
|
|
2126
|
+
req.on('error', (err) => done(() => reject(err)));
|
|
2127
|
+
if (abortSignal) {
|
|
2128
|
+
abortPoll = setInterval(() => {
|
|
2129
|
+
if (abortSignal.aborted && !settled) {
|
|
2130
|
+
req.destroy();
|
|
2131
|
+
done(() => reject(Object.assign(new Error('Stopped by user'), { name: 'AbortError' })));
|
|
2132
|
+
}
|
|
2133
|
+
}, 100);
|
|
2134
|
+
}
|
|
2095
2135
|
req.write(body);
|
|
2096
2136
|
req.end();
|
|
2097
2137
|
});
|
|
2098
2138
|
}
|
|
2099
|
-
function _httpsGet(url, headers, timeoutMs = 20000) {
|
|
2139
|
+
function _httpsGet(url, headers, timeoutMs = 20000, abortSignal) {
|
|
2100
2140
|
return new Promise((resolve, reject) => {
|
|
2101
2141
|
const u = new URL(url);
|
|
2102
2142
|
const transport = u.protocol === 'https:' ? https : http;
|
|
2103
2143
|
const chunks = [];
|
|
2144
|
+
let settled = false;
|
|
2145
|
+
let abortPoll;
|
|
2146
|
+
const done = (fn) => { if (settled)
|
|
2147
|
+
return; settled = true; if (abortPoll)
|
|
2148
|
+
clearInterval(abortPoll); fn(); };
|
|
2104
2149
|
const req = transport.get({ hostname: u.hostname, path: u.pathname + u.search, headers, timeout: timeoutMs }, (res) => {
|
|
2105
2150
|
// Follow redirects once
|
|
2106
2151
|
if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location) {
|
|
2107
2152
|
req.destroy();
|
|
2108
|
-
_httpsGet(new URL(res.headers.location, url).href, headers, timeoutMs).then(resolve, reject);
|
|
2153
|
+
done(() => { _httpsGet(new URL(res.headers.location, url).href, headers, timeoutMs, abortSignal).then(resolve, reject); });
|
|
2109
2154
|
return;
|
|
2110
2155
|
}
|
|
2111
2156
|
res.on('data', (c) => chunks.push(c));
|
|
2112
|
-
res.on('end', () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks), contentType: res.headers['content-type'] ?? '' }));
|
|
2157
|
+
res.on('end', () => done(() => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks), contentType: res.headers['content-type'] ?? '' })));
|
|
2113
2158
|
});
|
|
2114
|
-
req.on('timeout', () => { req.destroy(); reject(new Error(`Download timed out after ${timeoutMs}ms`)); });
|
|
2115
|
-
req.on('error', reject);
|
|
2159
|
+
req.on('timeout', () => { req.destroy(); done(() => reject(new Error(`Download timed out after ${timeoutMs}ms`))); });
|
|
2160
|
+
req.on('error', (err) => done(() => reject(err)));
|
|
2161
|
+
if (abortSignal) {
|
|
2162
|
+
abortPoll = setInterval(() => {
|
|
2163
|
+
if (abortSignal.aborted && !settled) {
|
|
2164
|
+
req.destroy();
|
|
2165
|
+
done(() => reject(Object.assign(new Error('Stopped by user'), { name: 'AbortError' })));
|
|
2166
|
+
}
|
|
2167
|
+
}, 100);
|
|
2168
|
+
}
|
|
2116
2169
|
});
|
|
2117
2170
|
}
|
|
2118
2171
|
/**
|
|
@@ -2122,7 +2175,7 @@ function _httpsGet(url, headers, timeoutMs = 20000) {
|
|
|
2122
2175
|
*
|
|
2123
2176
|
* { prompt, path, aspect_ratio? }
|
|
2124
2177
|
*/
|
|
2125
|
-
async function generateImage(input, workDir) {
|
|
2178
|
+
async function generateImage(input, workDir, abortSignal) {
|
|
2126
2179
|
const prompt = typeof input.prompt === 'string' ? input.prompt.trim() : '';
|
|
2127
2180
|
const relPath = typeof input.path === 'string' ? input.path.trim() : '';
|
|
2128
2181
|
const aspectRatio = typeof input.aspect_ratio === 'string' && VALID_ASPECT_RATIOS.has(input.aspect_ratio)
|
|
@@ -2135,12 +2188,14 @@ async function generateImage(input, workDir) {
|
|
|
2135
2188
|
if (!token)
|
|
2136
2189
|
return { error: 'Not authenticated. Run `nexrall-code login` first.' };
|
|
2137
2190
|
// 1. Request the backend to generate the image (async task + poll handled server-side).
|
|
2138
|
-
// Backend may take up to ~2.5 min — use a 170s timeout with room to spare.
|
|
2191
|
+
// Backend may take up to ~2.5 min — use a 170s timeout with room to spare. Without
|
|
2192
|
+
// abortSignal wired through here, Ctrl+C during this call previously had no effect
|
|
2193
|
+
// for up to the full 170s — the longest silent Ctrl+C stall of any tool.
|
|
2139
2194
|
let imageUrl;
|
|
2140
2195
|
let cost;
|
|
2141
2196
|
let balance;
|
|
2142
2197
|
try {
|
|
2143
|
-
const r = await _httpsPost(`${client_1.API_BASE}/api/code/assets/generate`, JSON.stringify({ prompt, aspect_ratio: aspectRatio }), { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, 170000);
|
|
2198
|
+
const r = await _httpsPost(`${client_1.API_BASE}/api/code/assets/generate`, JSON.stringify({ prompt, aspect_ratio: aspectRatio }), { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, 170000, abortSignal);
|
|
2144
2199
|
let json = {};
|
|
2145
2200
|
try {
|
|
2146
2201
|
json = JSON.parse(r.body);
|
|
@@ -2155,6 +2210,8 @@ async function generateImage(input, workDir) {
|
|
|
2155
2210
|
return { error: 'Image generation succeeded but no URL was returned.' };
|
|
2156
2211
|
}
|
|
2157
2212
|
catch (err) {
|
|
2213
|
+
if (err.name === 'AbortError')
|
|
2214
|
+
return { error: 'generate_image stopped by user', interrupted: true };
|
|
2158
2215
|
return { error: `generate_image failed: ${err.message}` };
|
|
2159
2216
|
}
|
|
2160
2217
|
// 2. Download the image URL into the workspace.
|
|
@@ -2164,7 +2221,7 @@ async function generateImage(input, workDir) {
|
|
|
2164
2221
|
}
|
|
2165
2222
|
catch (_) { }
|
|
2166
2223
|
try {
|
|
2167
|
-
const dl = await _httpsGet(imageUrl, { 'User-Agent': FETCH_USER_AGENT }, 60000);
|
|
2224
|
+
const dl = await _httpsGet(imageUrl, { 'User-Agent': FETCH_USER_AGENT }, 60000, abortSignal);
|
|
2168
2225
|
if (dl.status !== 200)
|
|
2169
2226
|
return { error: `Failed to download generated image (HTTP ${dl.status}).` };
|
|
2170
2227
|
fs.writeFileSync(destPath, dl.body);
|
|
@@ -2173,6 +2230,8 @@ async function generateImage(input, workDir) {
|
|
|
2173
2230
|
return { output: `Generated and saved image to ${destPath} (${kb} KB)${costNote}.` };
|
|
2174
2231
|
}
|
|
2175
2232
|
catch (err) {
|
|
2233
|
+
if (err.name === 'AbortError')
|
|
2234
|
+
return { error: 'generate_image stopped by user (image was generated and billed, but the download was interrupted)', interrupted: true };
|
|
2176
2235
|
return { error: `Failed to save image to ${destPath}: ${err.message}` };
|
|
2177
2236
|
}
|
|
2178
2237
|
}
|
|
@@ -2182,7 +2241,7 @@ async function generateImage(input, workDir) {
|
|
|
2182
2241
|
*
|
|
2183
2242
|
* { query, path?, count?, orientation? }
|
|
2184
2243
|
*/
|
|
2185
|
-
async function stockPhoto(input, workDir) {
|
|
2244
|
+
async function stockPhoto(input, workDir, abortSignal) {
|
|
2186
2245
|
const query = typeof input.query === 'string' ? input.query.trim() : '';
|
|
2187
2246
|
const relPath = typeof input.path === 'string' ? input.path.trim() : '';
|
|
2188
2247
|
const count = typeof input.count === 'number' ? Math.min(Math.max(Math.floor(input.count), 1), 10) : 3;
|
|
@@ -2200,7 +2259,7 @@ async function stockPhoto(input, workDir) {
|
|
|
2200
2259
|
u.searchParams.set('count', String(count));
|
|
2201
2260
|
if (orientation)
|
|
2202
2261
|
u.searchParams.set('orientation', orientation);
|
|
2203
|
-
const r = await _httpsGet(u.toString(), { Authorization: `Bearer ${token}` });
|
|
2262
|
+
const r = await _httpsGet(u.toString(), { Authorization: `Bearer ${token}` }, 20000, abortSignal);
|
|
2204
2263
|
let json = {};
|
|
2205
2264
|
try {
|
|
2206
2265
|
json = JSON.parse(r.body.toString('utf-8'));
|
|
@@ -2213,6 +2272,8 @@ async function stockPhoto(input, workDir) {
|
|
|
2213
2272
|
return { output: `No stock photos found for "${query}".` };
|
|
2214
2273
|
}
|
|
2215
2274
|
catch (err) {
|
|
2275
|
+
if (err.name === 'AbortError')
|
|
2276
|
+
return { error: 'stock_photo stopped by user', interrupted: true };
|
|
2216
2277
|
return { error: `stock_photo search failed: ${err.message}` };
|
|
2217
2278
|
}
|
|
2218
2279
|
// 2a. If path given — download the top result into the workspace.
|
|
@@ -2226,7 +2287,7 @@ async function stockPhoto(input, workDir) {
|
|
|
2226
2287
|
}
|
|
2227
2288
|
catch (_) { }
|
|
2228
2289
|
try {
|
|
2229
|
-
const dl = await _httpsGet(top.url, { 'User-Agent': FETCH_USER_AGENT });
|
|
2290
|
+
const dl = await _httpsGet(top.url, { 'User-Agent': FETCH_USER_AGENT }, 20000, abortSignal);
|
|
2230
2291
|
if (dl.status !== 200)
|
|
2231
2292
|
return { error: `Failed to download photo (HTTP ${dl.status}).` };
|
|
2232
2293
|
fs.writeFileSync(destPath, dl.body);
|
|
@@ -2235,6 +2296,8 @@ async function stockPhoto(input, workDir) {
|
|
|
2235
2296
|
return { output: `Downloaded "${top.alt || query}" to ${destPath} (${kb} KB).${credit}` };
|
|
2236
2297
|
}
|
|
2237
2298
|
catch (err) {
|
|
2299
|
+
if (err.name === 'AbortError')
|
|
2300
|
+
return { error: 'stock_photo download stopped by user', interrupted: true };
|
|
2238
2301
|
return { error: `Failed to save photo to ${destPath}: ${err.message}` };
|
|
2239
2302
|
}
|
|
2240
2303
|
}
|
|
@@ -2259,7 +2322,7 @@ async function stockPhoto(input, workDir) {
|
|
|
2259
2322
|
*
|
|
2260
2323
|
* { query }
|
|
2261
2324
|
*/
|
|
2262
|
-
async function webSearch(input) {
|
|
2325
|
+
async function webSearch(input, _workDir, abortSignal) {
|
|
2263
2326
|
const query = typeof input.query === 'string' ? input.query.trim() : '';
|
|
2264
2327
|
if (!query)
|
|
2265
2328
|
return { error: 'Missing required parameter: query' };
|
|
@@ -2267,7 +2330,7 @@ async function webSearch(input) {
|
|
|
2267
2330
|
if (!token)
|
|
2268
2331
|
return { error: 'Not authenticated. Run `nexrall-code login` first.' };
|
|
2269
2332
|
try {
|
|
2270
|
-
const r = await _httpsPost(`${client_1.API_BASE}/api/code/tools/web_search`, JSON.stringify({ query }), { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, 30000);
|
|
2333
|
+
const r = await _httpsPost(`${client_1.API_BASE}/api/code/tools/web_search`, JSON.stringify({ query }), { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, 30000, abortSignal);
|
|
2271
2334
|
let json = {};
|
|
2272
2335
|
try {
|
|
2273
2336
|
json = JSON.parse(r.body);
|
|
@@ -2281,6 +2344,8 @@ async function webSearch(input) {
|
|
|
2281
2344
|
return { output: text };
|
|
2282
2345
|
}
|
|
2283
2346
|
catch (err) {
|
|
2347
|
+
if (err.name === 'AbortError')
|
|
2348
|
+
return { error: 'web_search stopped by user', interrupted: true };
|
|
2284
2349
|
return { error: `web_search failed: ${err.message}` };
|
|
2285
2350
|
}
|
|
2286
2351
|
}
|
|
@@ -2415,11 +2480,13 @@ const TOOL_MAP = {
|
|
|
2415
2480
|
};
|
|
2416
2481
|
// Tools that accept a workDir for correct relative-path resolution.
|
|
2417
2482
|
// TOOL_MAP stores the base signatures; executeTool injects workDir at call time.
|
|
2483
|
+
// generate_image / stock_photo used to be listed here, but now go through
|
|
2484
|
+
// their own dedicated branch above (which passes abortSignal too) — kept out
|
|
2485
|
+
// of this set so there's exactly one dispatch path per tool, not two.
|
|
2418
2486
|
const WORKDIR_TOOLS = new Set([
|
|
2419
2487
|
'read_file', 'write_file', 'list_directory', 'search_files', 'glob',
|
|
2420
2488
|
'create_directory', 'edit_file', 'multi_edit', 'copy_file', 'move_file',
|
|
2421
2489
|
'delete_file', 'notebook_read', 'notebook_edit',
|
|
2422
|
-
'generate_image', 'stock_photo',
|
|
2423
2490
|
'get_symbols', 'get_workspace_symbols',
|
|
2424
2491
|
'go_to_definition', 'find_references', 'get_hover',
|
|
2425
2492
|
'memory_write', 'memory_read', 'use_skill',
|
|
@@ -2439,6 +2506,19 @@ async function executeTool(name, input, abortSignal, sandbox, workDir, agentScop
|
|
|
2439
2506
|
return await todoWrite(input, agentScope);
|
|
2440
2507
|
if (name === 'todo_read')
|
|
2441
2508
|
return await todoRead(input, agentScope);
|
|
2509
|
+
// Network-bound tools get abortSignal explicitly (not through the generic
|
|
2510
|
+
// WORKDIR_TOOLS branch below, whose signature is (input, workDir) only) —
|
|
2511
|
+
// these are the tools that previously had NO way to stop early on Ctrl+C,
|
|
2512
|
+
// silently making the user wait out their own timeout (up to 170s for
|
|
2513
|
+
// generate_image) instead of returning control immediately.
|
|
2514
|
+
if (name === 'fetch_url')
|
|
2515
|
+
return await fetchUrl(input, workDir, 0, abortSignal);
|
|
2516
|
+
if (name === 'web_search')
|
|
2517
|
+
return await webSearch(input, workDir, abortSignal);
|
|
2518
|
+
if (name === 'generate_image')
|
|
2519
|
+
return await generateImage(input, workDir, abortSignal);
|
|
2520
|
+
if (name === 'stock_photo')
|
|
2521
|
+
return await stockPhoto(input, workDir, abortSignal);
|
|
2442
2522
|
// Inject workDir into all file-system tools so relative paths resolve
|
|
2443
2523
|
// against the workspace root rather than process.cwd().
|
|
2444
2524
|
if (WORKDIR_TOOLS.has(name)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.45",
|
|
4
4
|
"description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nexrall <support@nexrall.com> (https://nexrall.com)",
|