@nexrall/code-core 1.1.0 → 1.3.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/dist/agent/loop.d.ts +15 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +65 -15
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +22 -1
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +18 -6
- package/dist/plugins/installer.d.ts +22 -0
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +35 -0
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +335 -62
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/agent/loop.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
1
|
import type { Message, AgentLoopOptions } from '../types';
|
|
2
|
+
export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
|
|
3
|
+
/**
|
|
4
|
+
* Find the latest index ≤ maxIdx where history can be cut safely.
|
|
5
|
+
*
|
|
6
|
+
* A safe cut point is a **turn boundary**: an assistant message (which always
|
|
7
|
+
* begins a fresh turn after a user message). Cutting there guarantees that
|
|
8
|
+
* `messages[cut..]` starts with an assistant whose `tool_use` blocks are all
|
|
9
|
+
* answered by `tool_result`s that remain in the kept slice — so we never orphan
|
|
10
|
+
* a tool_result (which the API rejects). We deliberately allow cutting across
|
|
11
|
+
* tool_result-bearing user messages: the OLD implementation only cut at a
|
|
12
|
+
* *non*-tool_result user message, which never exists inside a single long
|
|
13
|
+
* agentic run (every user turn is a tool_result), so compaction was a no-op
|
|
14
|
+
* exactly when a long task needs it most.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findSafeCutIndex(messages: Message[], maxIdx: number): number;
|
|
2
17
|
export declare function runAgentLoop(initialMessages: Message[], options: AgentLoopOptions): Promise<Message[]>;
|
|
3
18
|
//# sourceMappingURL=loop.d.ts.map
|
package/dist/agent/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EACjB,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EACjB,MAAM,UAAU,CAAC;AAmKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AAgRD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AA6ED,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA+WpB"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resolveMaxIterations = resolveMaxIterations;
|
|
37
|
+
exports.findSafeCutIndex = findSafeCutIndex;
|
|
36
38
|
exports.runAgentLoop = runAgentLoop;
|
|
37
39
|
const client_1 = require("../api/client");
|
|
38
40
|
const executor_1 = require("../tools/executor");
|
|
@@ -89,13 +91,20 @@ function runToolHooks(entries, phase, toolName, input, workDir, result) {
|
|
|
89
91
|
for (const hook of entry.hooks ?? []) {
|
|
90
92
|
if (hook.type !== 'command' || !hook.command)
|
|
91
93
|
continue;
|
|
94
|
+
// Per-hook timeout. Default 60s (was a hard 10s that made the canonical
|
|
95
|
+
// "auto-run tests on PostToolUse" use-case useless — any real suite is
|
|
96
|
+
// slower). Configurable via `timeout_ms` on the hook, capped at 10min.
|
|
97
|
+
const hookTimeout = typeof hook.timeout_ms === 'number' && hook.timeout_ms > 0
|
|
98
|
+
? Math.min(hook.timeout_ms, 600000)
|
|
99
|
+
: 60000;
|
|
92
100
|
let r;
|
|
93
101
|
try {
|
|
94
102
|
r = (0, child_process_1.spawnSync)(hook.command, {
|
|
95
103
|
shell: true,
|
|
96
104
|
cwd: workDir,
|
|
97
|
-
timeout:
|
|
105
|
+
timeout: hookTimeout,
|
|
98
106
|
encoding: 'utf-8',
|
|
107
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
99
108
|
input: payload,
|
|
100
109
|
env: {
|
|
101
110
|
...process.env,
|
|
@@ -152,18 +161,29 @@ function runSimpleHooks(defs, workDir) {
|
|
|
152
161
|
// fix → …). A too-low cap makes the agent appear to "freeze" mid-task. Keep the
|
|
153
162
|
// default high and let projects raise it further via settings / env.
|
|
154
163
|
const DEFAULT_MAX_ITERATIONS = 500;
|
|
155
|
-
const MAX_ITERATIONS_CEILING = 2000; //
|
|
164
|
+
const MAX_ITERATIONS_CEILING = 2000; // default auto-continue backstop (no explicit opt-in)
|
|
165
|
+
const HARD_ITERATIONS_CAP = 100000; // absolute safety cap — even explicit opt-in can't exceed this
|
|
156
166
|
const STALL_LIMIT = 8; // consecutive all-failed tool rounds → give up (runaway guard)
|
|
157
167
|
// Resolve the soft iteration budget. Precedence:
|
|
158
168
|
// options.maxIterations → env NEXRALL_MAX_ITERATIONS → settings.maxIterations → default
|
|
169
|
+
//
|
|
170
|
+
// The DEFAULT (nothing set) is clamped to MAX_ITERATIONS_CEILING so an ordinary
|
|
171
|
+
// run can never spin past 2000 rounds by accident. But an EXPLICIT value from
|
|
172
|
+
// any of the three opt-in channels is honoured up to HARD_ITERATIONS_CAP — this
|
|
173
|
+
// is what lets a genuinely long task (thousands of rounds) run when the user has
|
|
174
|
+
// deliberately asked for it, instead of dying at a hidden 2000 wall while the
|
|
175
|
+
// error message misleadingly tells them to "raise the limit".
|
|
159
176
|
function resolveMaxIterations(optionValue, settingsRaw) {
|
|
160
177
|
const fromEnv = Number(process.env.NEXRALL_MAX_ITERATIONS);
|
|
161
178
|
const fromSettings = Number(settingsRaw.maxIterations);
|
|
162
|
-
const
|
|
179
|
+
const explicit = (typeof optionValue === 'number' && optionValue > 0) ? optionValue
|
|
163
180
|
: Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv
|
|
164
181
|
: Number.isFinite(fromSettings) && fromSettings > 0 ? fromSettings
|
|
165
|
-
:
|
|
166
|
-
|
|
182
|
+
: undefined;
|
|
183
|
+
if (explicit === undefined)
|
|
184
|
+
return DEFAULT_MAX_ITERATIONS;
|
|
185
|
+
// Explicit opt-in: honour it, only bounded by the absolute safety cap.
|
|
186
|
+
return Math.min(Math.floor(explicit), HARD_ITERATIONS_CAP);
|
|
167
187
|
}
|
|
168
188
|
// When the soft budget is exhausted with work still pending, keep going instead
|
|
169
189
|
// of stopping. Precedence: options.autoContinue → env NEXRALL_AUTO_CONTINUE →
|
|
@@ -342,11 +362,20 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
342
362
|
});
|
|
343
363
|
// Extract final assistant text as the task output
|
|
344
364
|
const lastAssistant = [...result].reverse().find((m) => m.role === 'assistant');
|
|
345
|
-
|
|
365
|
+
let text = (lastAssistant?.content ?? [])
|
|
346
366
|
.filter((b) => b.type === 'text')
|
|
347
367
|
.map((b) => b.text)
|
|
348
368
|
.join('')
|
|
349
369
|
.trim();
|
|
370
|
+
// Cap the sub-agent's returned text so a verbose sub-task (e.g. "list every
|
|
371
|
+
// usage") can't blow up the PARENT's context in a single tool_result. Keep
|
|
372
|
+
// the head + tail — the conclusion is usually at the end.
|
|
373
|
+
const SUBTASK_MAX = 48000; // chars (~12k tokens)
|
|
374
|
+
if (text.length > SUBTASK_MAX) {
|
|
375
|
+
const head = text.slice(0, Math.floor(SUBTASK_MAX * 0.6));
|
|
376
|
+
const tail = text.slice(text.length - Math.floor(SUBTASK_MAX * 0.4));
|
|
377
|
+
text = `${head}\n\n[… sub-task output truncated (${text.length} chars) — kept the beginning and end …]\n\n${tail}`;
|
|
378
|
+
}
|
|
350
379
|
return { output: text || '(sub-task completed with no text output)' };
|
|
351
380
|
}
|
|
352
381
|
catch (err) {
|
|
@@ -382,14 +411,22 @@ function resolveAutoCompact(fromOptions, rawSettings) {
|
|
|
382
411
|
return s;
|
|
383
412
|
return true;
|
|
384
413
|
}
|
|
385
|
-
/**
|
|
414
|
+
/**
|
|
415
|
+
* Find the latest index ≤ maxIdx where history can be cut safely.
|
|
416
|
+
*
|
|
417
|
+
* A safe cut point is a **turn boundary**: an assistant message (which always
|
|
418
|
+
* begins a fresh turn after a user message). Cutting there guarantees that
|
|
419
|
+
* `messages[cut..]` starts with an assistant whose `tool_use` blocks are all
|
|
420
|
+
* answered by `tool_result`s that remain in the kept slice — so we never orphan
|
|
421
|
+
* a tool_result (which the API rejects). We deliberately allow cutting across
|
|
422
|
+
* tool_result-bearing user messages: the OLD implementation only cut at a
|
|
423
|
+
* *non*-tool_result user message, which never exists inside a single long
|
|
424
|
+
* agentic run (every user turn is a tool_result), so compaction was a no-op
|
|
425
|
+
* exactly when a long task needs it most.
|
|
426
|
+
*/
|
|
386
427
|
function findSafeCutIndex(messages, maxIdx) {
|
|
387
|
-
for (let i = Math.min(maxIdx, messages.length - 1); i
|
|
388
|
-
|
|
389
|
-
if (m.role !== 'user')
|
|
390
|
-
continue;
|
|
391
|
-
const hasToolResult = m.content.some((b) => b.type === 'tool_result');
|
|
392
|
-
if (!hasToolResult)
|
|
428
|
+
for (let i = Math.min(maxIdx, messages.length - 1); i >= 2; i--) {
|
|
429
|
+
if (messages[i].role === 'assistant')
|
|
393
430
|
return i;
|
|
394
431
|
}
|
|
395
432
|
return -1;
|
|
@@ -445,7 +482,13 @@ async function autoCompactMessages(messages, options) {
|
|
|
445
482
|
}
|
|
446
483
|
if (!summary)
|
|
447
484
|
return false;
|
|
448
|
-
|
|
485
|
+
// Replace the summarized head with a single user summary message. The cut is
|
|
486
|
+
// at a turn boundary (kept[0] is an assistant message — see findSafeCutIndex),
|
|
487
|
+
// so `user(summary) → assistant(kept[0])` is a valid, well-ordered sequence
|
|
488
|
+
// and no orphaned tool_result is left behind. We intentionally do NOT insert
|
|
489
|
+
// an assistant-ack here: that would put two assistant messages back-to-back
|
|
490
|
+
// (kept[0] is already an assistant), which the API rejects.
|
|
491
|
+
messages.splice(0, cut, { role: 'user', content: [{ type: 'text', text: `[Auto-compacted ${toSummarize.length} earlier messages]\n\nSummary of the earlier conversation so far:\n${summary}\n\nContinue the work from here.` }] });
|
|
449
492
|
// `kept` follows automatically since splice only replaced the head.
|
|
450
493
|
void kept;
|
|
451
494
|
return true;
|
|
@@ -710,8 +753,11 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
710
753
|
const toolResultBlocks = toolResults.map(({ block, result }) => ({
|
|
711
754
|
type: 'tool_result',
|
|
712
755
|
tool_use_id: block.id,
|
|
756
|
+
// When a tool returns BOTH an error and partial output (e.g. a bash
|
|
757
|
+
// command that timed out mid-test-run), surface both so the model can
|
|
758
|
+
// see WHY it hung — the failure detail is usually in the output tail.
|
|
713
759
|
content: result.error
|
|
714
|
-
? `Error: ${result.error}`
|
|
760
|
+
? (result.output ? `Error: ${result.error}\n\n${result.output}` : `Error: ${result.error}`)
|
|
715
761
|
: result.output ?? '',
|
|
716
762
|
is_error: result.error !== undefined,
|
|
717
763
|
}));
|
|
@@ -731,6 +777,10 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
731
777
|
content: toolResultContent,
|
|
732
778
|
};
|
|
733
779
|
messages.push(toolResultMessage);
|
|
780
|
+
// Incremental persistence hook: history is valid here (ends on a
|
|
781
|
+
// tool_result user turn). Let the caller checkpoint progress so a crash
|
|
782
|
+
// mid-run loses only the in-flight step, not the whole session.
|
|
783
|
+
options.onProgress?.(messages);
|
|
734
784
|
// Runaway guard: if every tool call in this round failed, count it. Enough
|
|
735
785
|
// consecutive all-failed rounds (e.g. a command that always errors, or the
|
|
736
786
|
// user denying every permission) means we're stuck — stop instead of
|
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAiBlD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAiBlD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CA0WlB;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}
|
package/dist/api/client.js
CHANGED
|
@@ -316,7 +316,19 @@ async function streamChat(messages, options, onEvent) {
|
|
|
316
316
|
}
|
|
317
317
|
parser.feed(chunk.toString('utf-8'));
|
|
318
318
|
});
|
|
319
|
-
stream.on('end', () => {
|
|
319
|
+
stream.on('end', () => {
|
|
320
|
+
clearInterval(heartbeatWatchdog);
|
|
321
|
+
// A clean 'end' with ZERO model events means the connection was dropped before
|
|
322
|
+
// the backend produced anything (deploy restart, proxy reset). Without this,
|
|
323
|
+
// the fallback below returns an EMPTY assistant message and the agent loop
|
|
324
|
+
// treats it as a silent no-op — the turn just vanishes. Nothing was emitted,
|
|
325
|
+
// so a retry cannot duplicate output → tag retryable.
|
|
326
|
+
if (!sawModelEvent && !completedMessage) {
|
|
327
|
+
reject(Object.assign(new Error('Connection closed before the model responded. Retrying…'), { retryable: true }));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
resolve();
|
|
331
|
+
});
|
|
320
332
|
// Treat stream destruction from abort as a clean exit, not an error
|
|
321
333
|
stream.on('error', (err) => {
|
|
322
334
|
clearInterval(heartbeatWatchdog);
|
|
@@ -324,6 +336,15 @@ async function streamChat(messages, options, onEvent) {
|
|
|
324
336
|
resolve();
|
|
325
337
|
return;
|
|
326
338
|
}
|
|
339
|
+
// Transport-level failures ("Premature close" / ECONNRESET / socket hang up)
|
|
340
|
+
// happen whenever the backend restarts mid-deploy or a proxy drops the socket.
|
|
341
|
+
// They are exactly as transient as an overloaded_error — if nothing has been
|
|
342
|
+
// emitted to the caller yet, retry the attempt transparently instead of
|
|
343
|
+
// surfacing "Stream failed: Premature close" and killing the whole turn.
|
|
344
|
+
if (!emittedToCaller) {
|
|
345
|
+
reject(Object.assign(err, { retryable: true }));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
327
348
|
reject(err);
|
|
328
349
|
});
|
|
329
350
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"AAwBA,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,eAAe,CAAC;IAC7B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAsBD,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAoB5D;
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"AAwBA,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,eAAe,CAAC;IAC7B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAsBD,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAoB5D;AAgID;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,kBAAkB,GAAG,IAAI,CAK3B"}
|
|
@@ -118,7 +118,17 @@ function matchPattern(pattern, value) {
|
|
|
118
118
|
// "name:*" → prefix match (Claude Bash semantics)
|
|
119
119
|
if (pattern.endsWith(':*')) {
|
|
120
120
|
const prefix = pattern.slice(0, -2);
|
|
121
|
-
|
|
121
|
+
if (value === prefix)
|
|
122
|
+
return true;
|
|
123
|
+
// Token-boundary prefix: "npm run test:*" must match "npm run test foo"
|
|
124
|
+
// but NOT "npm run testify-malware". The char right after the prefix has
|
|
125
|
+
// to be a shell token boundary (whitespace / end), otherwise it's a
|
|
126
|
+
// different command that merely shares a textual prefix.
|
|
127
|
+
if (value.startsWith(prefix)) {
|
|
128
|
+
const next = value.charAt(prefix.length);
|
|
129
|
+
return next === '' || /\s/.test(next);
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
122
132
|
}
|
|
123
133
|
if (pattern.includes('*') || pattern.includes('?')) {
|
|
124
134
|
try {
|
|
@@ -186,15 +196,17 @@ function ruleMatches(rule, tool, input, workDir, opts) {
|
|
|
186
196
|
(aliases[ruleTool]?.includes(tool) ?? false);
|
|
187
197
|
if (!matchesTool)
|
|
188
198
|
return false;
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
199
|
+
// Chaining guard for ALLOW rules on bash: a patterned allow rule (prefix or
|
|
200
|
+
// glob) must never auto-approve a command that chains a SECOND command onto
|
|
201
|
+
// the matched one. A newline acts like `;`, and `;`, `&&`, `||`, `|`, `&`,
|
|
202
|
+
// `$(...)`, `` `...` `` all let "npm run test; rm -rf ~" ride an
|
|
192
203
|
// "Bash(npm run test:*)" allow rule straight past the user prompt.
|
|
193
|
-
// Deny rules stay aggressive (they SHOULD match
|
|
204
|
+
// Deny rules stay aggressive (they SHOULD match chained commands), and a
|
|
194
205
|
// bare "Bash" allow (pattern === null) is an explicit "allow everything".
|
|
195
206
|
if (opts?.forAllow && tool === 'bash' && pattern !== null && pattern !== '' && pattern !== '*') {
|
|
196
207
|
const cmd = typeof input.command === 'string' ? input.command : '';
|
|
197
|
-
|
|
208
|
+
// newline / ; / && / || / | / trailing & / command-substitution / backtick
|
|
209
|
+
if (/[\r\n;`]|&&|\|\||\||&\s*$|\$\(/.test(cmd))
|
|
198
210
|
return false;
|
|
199
211
|
}
|
|
200
212
|
if (pattern === null || pattern === '' || pattern === '*')
|
|
@@ -33,6 +33,28 @@ export interface InstallResult {
|
|
|
33
33
|
scope: 'project' | 'global';
|
|
34
34
|
inspection: PluginInspection;
|
|
35
35
|
}
|
|
36
|
+
export interface RegistryPlugin {
|
|
37
|
+
name: string;
|
|
38
|
+
display_name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
source: string;
|
|
41
|
+
author: string;
|
|
42
|
+
homepage?: string;
|
|
43
|
+
keywords: string[];
|
|
44
|
+
has_commands: boolean;
|
|
45
|
+
has_agents: boolean;
|
|
46
|
+
has_hooks: boolean;
|
|
47
|
+
has_mcp: boolean;
|
|
48
|
+
verified: boolean;
|
|
49
|
+
official: boolean;
|
|
50
|
+
install_count: number;
|
|
51
|
+
}
|
|
52
|
+
/** Search the registry (empty query = list all, official first). */
|
|
53
|
+
export declare function searchRegistry(query?: string, limit?: number): Promise<RegistryPlugin[]>;
|
|
54
|
+
/** Fetch one plugin's registry entry, or null when it isn't listed. */
|
|
55
|
+
export declare function getRegistryPlugin(name: string): Promise<RegistryPlugin | null>;
|
|
56
|
+
/** Fire-and-forget install counter. Never throws, never blocks an install. */
|
|
57
|
+
export declare function reportInstall(name: string): void;
|
|
36
58
|
/** Parse a user-supplied plugin spec into a structured source. Throws on junk. */
|
|
37
59
|
export declare function parsePluginSource(spec: string): PluginSource;
|
|
38
60
|
/** What does this plugin contain? Callers must warn on hasHooks/hasMcp. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/plugins/installer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/plugins/installer.ts"],"names":[],"mappings":"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"}
|
|
@@ -33,6 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.searchRegistry = searchRegistry;
|
|
37
|
+
exports.getRegistryPlugin = getRegistryPlugin;
|
|
38
|
+
exports.reportInstall = reportInstall;
|
|
36
39
|
exports.parsePluginSource = parsePluginSource;
|
|
37
40
|
exports.inspectPluginDir = inspectPluginDir;
|
|
38
41
|
exports.installPlugin = installPlugin;
|
|
@@ -43,7 +46,39 @@ const fs = __importStar(require("fs"));
|
|
|
43
46
|
const path = __importStar(require("path"));
|
|
44
47
|
const os = __importStar(require("os"));
|
|
45
48
|
const child_process_1 = require("child_process");
|
|
49
|
+
const client_1 = require("../api/client");
|
|
46
50
|
const RECEIPT_FILE = '_install.json';
|
|
51
|
+
/** Search the registry (empty query = list all, official first). */
|
|
52
|
+
async function searchRegistry(query, limit = 50) {
|
|
53
|
+
const u = new URL(`${client_1.API_BASE}/api/code/plugins`);
|
|
54
|
+
if (query)
|
|
55
|
+
u.searchParams.set('q', query);
|
|
56
|
+
u.searchParams.set('limit', String(limit));
|
|
57
|
+
const res = await fetch(u, { headers: { Accept: 'application/json' } });
|
|
58
|
+
if (!res.ok)
|
|
59
|
+
throw new Error(`Registry unavailable (${res.status}).`);
|
|
60
|
+
const j = (await res.json());
|
|
61
|
+
return j.plugins ?? [];
|
|
62
|
+
}
|
|
63
|
+
/** Fetch one plugin's registry entry, or null when it isn't listed. */
|
|
64
|
+
async function getRegistryPlugin(name) {
|
|
65
|
+
if (!/^[\w.-]{1,64}$/.test(name))
|
|
66
|
+
return null;
|
|
67
|
+
const res = await fetch(`${client_1.API_BASE}/api/code/plugins/${encodeURIComponent(name)}`, {
|
|
68
|
+
headers: { Accept: 'application/json' },
|
|
69
|
+
});
|
|
70
|
+
if (res.status === 404)
|
|
71
|
+
return null;
|
|
72
|
+
if (!res.ok)
|
|
73
|
+
throw new Error(`Registry unavailable (${res.status}).`);
|
|
74
|
+
const j = (await res.json());
|
|
75
|
+
return j.plugin ?? null;
|
|
76
|
+
}
|
|
77
|
+
/** Fire-and-forget install counter. Never throws, never blocks an install. */
|
|
78
|
+
function reportInstall(name) {
|
|
79
|
+
fetch(`${client_1.API_BASE}/api/code/plugins/${encodeURIComponent(name)}/installed`, { method: 'POST' })
|
|
80
|
+
.catch(() => { });
|
|
81
|
+
}
|
|
47
82
|
// ─── Source parsing ───────────────────────────────────────────────────────────
|
|
48
83
|
/** Parse a user-supplied plugin spec into a structured source. Throws on junk. */
|
|
49
84
|
function parsePluginSource(spec) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"
|
|
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;AA6qDtE,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,GAClB,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
|
package/dist/tools/executor.js
CHANGED
|
@@ -39,6 +39,7 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const os = __importStar(require("os"));
|
|
40
40
|
const https = __importStar(require("https"));
|
|
41
41
|
const http = __importStar(require("http"));
|
|
42
|
+
const dns = __importStar(require("dns"));
|
|
42
43
|
const child_process_1 = require("child_process");
|
|
43
44
|
const sandbox_1 = require("./sandbox");
|
|
44
45
|
const auth_1 = require("../auth");
|
|
@@ -69,15 +70,31 @@ const BLOCKED_REGEXES = [
|
|
|
69
70
|
/dd\s+if=\/dev\/(zero|random|urandom)/,
|
|
70
71
|
// Fork bomb
|
|
71
72
|
/:\(\)\s*\{.*:\|:&/,
|
|
72
|
-
// Pipe execution from network. [^|\n]* between the fetcher and the pipe
|
|
73
|
-
// covers the URL/flags ("curl https://x.sh | sh"), which the old \s*-only
|
|
74
|
-
// version missed entirely; no-space variants (curl|bash) are still caught.
|
|
75
|
-
/curl[^|\n]*\|\s*(bash|sh|python\d*|node|perl|ruby)\b/,
|
|
76
|
-
/wget[^|\n]*\|\s*(bash|sh|python\d*|node|perl|ruby)\b/,
|
|
77
|
-
/curl\s+.*-[oO]\s*-.*\|\s*(bash|sh)/, // curl -o - … | bash
|
|
78
73
|
// Credential theft
|
|
79
74
|
/cat\s+\/etc\/(shadow|passwd)/,
|
|
80
75
|
];
|
|
76
|
+
// Network-pipe rules (curl … | sh) get their own tier. Testing them against the
|
|
77
|
+
// FULL string caused constant false positives: the pattern showing up as DATA —
|
|
78
|
+
// a heredoc'd README (`cat > README.md <<EOF … install.sh | sh … EOF`), a
|
|
79
|
+
// commit message (`git commit -m "docs: explain curl … | sh risk"`), an echo —
|
|
80
|
+
// is never executed by this bash invocation. So these run against the command
|
|
81
|
+
// with heredoc bodies AND quoted strings stripped (stripDataSections).
|
|
82
|
+
// The one real execution path that lives INSIDE quotes — `bash -c "curl … |
|
|
83
|
+
// sh"` — is covered by the dedicated SHELL_DASH_C rule below, which only fires
|
|
84
|
+
// when the quoted pipe is the argument of an interpreter's -c flag.
|
|
85
|
+
// [^|\n]* between the fetcher and the pipe covers the URL/flags ("curl
|
|
86
|
+
// https://x.sh | sh"); no-space variants (curl|bash) are still caught.
|
|
87
|
+
// python is exempted for `-m json.tool` (harmless pretty-printer: the
|
|
88
|
+
// downloaded bytes are parsed as JSON, never executed).
|
|
89
|
+
const NETWORK_PIPE_REGEXES = [
|
|
90
|
+
/curl[^|\n]*\|\s*(bash|sh|python\d*(?!\s+-m\s+json\.tool)|node|perl|ruby)\b/,
|
|
91
|
+
/wget[^|\n]*\|\s*(bash|sh|python\d*(?!\s+-m\s+json\.tool)|node|perl|ruby)\b/,
|
|
92
|
+
/curl\s+.*-[oO]\s*-.*\|\s*(bash|sh)\b/, // curl -o - … | bash
|
|
93
|
+
];
|
|
94
|
+
// `bash -c "curl … | sh"` / `sh -c 'wget … | bash'` — quoted, but executed for
|
|
95
|
+
// real. Matched at a command position on the raw (heredoc-stripped) string so
|
|
96
|
+
// the quotes are still visible to the regex.
|
|
97
|
+
const SHELL_DASH_C_PIPE = /(?:^|[;&|`\n(]|\$\(|\bsudo\s+|\bexec\s+)\s*(?:bash|sh|zsh|dash)\s+(?:-\w+\s+)*-c\s+(["'])[^"']*(?:curl|wget)[^"']*\|[^"']*\1?/;
|
|
81
98
|
// Tier 2 — words that are dangerous only when INVOKED as a command. A plain
|
|
82
99
|
// \b-word match caused constant false positives once heredocs were allowed:
|
|
83
100
|
// `grep reboot README.md`, `git log --grep shutdown`, or a heredoc body that
|
|
@@ -90,11 +107,13 @@ const BLOCKED_COMMAND_WORDS = /(?:^|[;&|`\n(]|\$\(|\bsudo\s+|\bdoas\s+|\bexec\s+
|
|
|
90
107
|
// Heredoc bodies first (<<TAG / <<-TAG / <<'TAG' / <<"TAG" … up to the line
|
|
91
108
|
// that equals TAG), then double- and single-quoted strings. Intentionally
|
|
92
109
|
// naive — this feeds a blocklist heuristic, not an executor.
|
|
110
|
+
function stripHeredocs(command) {
|
|
111
|
+
// Heredoc: the (['"]?) quote group always participates, so the \2 tag
|
|
112
|
+
// backreference is safe (JS backrefs to non-participating groups match "").
|
|
113
|
+
return command.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\2(?=\s|;|&|$)/g, ' ');
|
|
114
|
+
}
|
|
93
115
|
function stripDataSections(command) {
|
|
94
|
-
return command
|
|
95
|
-
// Heredoc: the (['"]?) quote group always participates, so the \2 tag
|
|
96
|
-
// backreference is safe (JS backrefs to non-participating groups match "").
|
|
97
|
-
.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\2(?=\s|;|&|$)/g, ' ')
|
|
116
|
+
return stripHeredocs(command)
|
|
98
117
|
.replace(/"(?:[^"\\]|\\[\s\S])*"/g, '""')
|
|
99
118
|
.replace(/'[^']*'/g, "''");
|
|
100
119
|
}
|
|
@@ -104,6 +123,19 @@ function isCommandBlocked(command) {
|
|
|
104
123
|
if (re.test(lower))
|
|
105
124
|
return re.toString();
|
|
106
125
|
}
|
|
126
|
+
// Network-pipe rules see the command with data sections (heredoc bodies,
|
|
127
|
+
// quoted strings) removed — mentions in docs/commit messages are data, not
|
|
128
|
+
// something this invocation executes.
|
|
129
|
+
const noData = stripDataSections(lower);
|
|
130
|
+
for (const re of NETWORK_PIPE_REGEXES) {
|
|
131
|
+
if (re.test(noData))
|
|
132
|
+
return re.toString();
|
|
133
|
+
}
|
|
134
|
+
// …but an interpreter -c argument IS executed: check it on the raw string
|
|
135
|
+
// (heredoc-stripped only, quotes intact).
|
|
136
|
+
if (SHELL_DASH_C_PIPE.test(stripHeredocs(lower))) {
|
|
137
|
+
return SHELL_DASH_C_PIPE.toString();
|
|
138
|
+
}
|
|
107
139
|
if (BLOCKED_COMMAND_WORDS.test(stripDataSections(lower))) {
|
|
108
140
|
return BLOCKED_COMMAND_WORDS.toString();
|
|
109
141
|
}
|
|
@@ -297,21 +329,26 @@ const _bgShells = new Map();
|
|
|
297
329
|
let _bgCounter = 0;
|
|
298
330
|
function startBackgroundShell(command, displayCommand, workDir, note) {
|
|
299
331
|
const id = `bg_${++_bgCounter}`;
|
|
300
|
-
|
|
332
|
+
// detached:true → own process group so kill_shell can signal the whole tree
|
|
333
|
+
// (a background dev server usually spawns children that must die with it).
|
|
334
|
+
const child = (0, child_process_1.spawn)(command, { shell: true, cwd: workDir ?? process.cwd(), env: process.env, detached: true });
|
|
301
335
|
const shell = {
|
|
302
336
|
id, command: displayCommand, child, output: '', readCursor: 0, truncated: false,
|
|
303
337
|
status: 'running', exitCode: null,
|
|
304
338
|
};
|
|
339
|
+
// Rolling buffer: keep the LAST MAX_OUTPUT_CHARS instead of the first, so a
|
|
340
|
+
// long-lived watcher/dev-server's recent output (the useful part) is never
|
|
341
|
+
// permanently lost behind an early cap.
|
|
305
342
|
const append = (chunk) => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
shell.output
|
|
343
|
+
shell.output += chunk;
|
|
344
|
+
if (shell.output.length > MAX_OUTPUT_CHARS) {
|
|
345
|
+
// Drop from the front but never behind the read cursor's logical position;
|
|
346
|
+
// readCursor is an absolute offset so we track how much we've discarded.
|
|
347
|
+
const overflow = shell.output.length - MAX_OUTPUT_CHARS;
|
|
348
|
+
shell.output = shell.output.slice(overflow);
|
|
349
|
+
shell.readCursor = Math.max(0, shell.readCursor - overflow);
|
|
311
350
|
shell.truncated = true;
|
|
312
351
|
}
|
|
313
|
-
else
|
|
314
|
-
shell.output += chunk;
|
|
315
352
|
};
|
|
316
353
|
child.stdout?.on('data', (c) => append(c.toString('utf-8')));
|
|
317
354
|
child.stderr?.on('data', (c) => append(c.toString('utf-8')));
|
|
@@ -357,18 +394,22 @@ async function killShell(input) {
|
|
|
357
394
|
return { error: `No background shell with id "${id}".` };
|
|
358
395
|
if (shell.status === 'running') {
|
|
359
396
|
shell.status = 'killed';
|
|
360
|
-
|
|
361
|
-
shell.child.kill('SIGTERM');
|
|
362
|
-
}
|
|
363
|
-
catch { /* ignore */ }
|
|
364
|
-
// Escalate to SIGKILL after 3 s if process is still alive
|
|
365
|
-
setTimeout(() => {
|
|
397
|
+
const killTree = (sig) => {
|
|
366
398
|
try {
|
|
367
|
-
if (shell.
|
|
368
|
-
shell.child.
|
|
399
|
+
if (shell.child.pid)
|
|
400
|
+
process.kill(-shell.child.pid, sig);
|
|
369
401
|
}
|
|
370
|
-
catch {
|
|
371
|
-
|
|
402
|
+
catch {
|
|
403
|
+
try {
|
|
404
|
+
shell.child.kill(sig);
|
|
405
|
+
}
|
|
406
|
+
catch { /* already dead */ }
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
killTree('SIGTERM');
|
|
410
|
+
// Escalate to SIGKILL after 3 s if process group is still alive
|
|
411
|
+
setTimeout(() => { if (shell.status === 'killed')
|
|
412
|
+
killTree('SIGKILL'); }, 3000);
|
|
372
413
|
}
|
|
373
414
|
return { output: `Background shell ${id} ${shell.status}.` };
|
|
374
415
|
}
|
|
@@ -416,43 +457,80 @@ async function bash(input, abortSignal, sandbox, workDir) {
|
|
|
416
457
|
r.output = `[${sandboxNote}]\n${r.output}`;
|
|
417
458
|
return r;
|
|
418
459
|
};
|
|
460
|
+
// detached:true puts the child in its own process group so we can signal the
|
|
461
|
+
// WHOLE tree (shell + its children — dev servers, test runners) on timeout /
|
|
462
|
+
// abort. Without it, kill() only hits the /bin/sh wrapper and leaves orphaned
|
|
463
|
+
// grandchildren holding ports (→ EADDRINUSE on the next run).
|
|
419
464
|
const child = (0, child_process_1.spawn)(effectiveCommand, {
|
|
420
465
|
shell: true,
|
|
421
466
|
cwd: workDir ?? process.cwd(),
|
|
422
467
|
env: process.env,
|
|
468
|
+
detached: true,
|
|
423
469
|
});
|
|
424
|
-
|
|
470
|
+
// Kill the child's entire process group (negative pid). Falls back to a
|
|
471
|
+
// plain kill if the group signal fails (e.g. process already gone).
|
|
472
|
+
const killTree = (sig) => {
|
|
473
|
+
try {
|
|
474
|
+
if (child.pid)
|
|
475
|
+
process.kill(-child.pid, sig);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
try {
|
|
479
|
+
child.kill(sig);
|
|
480
|
+
}
|
|
481
|
+
catch { /* already dead */ }
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
// Keep HEAD + TAIL rather than head-only: test runners print the failure
|
|
485
|
+
// summary at the END, so a large passing-then-failing suite must not lose its
|
|
486
|
+
// tail. We retain the first HEAD_CHARS and the last TAIL_CHARS.
|
|
487
|
+
const HEAD_CHARS = Math.floor(MAX_OUTPUT_CHARS * 0.25);
|
|
488
|
+
const TAIL_CHARS = MAX_OUTPUT_CHARS - HEAD_CHARS;
|
|
489
|
+
let head = '';
|
|
490
|
+
let tail = '';
|
|
491
|
+
let totalLen = 0;
|
|
425
492
|
let truncated = false;
|
|
426
493
|
let settled = false;
|
|
427
|
-
// Cap output in the data handler so we never accumulate more than MAX_OUTPUT_CHARS in RAM
|
|
428
494
|
const appendOutput = (chunk) => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
495
|
+
totalLen += chunk.length;
|
|
496
|
+
if (head.length < HEAD_CHARS) {
|
|
497
|
+
const room = HEAD_CHARS - head.length;
|
|
498
|
+
head += chunk.slice(0, room);
|
|
499
|
+
chunk = chunk.slice(room);
|
|
500
|
+
if (!chunk)
|
|
501
|
+
return;
|
|
435
502
|
}
|
|
436
|
-
|
|
437
|
-
|
|
503
|
+
// Remaining goes into a rolling tail buffer.
|
|
504
|
+
tail += chunk;
|
|
505
|
+
if (tail.length > TAIL_CHARS) {
|
|
506
|
+
tail = tail.slice(tail.length - TAIL_CHARS);
|
|
507
|
+
truncated = true;
|
|
438
508
|
}
|
|
439
509
|
};
|
|
510
|
+
const collect = () => {
|
|
511
|
+
if (!truncated)
|
|
512
|
+
return head + tail;
|
|
513
|
+
const omitted = totalLen - head.length - tail.length;
|
|
514
|
+
return `${head}\n\n[… ${omitted} chars omitted (kept first ${HEAD_CHARS / 1024}KB + last ${Math.round(TAIL_CHARS / 1024)}KB) …]\n\n${tail}`;
|
|
515
|
+
};
|
|
440
516
|
const done = (code, signal, timedOut = false) => {
|
|
441
517
|
if (settled)
|
|
442
518
|
return;
|
|
443
519
|
settled = true;
|
|
444
|
-
const
|
|
520
|
+
const body = collect();
|
|
445
521
|
if (timedOut) {
|
|
446
|
-
|
|
522
|
+
// Return the partial output alongside the error — it usually holds the
|
|
523
|
+
// reason the command hung (a stuck test, a prompt, a slow step).
|
|
524
|
+
resolve(prependNote({ error: `Command timed out after ${timeoutMs}ms`, interrupted: true, output: body ? `Partial output before timeout:\n${body}` : undefined }));
|
|
447
525
|
}
|
|
448
526
|
else if (abortSignal?.aborted) {
|
|
449
|
-
resolve({ error: 'Command stopped by user', interrupted: true });
|
|
527
|
+
resolve({ error: 'Command stopped by user', interrupted: true, output: body || undefined });
|
|
450
528
|
}
|
|
451
529
|
else if (code !== 0) {
|
|
452
|
-
resolve(prependNote({ output:
|
|
530
|
+
resolve(prependNote({ output: body + `\n[Exit code: ${code ?? signal ?? 'unknown'}]` }));
|
|
453
531
|
}
|
|
454
532
|
else {
|
|
455
|
-
resolve(prependNote({ output:
|
|
533
|
+
resolve(prependNote({ output: body }));
|
|
456
534
|
}
|
|
457
535
|
};
|
|
458
536
|
child.stdout?.on('data', (chunk) => appendOutput(chunk.toString('utf-8')));
|
|
@@ -464,13 +542,13 @@ async function bash(input, abortSignal, sandbox, workDir) {
|
|
|
464
542
|
clearTimeout(timer);
|
|
465
543
|
resolve({ error: err.message });
|
|
466
544
|
});
|
|
467
|
-
// Timeout kill: SIGTERM
|
|
545
|
+
// Timeout kill: SIGTERM the whole group, escalate to SIGKILL after 3 s
|
|
468
546
|
const timer = setTimeout(() => {
|
|
469
547
|
if (settled)
|
|
470
548
|
return;
|
|
471
|
-
|
|
549
|
+
killTree('SIGTERM');
|
|
472
550
|
setTimeout(() => { if (!settled)
|
|
473
|
-
|
|
551
|
+
killTree('SIGKILL'); }, 3000);
|
|
474
552
|
done(null, null, true);
|
|
475
553
|
}, timeoutMs);
|
|
476
554
|
child.on('close', () => clearTimeout(timer));
|
|
@@ -478,7 +556,7 @@ async function bash(input, abortSignal, sandbox, workDir) {
|
|
|
478
556
|
const pollAbort = setInterval(() => {
|
|
479
557
|
if (abortSignal?.aborted && !settled) {
|
|
480
558
|
clearInterval(pollAbort);
|
|
481
|
-
|
|
559
|
+
killTree('SIGTERM');
|
|
482
560
|
done(null, null, false);
|
|
483
561
|
}
|
|
484
562
|
}, 200);
|
|
@@ -528,6 +606,11 @@ async function searchFiles(input, workDir) {
|
|
|
528
606
|
else {
|
|
529
607
|
// Content search — prefer ripgrep (faster, .gitignore-aware) when
|
|
530
608
|
// installed, fall back to plain grep with hardcoded excludes otherwise.
|
|
609
|
+
// maxBuffer is raised well past the default 1 MB: on a large repo a common
|
|
610
|
+
// pattern can emit tens of MB and the default silently truncates + sets
|
|
611
|
+
// status:null (ENOBUFS), which used to surface as an empty "search failed".
|
|
612
|
+
const SEARCH_MAX_BUFFER = 64 * 1024 * 1024; // 64 MB
|
|
613
|
+
const spawnOpts = { encoding: 'utf-8', timeout: DEFAULT_TIMEOUT_MS, maxBuffer: SEARCH_MAX_BUFFER };
|
|
531
614
|
let result;
|
|
532
615
|
if (ripgrepAvailable()) {
|
|
533
616
|
const args = ['--line-number', '--no-heading', '--color=never', '--hidden', '--glob', '!.git'];
|
|
@@ -537,8 +620,10 @@ async function searchFiles(input, workDir) {
|
|
|
537
620
|
args.push(`-C${contextLines}`);
|
|
538
621
|
if (include)
|
|
539
622
|
args.push('--glob', include);
|
|
623
|
+
// -m is PER-FILE in rg/grep; the real global cap is applied on the
|
|
624
|
+
// output below. Keep a generous per-file cap so no single file floods.
|
|
540
625
|
args.push('-m', '200', '--regexp', pattern, resolved);
|
|
541
|
-
result = (0, child_process_1.spawnSync)('rg', args,
|
|
626
|
+
result = (0, child_process_1.spawnSync)('rg', args, spawnOpts);
|
|
542
627
|
}
|
|
543
628
|
else {
|
|
544
629
|
const args = ['-rn', '--binary-files=without-match', '--color=never'];
|
|
@@ -549,17 +634,31 @@ async function searchFiles(input, workDir) {
|
|
|
549
634
|
if (include)
|
|
550
635
|
args.push(`--include=${include}`);
|
|
551
636
|
args.push('--exclude-dir=.git', '--exclude-dir=node_modules', '--exclude-dir=dist', '--exclude-dir=.next', '--exclude-dir=__pycache__', '--exclude-dir=.turbo', '--exclude-dir=coverage', '--exclude-dir=.cache', '-m', '200', pattern, resolved);
|
|
552
|
-
result = (0, child_process_1.spawnSync)('grep', args,
|
|
637
|
+
result = (0, child_process_1.spawnSync)('grep', args, spawnOpts);
|
|
553
638
|
}
|
|
554
639
|
let output = result.stdout ?? '';
|
|
555
640
|
const stderr = result.stderr ?? '';
|
|
641
|
+
// Distinguish real failure modes. spawnSync sets `.error` (not `.status`)
|
|
642
|
+
// for timeout (ETIMEDOUT), buffer overflow (ENOBUFS) and spawn failures.
|
|
643
|
+
const spawnErr = result.error;
|
|
644
|
+
if (spawnErr) {
|
|
645
|
+
if (spawnErr.code === 'ETIMEDOUT' || result.signal === 'SIGTERM') {
|
|
646
|
+
const partial = output ? `\n\nPartial results before timeout:\n${globalCapMatches(output)}` : '';
|
|
647
|
+
return { error: `Search timed out after ${Math.round(DEFAULT_TIMEOUT_MS / 1000)}s — narrow the path or pattern (or add an "include" filter).${partial}` };
|
|
648
|
+
}
|
|
649
|
+
if (spawnErr.code === 'ENOBUFS') {
|
|
650
|
+
// We still captured up to maxBuffer of output — return the capped head
|
|
651
|
+
// instead of failing blind.
|
|
652
|
+
return { output: `${globalCapMatches(output)}\n[Too many matches — output exceeded ${SEARCH_MAX_BUFFER / (1024 * 1024)}MB and was capped. Narrow the pattern/path.]` };
|
|
653
|
+
}
|
|
654
|
+
return { error: `search failed: ${spawnErr.message}` };
|
|
655
|
+
}
|
|
556
656
|
// Both rg and grep use exit code 1 = "no matches", 2 = error.
|
|
557
657
|
if (result.status === 1 && !output)
|
|
558
658
|
return { output: 'No matches found.' };
|
|
559
659
|
if (result.status !== 0 && result.status !== 1)
|
|
560
660
|
return { error: stderr || 'search failed' };
|
|
561
|
-
|
|
562
|
-
output = output.slice(0, MAX_OUTPUT_CHARS) + '\n[Output truncated at 100KB]';
|
|
661
|
+
output = globalCapMatches(output);
|
|
563
662
|
return { output: output || 'No matches found.' };
|
|
564
663
|
}
|
|
565
664
|
}
|
|
@@ -567,6 +666,29 @@ async function searchFiles(input, workDir) {
|
|
|
567
666
|
return { error: err.message };
|
|
568
667
|
}
|
|
569
668
|
}
|
|
669
|
+
/**
|
|
670
|
+
* Enforce the documented global cap on grep/rg output: at most 200 matched
|
|
671
|
+
* lines (the tool contract) AND at most MAX_OUTPUT_CHARS bytes. The `-m 200`
|
|
672
|
+
* flag on rg/grep is PER-FILE, so a large repo can blow well past 200 total —
|
|
673
|
+
* this applies the real global ceiling after the fact.
|
|
674
|
+
*/
|
|
675
|
+
function globalCapMatches(output) {
|
|
676
|
+
if (!output)
|
|
677
|
+
return output;
|
|
678
|
+
const GLOBAL_MATCH_CAP = 200;
|
|
679
|
+
const lines = output.split('\n');
|
|
680
|
+
let capped = output;
|
|
681
|
+
let note = '';
|
|
682
|
+
if (lines.length > GLOBAL_MATCH_CAP) {
|
|
683
|
+
capped = lines.slice(0, GLOBAL_MATCH_CAP).join('\n');
|
|
684
|
+
note = `\n[Showing first ${GLOBAL_MATCH_CAP} of ${lines.length} matched lines — narrow the pattern/path for the rest.]`;
|
|
685
|
+
}
|
|
686
|
+
if (capped.length > MAX_OUTPUT_CHARS) {
|
|
687
|
+
capped = capped.slice(0, MAX_OUTPUT_CHARS);
|
|
688
|
+
note = `\n[Output truncated at ${MAX_OUTPUT_CHARS / 1024}KB.]`;
|
|
689
|
+
}
|
|
690
|
+
return capped + note;
|
|
691
|
+
}
|
|
570
692
|
function matchesGlob(name, pattern) {
|
|
571
693
|
// Convert simple glob (* → .*, ? → .) to regex
|
|
572
694
|
try {
|
|
@@ -694,6 +816,42 @@ function literalReplace(haystack, needle, replacement) {
|
|
|
694
816
|
return haystack;
|
|
695
817
|
return haystack.slice(0, idx) + replacement + haystack.slice(idx + needle.length);
|
|
696
818
|
}
|
|
819
|
+
// Max size an edit tool will load into memory. Guards against OOM on a huge file
|
|
820
|
+
// and mirrors read_file's cap (edits target source, not multi-MB assets).
|
|
821
|
+
const MAX_EDIT_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
822
|
+
/**
|
|
823
|
+
* Guard a path for in-place editing: reject missing files, oversize files, and
|
|
824
|
+
* binaries (a text edit on a binary corrupts it, and CRLF-normalize mangles
|
|
825
|
+
* embedded \r\n). Returns an error string, or the file's mode + content.
|
|
826
|
+
*/
|
|
827
|
+
function preflightEdit(resolved) {
|
|
828
|
+
let stat;
|
|
829
|
+
try {
|
|
830
|
+
stat = fs.statSync(resolved);
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
return { error: `File not found: ${resolved}. Read the file first to verify the path.` };
|
|
834
|
+
}
|
|
835
|
+
if (stat.isDirectory())
|
|
836
|
+
return { error: `Path is a directory, not a file: ${resolved}.` };
|
|
837
|
+
if (stat.size > MAX_EDIT_BYTES) {
|
|
838
|
+
return { error: `File too large to edit (${(stat.size / 1024 / 1024).toFixed(1)} MB, max ${MAX_EDIT_BYTES / 1024 / 1024} MB). Use bash (sed/awk) for very large files.` };
|
|
839
|
+
}
|
|
840
|
+
if (isBinaryFile(resolved)) {
|
|
841
|
+
return { error: `Refusing to edit binary file ${resolved} — a text replace would corrupt it.` };
|
|
842
|
+
}
|
|
843
|
+
return { mode: stat.mode, content: fs.readFileSync(resolved, 'utf-8') };
|
|
844
|
+
}
|
|
845
|
+
/** Atomic write that preserves the original file's permission bits (mode). */
|
|
846
|
+
function atomicWritePreservingMode(resolved, data, mode) {
|
|
847
|
+
const tmp = resolved + '.nexrall_tmp';
|
|
848
|
+
fs.writeFileSync(tmp, data, 'utf-8');
|
|
849
|
+
try {
|
|
850
|
+
fs.chmodSync(tmp, mode);
|
|
851
|
+
}
|
|
852
|
+
catch { /* non-fatal — keep going */ }
|
|
853
|
+
fs.renameSync(tmp, resolved);
|
|
854
|
+
}
|
|
697
855
|
// Normalize CRLF → LF so Windows files match LF old_strings.
|
|
698
856
|
function normalizeLF(s) {
|
|
699
857
|
return s.replace(/\r\n/g, '\n');
|
|
@@ -708,7 +866,10 @@ async function editFile(input, workDir) {
|
|
|
708
866
|
return { error: 'Missing required parameter: old_string' };
|
|
709
867
|
try {
|
|
710
868
|
const resolved = resolvePath(filePath, workDir);
|
|
711
|
-
const
|
|
869
|
+
const pre = preflightEdit(resolved);
|
|
870
|
+
if ('error' in pre)
|
|
871
|
+
return { error: pre.error };
|
|
872
|
+
const original = pre.content;
|
|
712
873
|
// Normalize CRLF → LF for matching; restore on write if needed
|
|
713
874
|
const wasCRLF = original.includes('\r\n');
|
|
714
875
|
const origNorm = normalizeLF(original);
|
|
@@ -724,11 +885,9 @@ async function editFile(input, workDir) {
|
|
|
724
885
|
let updated = literalReplace(origNorm, oldNorm, newNorm);
|
|
725
886
|
if (wasCRLF)
|
|
726
887
|
updated = updated.replace(/\n/g, '\r\n');
|
|
727
|
-
// Atomic write
|
|
728
|
-
//
|
|
729
|
-
|
|
730
|
-
fs.writeFileSync(tmp, updated, 'utf-8');
|
|
731
|
-
fs.renameSync(tmp, resolved);
|
|
888
|
+
// Atomic write (temp + rename) so a crash mid-write never leaves the file
|
|
889
|
+
// truncated, AND preserve the original mode bits (+x on scripts etc.).
|
|
890
|
+
atomicWritePreservingMode(resolved, updated, pre.mode);
|
|
732
891
|
const diff = buildDiff(filePath, oldNorm, newNorm, origNorm);
|
|
733
892
|
const linesBefore = origNorm.split('\n').length;
|
|
734
893
|
const linesAfter = updated.split('\n').length;
|
|
@@ -802,6 +961,95 @@ function stripHtml(html) {
|
|
|
802
961
|
}
|
|
803
962
|
const MAX_REDIRECTS = 10;
|
|
804
963
|
const FETCH_USER_AGENT = 'Nexrall-Code/1.0 (+https://nexrall.com)';
|
|
964
|
+
/** True if an IP literal is loopback / private / link-local / unique-local. */
|
|
965
|
+
function isPrivateIp(ip) {
|
|
966
|
+
const v4 = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
967
|
+
if (v4) {
|
|
968
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])];
|
|
969
|
+
if (a === 10)
|
|
970
|
+
return true; // 10.0.0.0/8
|
|
971
|
+
if (a === 127)
|
|
972
|
+
return true; // loopback
|
|
973
|
+
if (a === 0)
|
|
974
|
+
return true; // 0.0.0.0/8
|
|
975
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
976
|
+
return true; // 172.16.0.0/12
|
|
977
|
+
if (a === 192 && b === 168)
|
|
978
|
+
return true; // 192.168.0.0/16
|
|
979
|
+
if (a === 169 && b === 254)
|
|
980
|
+
return true; // link-local (cloud metadata!)
|
|
981
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
982
|
+
return true; // CGNAT 100.64.0.0/10
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
const low = ip.toLowerCase().replace(/^\[|\]$/g, '');
|
|
986
|
+
if (low === '::1' || low === '::')
|
|
987
|
+
return true; // IPv6 loopback / unspecified
|
|
988
|
+
if (low.startsWith('fe80'))
|
|
989
|
+
return true; // link-local
|
|
990
|
+
if (low.startsWith('fc') || low.startsWith('fd'))
|
|
991
|
+
return true; // unique-local fc00::/7
|
|
992
|
+
if (low.startsWith('::ffff:'))
|
|
993
|
+
return isPrivateIp(low.slice(7)); // IPv4-mapped
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* SSRF guard: reject a URL whose host is (or resolves to) a private / internal
|
|
998
|
+
* address. Blocks the classic prompt-injection → cloud-metadata exfiltration
|
|
999
|
+
* (http://169.254.169.254/…) and internal-service probing. Returns an error
|
|
1000
|
+
* string when the host is unsafe, or null when it's OK to fetch.
|
|
1001
|
+
*/
|
|
1002
|
+
async function ssrfCheck(parsedUrl) {
|
|
1003
|
+
// Escape hatch for local dev-server previews (fetch_url http://localhost:3000):
|
|
1004
|
+
// opt-in only, since it re-opens the SSRF surface.
|
|
1005
|
+
const env = (process.env.NEXRALL_ALLOW_LOCAL_FETCH ?? '').toLowerCase();
|
|
1006
|
+
if (env === '1' || env === 'true' || env === 'on')
|
|
1007
|
+
return null;
|
|
1008
|
+
const host = parsedUrl.hostname.replace(/^\[|\]$/g, '');
|
|
1009
|
+
const lower = host.toLowerCase();
|
|
1010
|
+
if (lower === 'localhost' || lower.endsWith('.localhost') || lower === 'metadata.google.internal') {
|
|
1011
|
+
return `Refusing to fetch internal host "${host}" (SSRF guard).`;
|
|
1012
|
+
}
|
|
1013
|
+
// Only SHORT-CIRCUIT on a form we can evaluate exactly: a valid dotted-quad
|
|
1014
|
+
// IPv4 or an IPv6 literal. Numeric shorthands like decimal (2130706433) or
|
|
1015
|
+
// octal (0177.0.0.1) must NOT be checked with isPrivateIp — they'd read as
|
|
1016
|
+
// "not private" and slip through, yet getaddrinfo expands them to 127.0.0.1 /
|
|
1017
|
+
// 169.254.169.254 at connect time. Those fall through to DNS below, which
|
|
1018
|
+
// canonicalizes them via the same resolver the real request uses.
|
|
1019
|
+
if (isValidDottedQuad(host) || host.includes(':')) {
|
|
1020
|
+
return isPrivateIp(host) ? `Refusing to fetch private/internal address "${host}" (SSRF guard).` : null;
|
|
1021
|
+
}
|
|
1022
|
+
// Hostname (or numeric shorthand) → resolve and reject if ANY resolved
|
|
1023
|
+
// address is private. getaddrinfo handles inet_aton forms (decimal/octal/hex),
|
|
1024
|
+
// so a disguised metadata IP resolves to its real address here and is caught.
|
|
1025
|
+
try {
|
|
1026
|
+
const addrs = await new Promise((res, rej) => {
|
|
1027
|
+
dns.lookup(host, { all: true }, (err, a) => (err ? rej(err) : res(a)));
|
|
1028
|
+
});
|
|
1029
|
+
if (!addrs.length)
|
|
1030
|
+
return null;
|
|
1031
|
+
for (const { address } of addrs) {
|
|
1032
|
+
if (isPrivateIp(address))
|
|
1033
|
+
return `Refusing to fetch "${host}" — it resolves to a private/internal address (${address}) (SSRF guard).`;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
catch {
|
|
1037
|
+
// DNS failure. For a plain hostname, let the real request surface the network
|
|
1038
|
+
// error. But for a NUMERIC shorthand we couldn't resolve, fail closed — we
|
|
1039
|
+
// can't prove it's public and it might be a disguised private literal.
|
|
1040
|
+
if (/^[\d.]+$|^0[xX]/.test(host)) {
|
|
1041
|
+
return `Refusing to fetch numeric host "${host}" — could not verify it is a public address (SSRF guard).`;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
/** True only for a canonical dotted-quad IPv4 (4 octets, each 0–255). */
|
|
1047
|
+
function isValidDottedQuad(host) {
|
|
1048
|
+
const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
1049
|
+
if (!m)
|
|
1050
|
+
return false;
|
|
1051
|
+
return m.slice(1).every((o) => Number(o) <= 255);
|
|
1052
|
+
}
|
|
805
1053
|
async function fetchUrl(input, _workDir, _redirectCount = 0) {
|
|
806
1054
|
const url = typeof input.url === 'string' ? input.url : '';
|
|
807
1055
|
if (!url)
|
|
@@ -819,10 +1067,33 @@ async function fetchUrl(input, _workDir, _redirectCount = 0) {
|
|
|
819
1067
|
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
820
1068
|
return { error: `Unsupported protocol: ${parsedUrl.protocol}. Only http and https are supported.` };
|
|
821
1069
|
}
|
|
1070
|
+
// SSRF guard — runs on every hop (redirects recurse through fetchUrl), so a
|
|
1071
|
+
// public URL that 302-redirects to 169.254.169.254 is still blocked.
|
|
1072
|
+
const ssrfError = await ssrfCheck(parsedUrl);
|
|
1073
|
+
if (ssrfError)
|
|
1074
|
+
return { error: ssrfError };
|
|
1075
|
+
const allowLocal = ['1', 'true', 'on'].includes((process.env.NEXRALL_ALLOW_LOCAL_FETCH ?? '').toLowerCase());
|
|
822
1076
|
return new Promise((resolve) => {
|
|
823
1077
|
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
1078
|
+
// Re-validate the address the socket ACTUALLY connects to. ssrfCheck() above
|
|
1079
|
+
// resolves at check-time; this closes the TOCTOU / DNS-rebinding window where
|
|
1080
|
+
// a hostname flips to a private IP between the check and the connect.
|
|
1081
|
+
const guardedLookup = ((hostname, opts, cb) => {
|
|
1082
|
+
const callback = (typeof opts === 'function' ? opts : cb);
|
|
1083
|
+
const options = (typeof opts === 'function' ? {} : opts);
|
|
1084
|
+
return dns.lookup(hostname, options, (err, address, family) => {
|
|
1085
|
+
if (err)
|
|
1086
|
+
return callback(err, address, family);
|
|
1087
|
+
const addrs = Array.isArray(address) ? address.map((a) => a.address) : [address];
|
|
1088
|
+
if (!allowLocal && addrs.some((a) => isPrivateIp(a))) {
|
|
1089
|
+
return callback(Object.assign(new Error('SSRF guard: host resolved to a private address at connect time'), { code: 'ESSRFBLOCKED' }), address, family);
|
|
1090
|
+
}
|
|
1091
|
+
return callback(null, address, family);
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
824
1094
|
const req = transport.get(url, {
|
|
825
1095
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
1096
|
+
lookup: guardedLookup,
|
|
826
1097
|
headers: {
|
|
827
1098
|
'User-Agent': FETCH_USER_AGENT,
|
|
828
1099
|
'Accept-Encoding': 'identity', // opt-out of gzip/deflate — we read raw Buffer as UTF-8
|
|
@@ -919,7 +1190,10 @@ async function multiEdit(input, workDir) {
|
|
|
919
1190
|
return { error: 'Missing required parameter: edits (must be a non-empty array)' };
|
|
920
1191
|
try {
|
|
921
1192
|
const resolved = resolvePath(filePath, workDir);
|
|
922
|
-
const
|
|
1193
|
+
const pre = preflightEdit(resolved);
|
|
1194
|
+
if ('error' in pre)
|
|
1195
|
+
return { error: pre.error };
|
|
1196
|
+
const rawFile = pre.content;
|
|
923
1197
|
// Work in LF-normalised space so CRLF files match LF old_strings
|
|
924
1198
|
const wasCRLF = rawFile.includes('\r\n');
|
|
925
1199
|
let content = normalizeLF(rawFile);
|
|
@@ -945,11 +1219,10 @@ async function multiEdit(input, workDir) {
|
|
|
945
1219
|
diffs.push(buildDiff(filePath, oldStr, newStr, content));
|
|
946
1220
|
content = literalReplace(content, oldStr, newStr);
|
|
947
1221
|
}
|
|
948
|
-
// Restore original line endings before writing (atomic temp+rename)
|
|
1222
|
+
// Restore original line endings before writing (atomic temp+rename),
|
|
1223
|
+
// preserving the original mode bits (+x on scripts etc.).
|
|
949
1224
|
const finalContent = wasCRLF ? content.replace(/\n/g, '\r\n') : content;
|
|
950
|
-
|
|
951
|
-
fs.writeFileSync(tmp, finalContent, 'utf-8');
|
|
952
|
-
fs.renameSync(tmp, resolved);
|
|
1225
|
+
atomicWritePreservingMode(resolved, finalContent, pre.mode);
|
|
953
1226
|
return {
|
|
954
1227
|
output: `Applied ${edits.length} edit(s) to ${resolved}:\n` +
|
|
955
1228
|
diffs.map((d, i) => `\n--- edit #${i + 1} ---\n${d}`).join('\n'),
|
package/dist/types.d.ts
CHANGED
|
@@ -151,6 +151,14 @@ export interface AgentLoopOptions {
|
|
|
151
151
|
takePendingInput?: () => string[];
|
|
152
152
|
/** Notify that a queued follow-up was injected into the conversation (for UI echo/logging). */
|
|
153
153
|
onInjectedInput?: (text: string) => void;
|
|
154
|
+
/**
|
|
155
|
+
* Fired at every turn boundary inside the loop with a snapshot of the current
|
|
156
|
+
* message history. Lets the caller persist progress INCREMENTALLY so a crash
|
|
157
|
+
* (or `kill`) hours into a long run doesn't lose the whole session — only the
|
|
158
|
+
* in-flight step. The array is a live reference; treat it as read-only and
|
|
159
|
+
* copy if you need to retain it. Cheap: callers should debounce heavy writes.
|
|
160
|
+
*/
|
|
161
|
+
onProgress?: (messages: Message[]) => void;
|
|
154
162
|
/**
|
|
155
163
|
* Max agent iterations (model response + tool round) before the runaway-loop
|
|
156
164
|
* backstop trips. Overrides the NEXRALL_MAX_ITERATIONS env var and the
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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)",
|