@nexrall/code-core 1.4.22 → 1.4.23
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 +36 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +231 -24
- package/dist/api/client.d.ts +12 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +351 -77
- package/dist/auth/index.d.ts +21 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +53 -0
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +100 -14
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/agent/loop.d.ts
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
import type { Message, AgentLoopOptions, EnvContext } from '../types';
|
|
2
2
|
export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
|
|
3
|
+
/**
|
|
4
|
+
* Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
|
|
5
|
+
* the CLI ships as a single esbuild bundle with no node_modules, and this is a
|
|
6
|
+
* dozen lines.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createLimiter(max: number): <T>(fn: () => Promise<T>) => Promise<T>;
|
|
9
|
+
/**
|
|
10
|
+
* Reduce a sub-agent's message history to the text its parent should receive.
|
|
11
|
+
*
|
|
12
|
+
* Pure + exported so the salvage rules can be tested without running a real
|
|
13
|
+
* sub-agent (which needs a live model stream and, for the timeout path, ten
|
|
14
|
+
* minutes of wall clock).
|
|
15
|
+
*
|
|
16
|
+
* `preferLast` — the normal completion path — returns the final assistant
|
|
17
|
+
* message, which is the sub-agent's actual answer.
|
|
18
|
+
*
|
|
19
|
+
* `preferLast: false` is the SALVAGE path, used when the sub-agent was cut off.
|
|
20
|
+
* A stopped sub-agent usually has no closing summary at all (it was killed
|
|
21
|
+
* mid-tool-round), so the last assistant message is frequently empty or a
|
|
22
|
+
* fragment. Concatenating what it did produce is far more useful to the parent
|
|
23
|
+
* model than nothing: it can build on the work instead of redoing it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function extractSubTaskText(messages: Message[], preferLast?: boolean): string;
|
|
26
|
+
/** Apply the parent-context cap to a sub-task's text, keeping head + tail. */
|
|
27
|
+
export declare function capSubTaskText(text: string, max?: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* Summarise what a cut-short sub-agent actually accomplished, so the parent model
|
|
30
|
+
* can continue from it rather than starting over.
|
|
31
|
+
*
|
|
32
|
+
* This is the whole point of the salvage path. Previously a timed-out sub-agent
|
|
33
|
+
* returned ONLY an error string: ten minutes of work, dozens of tool calls and
|
|
34
|
+
* any files it wrote were invisible to the parent, which typically responded by
|
|
35
|
+
* re-running the same work from scratch — while the tokens for the discarded run
|
|
36
|
+
* had already been billed in full.
|
|
37
|
+
*/
|
|
38
|
+
export declare function summariseSubTaskProgress(messages: Message[]): string;
|
|
3
39
|
/** The context window (tokens) for a model alias — the same lookup runAgentLoop uses internally. */
|
|
4
40
|
export declare function contextWindowFor(model?: 'turbo' | 'pro' | 'ultra'): number;
|
|
5
41
|
/** Auto-prune / auto-compact thresholds as fractions of the context window — for UI display (e.g. `/context`). */
|
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,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAyKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -35,6 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = void 0;
|
|
37
37
|
exports.resolveMaxIterations = resolveMaxIterations;
|
|
38
|
+
exports.createLimiter = createLimiter;
|
|
39
|
+
exports.extractSubTaskText = extractSubTaskText;
|
|
40
|
+
exports.capSubTaskText = capSubTaskText;
|
|
41
|
+
exports.summariseSubTaskProgress = summariseSubTaskProgress;
|
|
38
42
|
exports.contextWindowFor = contextWindowFor;
|
|
39
43
|
exports.compactionThresholds = compactionThresholds;
|
|
40
44
|
exports.estimateBodyBytes = estimateBodyBytes;
|
|
@@ -241,6 +245,58 @@ async function withFileLock(absPath, fn) {
|
|
|
241
245
|
_fileLocks.delete(absPath);
|
|
242
246
|
}
|
|
243
247
|
}
|
|
248
|
+
// ─── Sub-agent fan-out limiter ────────────────────────────────────────────────
|
|
249
|
+
//
|
|
250
|
+
// Tool calls in one turn run via Promise.all with no ceiling. For ordinary tools
|
|
251
|
+
// that is right — they're cheap and mostly I/O — but a `task` call spawns a WHOLE
|
|
252
|
+
// nested agent loop: its own model stream, its own tool executions, its own
|
|
253
|
+
// sub-process spawns. A model that emits ten `task` blocks in one turn therefore
|
|
254
|
+
// starts ten concurrent agents, each billing tokens and competing for the same
|
|
255
|
+
// CPU, file handles and API rate limit. The practical symptoms are the ones users
|
|
256
|
+
// report as "it got slow and then stalled": every sub-agent's stream slows, some
|
|
257
|
+
// trip their own stall watchdog, and one shared rate limit is spread across ten
|
|
258
|
+
// callers.
|
|
259
|
+
//
|
|
260
|
+
// Anthropic hit the same wall and capped Claude Code's concurrent subagents at 20
|
|
261
|
+
// (v2.1.217, July 2026); community guidance settles far lower, around 3-5, because
|
|
262
|
+
// past that the synthesis overhead cancels the parallelism. We default to 4:
|
|
263
|
+
// enough for genuine fan-out (the case sub-agents exist for), low enough that a
|
|
264
|
+
// runaway `task` burst degrades into a queue instead of a thundering herd.
|
|
265
|
+
//
|
|
266
|
+
// This is a QUEUE, not a rejection: every sub-task still runs, just at most N at a
|
|
267
|
+
// time. Failing the excess would be worse than serialising it.
|
|
268
|
+
const MAX_CONCURRENT_SUBTASKS = (() => {
|
|
269
|
+
const raw = Number(process.env.NEXRALL_MAX_CONCURRENT_SUBTASKS);
|
|
270
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 4;
|
|
271
|
+
})();
|
|
272
|
+
/**
|
|
273
|
+
* Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
|
|
274
|
+
* the CLI ships as a single esbuild bundle with no node_modules, and this is a
|
|
275
|
+
* dozen lines.
|
|
276
|
+
*/
|
|
277
|
+
function createLimiter(max) {
|
|
278
|
+
let active = 0;
|
|
279
|
+
const queue = [];
|
|
280
|
+
const release = () => {
|
|
281
|
+
active--;
|
|
282
|
+
queue.shift()?.();
|
|
283
|
+
};
|
|
284
|
+
return async (fn) => {
|
|
285
|
+
if (active >= max)
|
|
286
|
+
await new Promise((resolve) => queue.push(resolve));
|
|
287
|
+
active++;
|
|
288
|
+
try {
|
|
289
|
+
return await fn();
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
release();
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
// Process-wide, deliberately: the limit exists to protect shared resources (CPU,
|
|
297
|
+
// the API rate limit, file handles), and those are shared across every concurrent
|
|
298
|
+
// turn in this process, not just the tool calls of one message.
|
|
299
|
+
const _subTaskLimit = createLimiter(MAX_CONCURRENT_SUBTASKS);
|
|
244
300
|
// ─── Human-readable tool descriptions ────────────────────────────────────────
|
|
245
301
|
function humanDescription(name, input) {
|
|
246
302
|
switch (name) {
|
|
@@ -326,7 +382,15 @@ function humanDescription(name, input) {
|
|
|
326
382
|
}
|
|
327
383
|
}
|
|
328
384
|
// ─── Sub-task runner ──────────────────────────────────────────────────────────
|
|
329
|
-
|
|
385
|
+
// Deepest nesting level allowed to spawn a sub-agent. The main agent runs at
|
|
386
|
+
// depth 0 and the sub-agents it spawns at depth 1.
|
|
387
|
+
//
|
|
388
|
+
// This is 1, not 2. With 2 the guard below (`depth >= MAX_TASK_DEPTH`) let a
|
|
389
|
+
// depth-1 sub-agent spawn depth-2 grandchildren — contradicting both the constant's
|
|
390
|
+
// own comment and the system prompt's promise that "sub-agents cannot spawn further
|
|
391
|
+
// sub-agents", and quietly making the worst-case fan-out quadratic. The value and
|
|
392
|
+
// the documented behaviour now agree.
|
|
393
|
+
const MAX_TASK_DEPTH = 1;
|
|
330
394
|
let _subTaskCounter = 0; // unique per-process id → per-sub-agent todo scope
|
|
331
395
|
// A sub-agent that stalls (hung tool, model provider stuck, infinite tool-call
|
|
332
396
|
// loop bypassing the iteration budget somehow) used to have NO ceiling of its
|
|
@@ -338,13 +402,112 @@ let _subTaskCounter = 0; // unique per-process id → per-sub-agent todo scope
|
|
|
338
402
|
const SUBTASK_TIMEOUT_MS = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) > 0
|
|
339
403
|
? Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS)
|
|
340
404
|
: 10 * 60 * 1000; // 10 minutes, matching the general industry norm for a stalled-agent cutoff
|
|
405
|
+
/** Cap on the text a sub-task hands back, so one verbose sub-agent can't blow up
|
|
406
|
+
* the PARENT's context in a single tool_result. */
|
|
407
|
+
const SUBTASK_MAX = 48000; // chars (~12k tokens)
|
|
408
|
+
/**
|
|
409
|
+
* Slice `s` to at most `max` UTF-16 units without splitting a surrogate pair.
|
|
410
|
+
*
|
|
411
|
+
* A bare `slice()` can cut between the high and low half of a non-BMP character
|
|
412
|
+
* (emoji, many CJK extension glyphs), producing a lone surrogate — invalid UTF-16
|
|
413
|
+
* that the Anthropic API rejects outright with "no low surrogate in string". That
|
|
414
|
+
* exact failure has already been shipped and fixed once in this codebase; every
|
|
415
|
+
* new cap on model-facing text has to be surrogate-aware from the start.
|
|
416
|
+
*/
|
|
417
|
+
function sliceSafeEnd(s, max) {
|
|
418
|
+
if (s.length <= max)
|
|
419
|
+
return s;
|
|
420
|
+
let end = max;
|
|
421
|
+
const code = s.charCodeAt(end - 1);
|
|
422
|
+
if (code >= 0xd800 && code <= 0xdbff)
|
|
423
|
+
end--; // trailing high surrogate — drop it
|
|
424
|
+
return s.slice(0, end);
|
|
425
|
+
}
|
|
426
|
+
/** Mirror of sliceSafeEnd for a tail slice: never START on a low surrogate. */
|
|
427
|
+
function sliceSafeStart(s, from) {
|
|
428
|
+
if (from <= 0)
|
|
429
|
+
return s;
|
|
430
|
+
let start = from;
|
|
431
|
+
const code = s.charCodeAt(start);
|
|
432
|
+
if (code >= 0xdc00 && code <= 0xdfff)
|
|
433
|
+
start++; // leading low surrogate — drop it
|
|
434
|
+
return s.slice(start);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Reduce a sub-agent's message history to the text its parent should receive.
|
|
438
|
+
*
|
|
439
|
+
* Pure + exported so the salvage rules can be tested without running a real
|
|
440
|
+
* sub-agent (which needs a live model stream and, for the timeout path, ten
|
|
441
|
+
* minutes of wall clock).
|
|
442
|
+
*
|
|
443
|
+
* `preferLast` — the normal completion path — returns the final assistant
|
|
444
|
+
* message, which is the sub-agent's actual answer.
|
|
445
|
+
*
|
|
446
|
+
* `preferLast: false` is the SALVAGE path, used when the sub-agent was cut off.
|
|
447
|
+
* A stopped sub-agent usually has no closing summary at all (it was killed
|
|
448
|
+
* mid-tool-round), so the last assistant message is frequently empty or a
|
|
449
|
+
* fragment. Concatenating what it did produce is far more useful to the parent
|
|
450
|
+
* model than nothing: it can build on the work instead of redoing it.
|
|
451
|
+
*/
|
|
452
|
+
function extractSubTaskText(messages, preferLast = true) {
|
|
453
|
+
const assistants = messages.filter((m) => m.role === 'assistant');
|
|
454
|
+
const textOf = (m) => (m?.content ?? [])
|
|
455
|
+
.filter((b) => b.type === 'text' && typeof b.text === 'string')
|
|
456
|
+
.map((b) => b.text)
|
|
457
|
+
.join('')
|
|
458
|
+
.trim();
|
|
459
|
+
if (preferLast)
|
|
460
|
+
return textOf(assistants[assistants.length - 1]);
|
|
461
|
+
return assistants.map(textOf).filter(Boolean).join('\n\n').trim();
|
|
462
|
+
}
|
|
463
|
+
/** Apply the parent-context cap to a sub-task's text, keeping head + tail. */
|
|
464
|
+
function capSubTaskText(text, max = SUBTASK_MAX) {
|
|
465
|
+
if (text.length <= max)
|
|
466
|
+
return text;
|
|
467
|
+
const head = sliceSafeEnd(text, Math.floor(max * 0.6));
|
|
468
|
+
const tail = sliceSafeStart(text, text.length - Math.floor(max * 0.4));
|
|
469
|
+
return `${head}\n\n[… sub-task output truncated (${text.length} chars) — kept the beginning and end …]\n\n${tail}`;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Summarise what a cut-short sub-agent actually accomplished, so the parent model
|
|
473
|
+
* can continue from it rather than starting over.
|
|
474
|
+
*
|
|
475
|
+
* This is the whole point of the salvage path. Previously a timed-out sub-agent
|
|
476
|
+
* returned ONLY an error string: ten minutes of work, dozens of tool calls and
|
|
477
|
+
* any files it wrote were invisible to the parent, which typically responded by
|
|
478
|
+
* re-running the same work from scratch — while the tokens for the discarded run
|
|
479
|
+
* had already been billed in full.
|
|
480
|
+
*/
|
|
481
|
+
function summariseSubTaskProgress(messages) {
|
|
482
|
+
const toolNames = [];
|
|
483
|
+
for (const m of messages) {
|
|
484
|
+
if (m.role !== 'assistant' || !Array.isArray(m.content))
|
|
485
|
+
continue;
|
|
486
|
+
for (const b of m.content) {
|
|
487
|
+
if (b?.type === 'tool_use' && typeof b.name === 'string')
|
|
488
|
+
toolNames.push(b.name);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (toolNames.length === 0)
|
|
492
|
+
return '';
|
|
493
|
+
// Collapse to "name ×N" so a 40-call run reads as a short inventory rather
|
|
494
|
+
// than forty repeated lines of the same tool name.
|
|
495
|
+
const counts = new Map();
|
|
496
|
+
for (const n of toolNames)
|
|
497
|
+
counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
498
|
+
const inventory = [...counts.entries()]
|
|
499
|
+
.sort((a, b) => b[1] - a[1])
|
|
500
|
+
.map(([name, n]) => (n > 1 ? `${name} ×${n}` : name))
|
|
501
|
+
.join(', ');
|
|
502
|
+
return `Tool calls completed before it was stopped (${toolNames.length} total): ${inventory}.`;
|
|
503
|
+
}
|
|
341
504
|
async function runSubTask(input, options, agentTypes) {
|
|
342
505
|
const prompt = typeof input.prompt === 'string' ? input.prompt.trim() : '';
|
|
343
506
|
if (!prompt)
|
|
344
507
|
return { error: 'task tool requires a non-empty prompt' };
|
|
345
508
|
const depth = options._depth ?? 0;
|
|
346
509
|
if (depth >= MAX_TASK_DEPTH) {
|
|
347
|
-
return { error:
|
|
510
|
+
return { error: 'Sub-agents cannot spawn further sub-agents. Do this work directly, or report back so the main agent can delegate it.' };
|
|
348
511
|
}
|
|
349
512
|
// Resolve an optional custom agent type (subagent_type).
|
|
350
513
|
const requestedType = typeof input.subagent_type === 'string' ? input.subagent_type : '';
|
|
@@ -423,32 +586,57 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
423
586
|
onThinkingDelta: (text) => options.onThinkingDelta?.(text),
|
|
424
587
|
onThinkingProgress: (tok) => options.onThinkingProgress?.(tok),
|
|
425
588
|
});
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
589
|
+
// ── Stall timeout: SALVAGE, don't discard ────────────────────────────────
|
|
590
|
+
//
|
|
591
|
+
// The sub-agent hit its wall-clock cap (rather than finishing, or the parent
|
|
592
|
+
// aborting). This used to return ONLY an error string — throwing away
|
|
593
|
+
// everything the sub-agent had produced in up to ten minutes of work. The
|
|
594
|
+
// tokens were billed in full either way, and the parent model, told merely
|
|
595
|
+
// that "it stalled", would routinely re-run the identical work from scratch.
|
|
596
|
+
//
|
|
597
|
+
// The timeout still has to be reported unmistakably (the parent must not
|
|
598
|
+
// mistake a truncated run for a complete answer), but it is reported ALONGSIDE
|
|
599
|
+
// whatever was actually accomplished, not instead of it. `preferLast: false`
|
|
600
|
+
// because a killed sub-agent rarely has a closing summary — its useful output
|
|
601
|
+
// is spread across the assistant turns it did manage to produce.
|
|
430
602
|
if (subAbort.aborted && !options.abortSignal?.aborted) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const head = text.slice(0, Math.floor(SUBTASK_MAX * 0.6));
|
|
446
|
-
const tail = text.slice(text.length - Math.floor(SUBTASK_MAX * 0.4));
|
|
447
|
-
text = `${head}\n\n[… sub-task output truncated (${text.length} chars) — kept the beginning and end …]\n\n${tail}`;
|
|
603
|
+
const mins = Math.round(SUBTASK_TIMEOUT_MS / 60000);
|
|
604
|
+
const partial = capSubTaskText(extractSubTaskText(result, false));
|
|
605
|
+
const progress = summariseSubTaskProgress(result);
|
|
606
|
+
const sections = [
|
|
607
|
+
`Sub-task STOPPED after ${mins} minutes without completing — treat the following as PARTIAL, unverified work, not a finished answer.`,
|
|
608
|
+
progress,
|
|
609
|
+
partial ? `Partial output before it was stopped:\n\n${partial}` : '',
|
|
610
|
+
'Do NOT simply re-run the same sub-task: build on what is above, or split the remaining work into smaller, more focused sub-tasks.',
|
|
611
|
+
].filter(Boolean);
|
|
612
|
+
// Returned as `error` (not `output`) on purpose: the loop's ledger counts an
|
|
613
|
+
// errored call as a non-effect, which is right — nothing here is verified —
|
|
614
|
+
// and the STALL_LIMIT runaway guard must still see repeated timeouts as
|
|
615
|
+
// failures so a permanently stuck sub-task can't loop forever.
|
|
616
|
+
return { error: sections.join('\n\n') };
|
|
448
617
|
}
|
|
618
|
+
// Normal completion: the final assistant message is the sub-agent's answer.
|
|
619
|
+
const text = capSubTaskText(extractSubTaskText(result, true));
|
|
449
620
|
return { output: text || '(sub-task completed with no text output)' };
|
|
450
621
|
}
|
|
451
622
|
catch (err) {
|
|
623
|
+
// Same salvage rule as the timeout path above, for the other way a sub-agent
|
|
624
|
+
// dies: runAgentLoop throws AgentTurnError when its stream fails, and that
|
|
625
|
+
// error CARRIES the history completed up to the failure precisely so callers
|
|
626
|
+
// don't lose it (see types.ts). Discarding it here — as this catch used to —
|
|
627
|
+
// reproduced the exact waste that class was written to prevent, one level down.
|
|
628
|
+
const salvaged = (0, types_1.salvageHistory)(err);
|
|
629
|
+
if (salvaged) {
|
|
630
|
+
const partial = capSubTaskText(extractSubTaskText(salvaged, false));
|
|
631
|
+
const progress = summariseSubTaskProgress(salvaged);
|
|
632
|
+
const sections = [
|
|
633
|
+
`Sub-task FAILED before completing: ${err.message}`,
|
|
634
|
+
progress,
|
|
635
|
+
partial ? `Partial output before the failure:\n\n${partial}` : '',
|
|
636
|
+
'Treat the above as PARTIAL, unverified work. Build on it rather than re-running the whole sub-task.',
|
|
637
|
+
].filter(Boolean);
|
|
638
|
+
return { error: sections.join('\n\n') };
|
|
639
|
+
}
|
|
452
640
|
return { error: `Sub-task failed: ${err.message}` };
|
|
453
641
|
}
|
|
454
642
|
finally {
|
|
@@ -1515,7 +1703,26 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1515
1703
|
result = { error: 'Permission denied by user' };
|
|
1516
1704
|
}
|
|
1517
1705
|
else if (name === 'task') {
|
|
1518
|
-
|
|
1706
|
+
// Gated so a burst of `task` blocks in one message becomes a QUEUE
|
|
1707
|
+
// rather than N concurrent agent loops (see MAX_CONCURRENT_SUBTASKS).
|
|
1708
|
+
//
|
|
1709
|
+
// The gate wraps the CALL, not the inside of runSubTask, and that
|
|
1710
|
+
// placement is the whole trick: runSubTask arms its own 10-minute stall
|
|
1711
|
+
// timeout as its first act, so acquiring the slot inside it would start
|
|
1712
|
+
// that clock while the sub-task was still sitting in the queue. A
|
|
1713
|
+
// sub-task queued behind three long-running siblings could then be
|
|
1714
|
+
// "stopped for stalling" having never executed a single step.
|
|
1715
|
+
//
|
|
1716
|
+
// Only TOP-LEVEL fan-out is gated (`depth === 0`) — a deliberate
|
|
1717
|
+
// deadlock guard, kept even though MAX_TASK_DEPTH currently makes a
|
|
1718
|
+
// nested spawn impossible anyway. A nested spawn would request a slot
|
|
1719
|
+
// while its parent still holds one; if every slot were held by a parent
|
|
1720
|
+
// waiting on a child that can never be scheduled, the run would wedge
|
|
1721
|
+
// permanently. Keeping the limiter acyclic (a holder never re-enters it)
|
|
1722
|
+
// means raising MAX_TASK_DEPTH later can't silently reintroduce that.
|
|
1723
|
+
result = depth === 0
|
|
1724
|
+
? await _subTaskLimit(() => runSubTask(input, options, agentTypes))
|
|
1725
|
+
: await runSubTask(input, options, agentTypes);
|
|
1519
1726
|
}
|
|
1520
1727
|
else {
|
|
1521
1728
|
const pre = runToolHooks(hooks.PreToolUse, 'PreToolUse', name, input, options.workDir);
|
package/dist/api/client.d.ts
CHANGED
|
@@ -76,6 +76,18 @@ export declare function streamChat(messages: Message[], options: StreamChatOptio
|
|
|
76
76
|
* bounds the cost if this request never lands.
|
|
77
77
|
*/
|
|
78
78
|
export declare function cancelTurn(turnId: string): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Revoke the stored refresh token server-side.
|
|
81
|
+
*
|
|
82
|
+
* Deleting the local config alone is NOT a logout once refresh tokens exist: the
|
|
83
|
+
* token stays valid for its full 60-day life and can keep minting access tokens
|
|
84
|
+
* for anyone who recovered it (a synced dotfile, a backup, a shared machine).
|
|
85
|
+
* The backend already exposes /api/auth/logout for exactly this.
|
|
86
|
+
*
|
|
87
|
+
* Best-effort and never throws — a failed revoke must not stop the local
|
|
88
|
+
* credentials from being cleared, which is the part the user can see.
|
|
89
|
+
*/
|
|
90
|
+
export declare function revokeRefreshToken(): Promise<void>;
|
|
79
91
|
export declare function getBalance(): Promise<number>;
|
|
80
92
|
export declare function exchangeVscodeCode(code: string): Promise<AuthConfig>;
|
|
81
93
|
export declare function login(email: string, password: string): Promise<AuthConfig>;
|
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":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiErH,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAIzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAeL;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiErH,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAIzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAeL;AAiGD,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;IAChB,6IAA6I;IAC7I,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAiDD,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,CA6jClB;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU9D;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAYxD;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CA0BlD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA+B1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA0BhF"}
|
package/dist/api/client.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.API_BASE = void 0;
|
|
|
7
7
|
exports.chooseFinalContent = chooseFinalContent;
|
|
8
8
|
exports.streamChat = streamChat;
|
|
9
9
|
exports.cancelTurn = cancelTurn;
|
|
10
|
+
exports.revokeRefreshToken = revokeRefreshToken;
|
|
10
11
|
exports.getBalance = getBalance;
|
|
11
12
|
exports.exchangeVscodeCode = exchangeVscodeCode;
|
|
12
13
|
exports.login = login;
|
|
@@ -115,6 +116,91 @@ function authHeaders() {
|
|
|
115
116
|
Authorization: `Bearer ${token}`,
|
|
116
117
|
};
|
|
117
118
|
}
|
|
119
|
+
// ─── Silent token refresh ─────────────────────────────────────────────────────
|
|
120
|
+
/**
|
|
121
|
+
* In-flight refresh, shared by every caller that discovers an expired token at the
|
|
122
|
+
* same moment.
|
|
123
|
+
*
|
|
124
|
+
* This is a correctness requirement, not a micro-optimisation. Refresh tokens
|
|
125
|
+
* ROTATE: presenting one invalidates it and returns a replacement, and presenting
|
|
126
|
+
* a spent one is treated by the backend as theft (`reuse_detected`) — it revokes
|
|
127
|
+
* the entire token family and forces a real re-login. A long turn can easily have
|
|
128
|
+
* several requests in flight (the stream, a cancel, a balance poll), so without
|
|
129
|
+
* this they would each present the SAME refresh token and all but the first would
|
|
130
|
+
* look exactly like an attack. De-duplicating means one rotation, one winner,
|
|
131
|
+
* everyone else awaiting the same promise.
|
|
132
|
+
*/
|
|
133
|
+
let _refreshInFlight = null;
|
|
134
|
+
/**
|
|
135
|
+
* Exchange the stored refresh token for a fresh access token. Resolves true when
|
|
136
|
+
* the tokens on disk were updated, false when refreshing isn't possible (no
|
|
137
|
+
* refresh token stored, or the server rejected it).
|
|
138
|
+
*
|
|
139
|
+
* Deliberately never throws: every call site treats `false` as "carry on and let
|
|
140
|
+
* the original 401/403 surface", which is the correct fallback in all cases.
|
|
141
|
+
*/
|
|
142
|
+
async function refreshAccessToken() {
|
|
143
|
+
if (_refreshInFlight)
|
|
144
|
+
return _refreshInFlight;
|
|
145
|
+
_refreshInFlight = (async () => {
|
|
146
|
+
const refreshToken = (0, index_1.getRefreshToken)();
|
|
147
|
+
// No refresh token: a NEXRALL_TOKEN/CI credential, or a config written before
|
|
148
|
+
// this field existed. Nothing to do — the caller falls back to the auth error.
|
|
149
|
+
if (!refreshToken)
|
|
150
|
+
return false;
|
|
151
|
+
try {
|
|
152
|
+
const res = await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/refresh`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
155
|
+
body: JSON.stringify({ refreshToken }),
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok)
|
|
158
|
+
return false; // 401 = invalid/expired/reused → a real re-login is needed
|
|
159
|
+
const data = (await res.json());
|
|
160
|
+
if (!data.token)
|
|
161
|
+
return false;
|
|
162
|
+
(0, index_1.updateTokens)(data.token, data.refreshToken);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Network failure. Not an auth problem — report failure so the caller's own
|
|
167
|
+
// retry/backoff machinery handles it as the transport error it is.
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
})();
|
|
171
|
+
try {
|
|
172
|
+
return await _refreshInFlight;
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
_refreshInFlight = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Is this response an EXPIRED-CREDENTIAL failure that a refresh could fix?
|
|
180
|
+
*
|
|
181
|
+
* The backend's auth middleware answers 401 for a missing token and 403 for one
|
|
182
|
+
* that is invalid or expired. 403 is ambiguous — Anthropic's upstream
|
|
183
|
+
* "forbidden" passthrough uses it too — so the body has to disambiguate: the
|
|
184
|
+
* upstream shape is `{error:{type:'forbidden'}}` (a nested object), while the auth
|
|
185
|
+
* middleware sends a flat `{error:'Invalid or expired token'}` string. Refreshing
|
|
186
|
+
* on the upstream shape would be harmless but pointless; getting it backwards and
|
|
187
|
+
* NOT refreshing on the auth shape is the bug being fixed, so the test suite pins
|
|
188
|
+
* both directions.
|
|
189
|
+
*/
|
|
190
|
+
function isExpiredTokenResponse(status, body) {
|
|
191
|
+
if (status !== 401 && status !== 403)
|
|
192
|
+
return false;
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(body);
|
|
195
|
+
// Nested object → Anthropic's upstream passthrough, not our auth layer.
|
|
196
|
+
if (parsed?.error && typeof parsed.error === 'object')
|
|
197
|
+
return false;
|
|
198
|
+
return typeof parsed?.error === 'string' && /token|auth|expired/i.test(parsed.error);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
118
204
|
// ─── Retry helpers ────────────────────────────────────────────────────────────
|
|
119
205
|
const MAX_RETRIES = 5;
|
|
120
206
|
const RETRY_BASE_MS = 1000;
|
|
@@ -301,6 +387,71 @@ async function streamChat(messages, options, onEvent) {
|
|
|
301
387
|
onEvent({ type: 'retry_resolved' });
|
|
302
388
|
}
|
|
303
389
|
};
|
|
390
|
+
/**
|
|
391
|
+
* Abandon the current turnId and prepare to restart the turn from scratch.
|
|
392
|
+
*
|
|
393
|
+
* Used whenever the server tells us this turn can no longer be followed — the
|
|
394
|
+
* buffered frame window moved past our cursor (410), the turn is simply gone
|
|
395
|
+
* (404, e.g. the owning pod was replaced by a deploy), or a mid-stream frame
|
|
396
|
+
* carried `notResumable`. In all of those cases resuming again with the same
|
|
397
|
+
* cursor would hit the identical wall, so the only recovery is a clean restart.
|
|
398
|
+
*
|
|
399
|
+
* Two things make this more than a state reset, and both are easy to get wrong
|
|
400
|
+
* — which is why all three call sites funnel through here rather than each
|
|
401
|
+
* open-coding it:
|
|
402
|
+
*
|
|
403
|
+
* 1. The OLD turn must be cancelled and AWAITED first. A 410/404 does not mean
|
|
404
|
+
* the turn ended — it is very likely still generating and still holding its
|
|
405
|
+
* idempotency claim, so re-POSTing the same turnId would be rejected as
|
|
406
|
+
* `turnInFlight` and we would burn the entire retry budget waiting, while
|
|
407
|
+
* the orphaned turn kept generating and billing.
|
|
408
|
+
* 2. Anything already rendered has to be reconciled. A restart re-sends the
|
|
409
|
+
* answer from the beginning, so unless the caller can un-render its partial
|
|
410
|
+
* output, restarting would duplicate it on screen.
|
|
411
|
+
*
|
|
412
|
+
* Returns `false` when the caller cannot roll back its rendered output; the
|
|
413
|
+
* caller must then surface a terminal error instead of restarting.
|
|
414
|
+
*/
|
|
415
|
+
const prepareRestart = async (reason) => {
|
|
416
|
+
await cancelTurn(turnId);
|
|
417
|
+
turnId = (0, crypto_1.randomUUID)(); // a restart is semantically a NEW turn
|
|
418
|
+
resuming = false;
|
|
419
|
+
serverResumable = false;
|
|
420
|
+
lastEventId = 0;
|
|
421
|
+
carryText = [];
|
|
422
|
+
carryToolUse = [];
|
|
423
|
+
// Snapshot BEFORE resetting: the rollback decision needs to know what the user
|
|
424
|
+
// can currently see, but the counters must not carry into the restarted turn or
|
|
425
|
+
// a second restart would report characters this one already discarded.
|
|
426
|
+
const hadRendered = emittedAnythingAcrossAttempts;
|
|
427
|
+
const renderedChars = emittedCharsAcrossAttempts;
|
|
428
|
+
emittedAnythingAcrossAttempts = false;
|
|
429
|
+
emittedCharsAcrossAttempts = 0;
|
|
430
|
+
if (hadRendered && !allowRestartAfterRender)
|
|
431
|
+
return false;
|
|
432
|
+
if (hadRendered) {
|
|
433
|
+
onEvent({ type: 'stream_restart', reason, discardedChars: renderedChars });
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
};
|
|
437
|
+
/**
|
|
438
|
+
* Does an error response body carry the backend's explicit `notResumable` flag?
|
|
439
|
+
*
|
|
440
|
+
* The resume endpoint sets this on every "give up on this turnId" response
|
|
441
|
+
* (routes/code.js: the 410 gap case, the 404 no-such-turn case, and the
|
|
442
|
+
* synthesised mid-stream error frames). Reading the FLAG rather than matching
|
|
443
|
+
* the status code alone means a new give-up condition on the server is handled
|
|
444
|
+
* correctly here the day it ships, instead of falling through to the generic
|
|
445
|
+
* terminal-error path and killing a turn that was perfectly recoverable.
|
|
446
|
+
*/
|
|
447
|
+
const bodySaysNotResumable = (body) => {
|
|
448
|
+
try {
|
|
449
|
+
return JSON.parse(body)?.notResumable === true;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
304
455
|
// One network attempt: connect (with connect-time 429/5xx retry) and consume the stream to
|
|
305
456
|
// completion. Returns the assembled assistant Message, or throws — tagging a transient
|
|
306
457
|
// pre-content failure with { retryable: true } for the outer loop below.
|
|
@@ -318,8 +469,19 @@ async function streamChat(messages, options, onEvent) {
|
|
|
318
469
|
// short in-loop retry budget — the final throw tags `retryable: true` so it still
|
|
319
470
|
// reaches the outer 5-minute stream-retry loop instead of dying as a terminal error.
|
|
320
471
|
let forbiddenExhausted = false;
|
|
472
|
+
// Whether this attempt has already spent its one silent token refresh. Scoped to
|
|
473
|
+
// the attempt, not the whole call: if a freshly-minted token is ALSO rejected the
|
|
474
|
+
// failure isn't expiry, and retrying would pointlessly rotate the refresh token
|
|
475
|
+
// against a server that keeps refusing.
|
|
476
|
+
let refreshedThisAttempt = false;
|
|
477
|
+
// Set when the failure is a dead credential that refreshing could not fix, so the
|
|
478
|
+
// terminal throw below can tag it `authExpired` — letting the UI show a "sign in
|
|
479
|
+
// again" prompt instead of a generic error, and keeping it out of the outer
|
|
480
|
+
// retry loop (a re-login is not something reconnecting can solve).
|
|
481
|
+
let sessionExpired = false;
|
|
321
482
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
322
483
|
try {
|
|
484
|
+
errorBodyOverride = null; // a fresh response — never reuse the previous body
|
|
323
485
|
response = await (0, node_fetch_1.default)(...buildFetchArgs());
|
|
324
486
|
if (response.status === 429 && attempt < MAX_RETRIES && canRetry()) {
|
|
325
487
|
reportRetry('Rate limited by the API — retrying');
|
|
@@ -332,6 +494,57 @@ async function streamChat(messages, options, onEvent) {
|
|
|
332
494
|
await sleepWithinBudget(backoffMs(attempt));
|
|
333
495
|
continue;
|
|
334
496
|
}
|
|
497
|
+
// ── Expired access token → refresh once, then retry ──────────────────
|
|
498
|
+
//
|
|
499
|
+
// The backend issues access tokens with a 2-HOUR TTL and has always
|
|
500
|
+
// returned a long-lived rotating refresh token alongside them. The web app
|
|
501
|
+
// refreshes silently; the CLI and extension discarded the refresh token
|
|
502
|
+
// entirely and stored only the access token — so after exactly two hours,
|
|
503
|
+
// mid-session, every request started failing with "Invalid or expired
|
|
504
|
+
// token" and the run simply died with no recovery and no useful guidance.
|
|
505
|
+
// That is the "it just stops working after a while" report.
|
|
506
|
+
//
|
|
507
|
+
// Refreshed at most once per attempt (`refreshedThisAttempt`): if the new
|
|
508
|
+
// token is ALSO rejected, the problem isn't expiry and looping would just
|
|
509
|
+
// rotate the refresh token repeatedly against a server that keeps saying no
|
|
510
|
+
// — every rotation invalidating the previous one.
|
|
511
|
+
//
|
|
512
|
+
// A successful refresh retries transparently. This is before any streaming,
|
|
513
|
+
// so nothing has been rendered and nothing can be duplicated.
|
|
514
|
+
if (response.status === 401 || response.status === 403) {
|
|
515
|
+
const bodyText = await response.text().catch(() => '');
|
|
516
|
+
if (isExpiredTokenResponse(response.status, bodyText)) {
|
|
517
|
+
if (!refreshedThisAttempt) {
|
|
518
|
+
refreshedThisAttempt = true;
|
|
519
|
+
if (await refreshAccessToken()) {
|
|
520
|
+
reportRetry('Session expired — signing you back in');
|
|
521
|
+
attempt--; // a refresh is not a failed attempt; don't spend the budget on it
|
|
522
|
+
continue; // buildFetchArgs()/authHeaders() will pick up the new token
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// Either refreshing was impossible (no refresh token stored — a config
|
|
526
|
+
// predating this feature, or a NEXRALL_TOKEN credential), it was refused
|
|
527
|
+
// (expired/revoked/reused), or the freshly-minted token was rejected too.
|
|
528
|
+
// All three genuinely need a human.
|
|
529
|
+
//
|
|
530
|
+
// Signalled by BREAKING with a rewritten body rather than throwing: a
|
|
531
|
+
// throw here lands in this loop's own catch, which cannot tell it from a
|
|
532
|
+
// network failure and would retry it — firing a burst of full-context
|
|
533
|
+
// POSTs at a credential that will never be accepted. Breaking routes it
|
|
534
|
+
// through the terminal `!response.ok` handler below, which is exactly
|
|
535
|
+
// what a deterministic auth failure should use.
|
|
536
|
+
sessionExpired = true;
|
|
537
|
+
errorBodyOverride = JSON.stringify({
|
|
538
|
+
// Replaces the backend's bare "Invalid or expired token", which reads
|
|
539
|
+
// like a bug rather than something the user can act on.
|
|
540
|
+
error: 'Your session has expired. Run `nex login` to sign in again.',
|
|
541
|
+
});
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
// Not an expiry — hand the body we already consumed to the handlers below
|
|
545
|
+
// (node-fetch bodies are single-use).
|
|
546
|
+
errorBodyOverride = bodyText;
|
|
547
|
+
}
|
|
335
548
|
// 403 `{"error":{"type":"forbidden","message":"Request not allowed"}}` is
|
|
336
549
|
// Anthropic's own upstream response shape (reproduced verbatim through the AI
|
|
337
550
|
// Gateway passthrough — see services/aiGatewayClient.js), not something Nexrall's
|
|
@@ -345,7 +558,11 @@ async function streamChat(messages, options, onEvent) {
|
|
|
345
558
|
// burning the whole reconnect budget on something that will never succeed.
|
|
346
559
|
const FORBIDDEN_RETRY_LIMIT = 2;
|
|
347
560
|
if (response.status === 403) {
|
|
348
|
-
|
|
561
|
+
// The expiry check above already consumed the body on this path, and a
|
|
562
|
+
// node-fetch body is single-use — calling .text() again yields '' and the
|
|
563
|
+
// upstream-forbidden shape would never be recognised, turning a retryable
|
|
564
|
+
// blip into an instant terminal failure.
|
|
565
|
+
const bodyText = errorBodyOverride ?? await response.text().catch(() => '');
|
|
349
566
|
let isUpstreamForbidden = false;
|
|
350
567
|
try {
|
|
351
568
|
const parsed = JSON.parse(bodyText);
|
|
@@ -374,53 +591,49 @@ async function streamChat(messages, options, onEvent) {
|
|
|
374
591
|
errorBodyOverride = bodyText;
|
|
375
592
|
break;
|
|
376
593
|
}
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
discardedChars: renderedChars,
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
if (attempt < MAX_RETRIES && canRetry()) {
|
|
419
|
-
reportRetry('Could not resume — restarting this turn');
|
|
420
|
-
await sleepWithinBudget(backoffMs(attempt));
|
|
421
|
-
continue;
|
|
594
|
+
// A resume that the server refuses: this turnId can no longer be followed.
|
|
595
|
+
//
|
|
596
|
+
// 410 Gone — the buffered frame window moved past our cursor (we were
|
|
597
|
+
// away too long, or the turn produced a lot meanwhile).
|
|
598
|
+
// Continuity can't be proven and skipping frames would
|
|
599
|
+
// corrupt the message being rebuilt.
|
|
600
|
+
// 404 Not Found — the turn is gone entirely: nothing buffered AND no live
|
|
601
|
+
// registry record, which is what a deploy/pod replacement
|
|
602
|
+
// looks like from here.
|
|
603
|
+
//
|
|
604
|
+
// 404 used to fall straight through to the generic terminal-error handler
|
|
605
|
+
// below, so a backend rolling restart mid-turn killed the turn outright with
|
|
606
|
+
// "No such turn to resume." — even though the recovery path for it (mint a
|
|
607
|
+
// new turnId and restart) was already sitting right here for 410. That was
|
|
608
|
+
// the "disconnects and never reconnects" case.
|
|
609
|
+
//
|
|
610
|
+
// Matched on the backend's explicit `notResumable` flag in the body rather
|
|
611
|
+
// than on a hardcoded status list, so a new give-up condition on the server
|
|
612
|
+
// is handled the day it ships. The status check keeps 410 working even if a
|
|
613
|
+
// proxy strips the body.
|
|
614
|
+
if (resuming && response.status >= 400 && response.status < 500) {
|
|
615
|
+
// May already have been read by the token-expiry check above (401/403);
|
|
616
|
+
// node-fetch bodies are single-use, so reuse it rather than reading ''.
|
|
617
|
+
const body = errorBodyOverride ?? await response.text().catch(() => '');
|
|
618
|
+
if (response.status === 410 || response.status === 404 || bodySaysNotResumable(body)) {
|
|
619
|
+
const canRestart = await prepareRestart('Connection lost too long to resume');
|
|
620
|
+
if (!canRestart) {
|
|
621
|
+
// Output is already on screen and the caller can't take it back, so a restart
|
|
622
|
+
// would duplicate it. Surface the failure instead of corrupting the transcript.
|
|
623
|
+
throw new Error('Connection lost and this turn could no longer be resumed. Send your message again.');
|
|
624
|
+
}
|
|
625
|
+
if (attempt < MAX_RETRIES && canRetry()) {
|
|
626
|
+
reportRetry('Could not resume — restarting this turn');
|
|
627
|
+
await sleepWithinBudget(backoffMs(attempt));
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
errorBodyOverride = JSON.stringify({ error: 'Could not resume this turn — send your message again.' });
|
|
631
|
+
break;
|
|
422
632
|
}
|
|
423
|
-
|
|
633
|
+
// A 4xx during resume that is NOT a give-up signal (401/403 auth, 400 bad
|
|
634
|
+
// turnId). Deterministic — fall through to the terminal handler, reusing the
|
|
635
|
+
// body we already consumed since node-fetch bodies are single-use.
|
|
636
|
+
errorBodyOverride = body;
|
|
424
637
|
break;
|
|
425
638
|
}
|
|
426
639
|
// 409 from the backend's turn-idempotency gate. Two distinct meanings, and the
|
|
@@ -518,7 +731,16 @@ async function streamChat(messages, options, onEvent) {
|
|
|
518
731
|
// `retryable: true` on `forbiddenExhausted` hands the transient 403-upstream-forbidden
|
|
519
732
|
// case (see above) to the outer stream-retry loop instead of letting it die as a
|
|
520
733
|
// terminal error the moment the short in-connect-loop budget runs out.
|
|
521
|
-
|
|
734
|
+
//
|
|
735
|
+
// `authExpired` is the opposite signal: the credential is dead and no amount of
|
|
736
|
+
// reconnecting will help, so it must stay terminal AND be distinguishable, so the
|
|
737
|
+
// UI can offer a sign-in instead of showing a generic failure.
|
|
738
|
+
throw Object.assign(new Error(errMsg), {
|
|
739
|
+
status: response.status,
|
|
740
|
+
balance,
|
|
741
|
+
...(forbiddenExhausted ? { retryable: true } : {}),
|
|
742
|
+
...(sessionExpired ? { authExpired: true } : {}),
|
|
743
|
+
});
|
|
522
744
|
}
|
|
523
745
|
if (!response.body) {
|
|
524
746
|
throw new Error('Response body is null');
|
|
@@ -928,6 +1150,15 @@ async function streamChat(messages, options, onEvent) {
|
|
|
928
1150
|
reject(tagTransient(new Error(message)));
|
|
929
1151
|
}
|
|
930
1152
|
else {
|
|
1153
|
+
// Terminal error. The three branches above each stop the watchdog
|
|
1154
|
+
// and tear the socket down; this one used to do neither, so a
|
|
1155
|
+
// fatal SSE error left a 5-second interval running and the
|
|
1156
|
+
// response body open for the lifetime of the process. The
|
|
1157
|
+
// rejection settles the promise either way, which is exactly why
|
|
1158
|
+
// the leak was invisible — nothing downstream ever noticed, it
|
|
1159
|
+
// just accumulated one timer per failed turn across a long session.
|
|
1160
|
+
clearInterval(heartbeatWatchdog);
|
|
1161
|
+
stream.destroy?.();
|
|
931
1162
|
onEvent({ type: 'error', message });
|
|
932
1163
|
reject(new Error(message));
|
|
933
1164
|
}
|
|
@@ -1011,32 +1242,18 @@ async function streamChat(messages, options, onEvent) {
|
|
|
1011
1242
|
if (e.retryable && sAttempt < MAX_RETRIES && canRetry()) {
|
|
1012
1243
|
// The backend explicitly said this turnId's buffer is unrecoverable (gap in the
|
|
1013
1244
|
// Redis frame stream, or the owning pod is gone — see the `notResumable` case in
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1245
|
+
// the SSE parser above). Resuming with the same `lastEventId` would just hit the
|
|
1246
|
+
// same wall again, so release the old claim, mint a fresh turnId and restart
|
|
1247
|
+
// clean instead of falling into the "prefer resuming" branch below.
|
|
1248
|
+
//
|
|
1249
|
+
// Shares prepareRestart() with the HTTP-status path in runAttempt: the two used
|
|
1250
|
+
// to be independent copies of the same fifteen lines, and the cancel-then-remint
|
|
1251
|
+
// ordering they encode is exactly the kind of thing that silently drifts apart.
|
|
1018
1252
|
if (e.forceRestart) {
|
|
1019
|
-
await
|
|
1020
|
-
|
|
1021
|
-
resuming = false;
|
|
1022
|
-
serverResumable = false;
|
|
1023
|
-
lastEventId = 0;
|
|
1024
|
-
carryText = [];
|
|
1025
|
-
carryToolUse = [];
|
|
1026
|
-
const hadRendered = emittedAnythingAcrossAttempts;
|
|
1027
|
-
const renderedChars = emittedCharsAcrossAttempts;
|
|
1028
|
-
emittedAnythingAcrossAttempts = false;
|
|
1029
|
-
emittedCharsAcrossAttempts = 0;
|
|
1030
|
-
if (hadRendered && !allowRestartAfterRender) {
|
|
1253
|
+
const canRestart = await prepareRestart(err.message || 'Could not resume — restarting this turn');
|
|
1254
|
+
if (!canRestart) {
|
|
1031
1255
|
throw new Error('Connection lost and this turn could no longer be resumed. Send your message again.');
|
|
1032
1256
|
}
|
|
1033
|
-
if (hadRendered) {
|
|
1034
|
-
onEvent({
|
|
1035
|
-
type: 'stream_restart',
|
|
1036
|
-
reason: err.message || 'Could not resume — restarting this turn',
|
|
1037
|
-
discardedChars: renderedChars,
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
1257
|
reportRetry('Could not resume — restarting this turn');
|
|
1041
1258
|
await sleepWithinBudget(backoffMs(sAttempt));
|
|
1042
1259
|
continue;
|
|
@@ -1116,12 +1333,50 @@ async function cancelTurn(turnId) {
|
|
|
1116
1333
|
// Ignored deliberately — see above.
|
|
1117
1334
|
}
|
|
1118
1335
|
}
|
|
1336
|
+
// ─── Logout ───────────────────────────────────────────────────────────────────
|
|
1337
|
+
/**
|
|
1338
|
+
* Revoke the stored refresh token server-side.
|
|
1339
|
+
*
|
|
1340
|
+
* Deleting the local config alone is NOT a logout once refresh tokens exist: the
|
|
1341
|
+
* token stays valid for its full 60-day life and can keep minting access tokens
|
|
1342
|
+
* for anyone who recovered it (a synced dotfile, a backup, a shared machine).
|
|
1343
|
+
* The backend already exposes /api/auth/logout for exactly this.
|
|
1344
|
+
*
|
|
1345
|
+
* Best-effort and never throws — a failed revoke must not stop the local
|
|
1346
|
+
* credentials from being cleared, which is the part the user can see.
|
|
1347
|
+
*/
|
|
1348
|
+
async function revokeRefreshToken() {
|
|
1349
|
+
const refreshToken = (0, index_1.getRefreshToken)();
|
|
1350
|
+
if (!refreshToken)
|
|
1351
|
+
return;
|
|
1352
|
+
try {
|
|
1353
|
+
await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/logout`, {
|
|
1354
|
+
method: 'POST',
|
|
1355
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1356
|
+
body: JSON.stringify({ refreshToken }),
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
// Offline logout: the local credentials still get cleared by the caller.
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1119
1363
|
// ─── Get Balance ──────────────────────────────────────────────────────────────
|
|
1120
1364
|
async function getBalance() {
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1365
|
+
const fetchOnce = () => (0, node_fetch_1.default)(`${exports.API_BASE}/api/code/balance`, { method: 'GET', headers: authHeaders() });
|
|
1366
|
+
let response = await fetchOnce();
|
|
1367
|
+
// Same silent refresh as the chat path. This endpoint is polled on session start
|
|
1368
|
+
// and after turns, so on an expired token it is frequently the FIRST thing the
|
|
1369
|
+
// user sees fail — and a startup that reports "API error 403" while the chat
|
|
1370
|
+
// path quietly recovers is just confusing.
|
|
1371
|
+
if (response.status === 401 || response.status === 403) {
|
|
1372
|
+
const body = await response.text().catch(() => '');
|
|
1373
|
+
if (isExpiredTokenResponse(response.status, body) && await refreshAccessToken()) {
|
|
1374
|
+
response = await fetchOnce();
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
throw new Error(`API error ${response.status}: ${body}`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1125
1380
|
if (!response.ok) {
|
|
1126
1381
|
const errText = await response.text();
|
|
1127
1382
|
throw new Error(`API error ${response.status}: ${errText}`);
|
|
@@ -1143,7 +1398,18 @@ async function exchangeVscodeCode(code) {
|
|
|
1143
1398
|
const data = (await response.json());
|
|
1144
1399
|
if (!data.token)
|
|
1145
1400
|
throw new Error('No token in exchange response');
|
|
1146
|
-
|
|
1401
|
+
// `refreshToken` is forwarded when present but is NOT expected today: unlike the
|
|
1402
|
+
// email-login path, /vscode-exchange mints a long-lived (30-day) token directly
|
|
1403
|
+
// rather than going through mintSession's short-access-token + refresh pair. So
|
|
1404
|
+
// the extension is not exposed to the 2-hour expiry that broke CLI sessions —
|
|
1405
|
+
// it just expires much later, with no silent renewal. Reading the field now means
|
|
1406
|
+
// that if the backend is ever switched to mintSession for consistency, the
|
|
1407
|
+
// extension starts refreshing silently instead of breaking at the 2-hour mark.
|
|
1408
|
+
return {
|
|
1409
|
+
token: data.token,
|
|
1410
|
+
...(data.refreshToken ? { refreshToken: data.refreshToken } : {}),
|
|
1411
|
+
email: data.user?.email ?? '',
|
|
1412
|
+
};
|
|
1147
1413
|
}
|
|
1148
1414
|
// ─── Login ────────────────────────────────────────────────────────────────────
|
|
1149
1415
|
async function login(email, password) {
|
|
@@ -1160,6 +1426,14 @@ async function login(email, password) {
|
|
|
1160
1426
|
if (!data.token) {
|
|
1161
1427
|
throw new Error('Login response missing token');
|
|
1162
1428
|
}
|
|
1163
|
-
|
|
1429
|
+
// KEEP THE REFRESH TOKEN. The backend has returned one on this path all along
|
|
1430
|
+
// (routes/auth.js mintSession) and pairs it with an access token that expires in
|
|
1431
|
+
// TWO HOURS. Discarding it here is what made a long working session die mid-run
|
|
1432
|
+
// with "Invalid or expired token" and no way back except a manual `nex login`.
|
|
1433
|
+
return {
|
|
1434
|
+
token: data.token,
|
|
1435
|
+
...(data.refreshToken ? { refreshToken: data.refreshToken } : {}),
|
|
1436
|
+
email: data.email ?? email,
|
|
1437
|
+
};
|
|
1164
1438
|
}
|
|
1165
1439
|
//# sourceMappingURL=client.js.map
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -4,4 +4,25 @@ export declare function loadAuth(): AuthConfig | null;
|
|
|
4
4
|
export declare function clearAuth(): void;
|
|
5
5
|
export declare function isAuthenticated(): boolean;
|
|
6
6
|
export declare function getToken(): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* The stored refresh token, or null when there isn't one to use.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately returns null for a token supplied via NEXRALL_TOKEN: that path is
|
|
11
|
+
* for CI/headless, where the caller owns the credential's lifecycle and there is
|
|
12
|
+
* no refresh token to pair with it. Silently reading a refresh token out of the
|
|
13
|
+
* config file while the access token came from the environment would also mix two
|
|
14
|
+
* different identities if they disagree.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getRefreshToken(): string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Replace the stored token pair after a successful refresh, preserving any other
|
|
19
|
+
* fields already in the config (notably `email`, which the refresh response does
|
|
20
|
+
* not echo back and which the UI shows).
|
|
21
|
+
*
|
|
22
|
+
* Refresh tokens ROTATE: the backend invalidates the presented one and returns a
|
|
23
|
+
* replacement, and reusing a spent token is treated as theft (`reuse_detected`)
|
|
24
|
+
* and revokes the family. So this must persist BOTH halves, and must not be
|
|
25
|
+
* skipped just because the access token is what the caller needed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function updateTokens(token: string, refreshToken?: string): void;
|
|
7
28
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/auth/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAiB3C,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAGjD;AAED,wBAAgB,QAAQ,IAAI,UAAU,GAAG,IAAI,CAwB5C;AAED,wBAAgB,SAAS,IAAI,IAAI,CAQhC;AAED,wBAAgB,eAAe,IAAI,OAAO,CAGzC;AAED,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAGxC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAiB3C,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAGjD;AAED,wBAAgB,QAAQ,IAAI,UAAU,GAAG,IAAI,CAwB5C;AAED,wBAAgB,SAAS,IAAI,IAAI,CAQhC;AAED,wBAAgB,eAAe,IAAI,OAAO,CAGzC;AAED,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAGxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAM/C;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAsBvE"}
|
package/dist/auth/index.js
CHANGED
|
@@ -38,6 +38,8 @@ exports.loadAuth = loadAuth;
|
|
|
38
38
|
exports.clearAuth = clearAuth;
|
|
39
39
|
exports.isAuthenticated = isAuthenticated;
|
|
40
40
|
exports.getToken = getToken;
|
|
41
|
+
exports.getRefreshToken = getRefreshToken;
|
|
42
|
+
exports.updateTokens = updateTokens;
|
|
41
43
|
const fs = __importStar(require("fs"));
|
|
42
44
|
const path = __importStar(require("path"));
|
|
43
45
|
const os = __importStar(require("os"));
|
|
@@ -97,4 +99,55 @@ function getToken() {
|
|
|
97
99
|
const auth = loadAuth();
|
|
98
100
|
return auth?.token ?? null;
|
|
99
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* The stored refresh token, or null when there isn't one to use.
|
|
104
|
+
*
|
|
105
|
+
* Deliberately returns null for a token supplied via NEXRALL_TOKEN: that path is
|
|
106
|
+
* for CI/headless, where the caller owns the credential's lifecycle and there is
|
|
107
|
+
* no refresh token to pair with it. Silently reading a refresh token out of the
|
|
108
|
+
* config file while the access token came from the environment would also mix two
|
|
109
|
+
* different identities if they disagree.
|
|
110
|
+
*/
|
|
111
|
+
function getRefreshToken() {
|
|
112
|
+
const envToken = process.env.NEXRALL_TOKEN;
|
|
113
|
+
if (envToken && envToken.trim().length > 0)
|
|
114
|
+
return null;
|
|
115
|
+
const auth = loadAuth();
|
|
116
|
+
const rt = auth?.refreshToken;
|
|
117
|
+
return typeof rt === 'string' && rt.length > 0 ? rt : null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Replace the stored token pair after a successful refresh, preserving any other
|
|
121
|
+
* fields already in the config (notably `email`, which the refresh response does
|
|
122
|
+
* not echo back and which the UI shows).
|
|
123
|
+
*
|
|
124
|
+
* Refresh tokens ROTATE: the backend invalidates the presented one and returns a
|
|
125
|
+
* replacement, and reusing a spent token is treated as theft (`reuse_detected`)
|
|
126
|
+
* and revokes the family. So this must persist BOTH halves, and must not be
|
|
127
|
+
* skipped just because the access token is what the caller needed.
|
|
128
|
+
*/
|
|
129
|
+
function updateTokens(token, refreshToken) {
|
|
130
|
+
// Read the file directly rather than via loadAuth(): under NEXRALL_TOKEN,
|
|
131
|
+
// loadAuth short-circuits to the env value and would let a refresh overwrite the
|
|
132
|
+
// on-disk config with a synthetic single-field object, destroying the user's
|
|
133
|
+
// real stored credentials.
|
|
134
|
+
let existing = {};
|
|
135
|
+
try {
|
|
136
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
137
|
+
const parsed = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
138
|
+
if (typeof parsed === 'object' && parsed !== null)
|
|
139
|
+
existing = parsed;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch { /* corrupt or unreadable — start clean rather than refuse to save */ }
|
|
143
|
+
ensureConfigDir();
|
|
144
|
+
const next = {
|
|
145
|
+
...existing,
|
|
146
|
+
token,
|
|
147
|
+
// Keep the previous refresh token if the server didn't send a new one, rather
|
|
148
|
+
// than deleting the only means of refreshing again.
|
|
149
|
+
...(refreshToken ? { refreshToken } : {}),
|
|
150
|
+
};
|
|
151
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
152
|
+
}
|
|
100
153
|
//# sourceMappingURL=index.js.map
|
|
@@ -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;AA4sEtE,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,CAoBrB"}
|
package/dist/tools/executor.js
CHANGED
|
@@ -405,13 +405,12 @@ async function writeFile(input, workDir) {
|
|
|
405
405
|
}
|
|
406
406
|
catch { /* ignore */ }
|
|
407
407
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
408
|
+
// Atomic (temp + rename), matching edit_file/multi_edit. A plain
|
|
409
|
+
// writeFileSync on the target truncates it first and then streams the
|
|
410
|
+
// content back, so an interrupt partway through a large write left a
|
|
411
|
+
// TRUNCATED source file and reported nothing — the worst outcome for the
|
|
412
|
+
// very tool most likely to be handed a huge payload.
|
|
413
|
+
atomicWrite(resolved, content, existingMode);
|
|
415
414
|
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
416
415
|
const lines = content.split('\n').length;
|
|
417
416
|
if (isNew) {
|
|
@@ -1272,15 +1271,102 @@ function preflightEdit(resolved) {
|
|
|
1272
1271
|
}
|
|
1273
1272
|
return { mode: stat.mode, content: fs.readFileSync(resolved, 'utf-8') };
|
|
1274
1273
|
}
|
|
1275
|
-
/**
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1274
|
+
/**
|
|
1275
|
+
* Write `data` to `resolved` atomically: build it in a sibling temp file, then
|
|
1276
|
+
* rename over the target. rename(2) is atomic within a filesystem, so a reader —
|
|
1277
|
+
* or a crash — sees either the entire old file or the entire new one, never a
|
|
1278
|
+
* half-written truncation.
|
|
1279
|
+
*
|
|
1280
|
+
* This matters most for exactly the case it used NOT to cover: a large
|
|
1281
|
+
* `write_file`. `fs.writeFileSync` on the target truncates it to zero and then
|
|
1282
|
+
* streams the content back in, so a Ctrl+C, an OOM or a machine losing power
|
|
1283
|
+
* partway through leaves a TRUNCATED source file with no error reported
|
|
1284
|
+
* anywhere — the silent-corruption failure mode this whole module tries to avoid.
|
|
1285
|
+
*
|
|
1286
|
+
* The temp name carries the pid: `withFileLock` in agent/loop.ts serialises
|
|
1287
|
+
* writes to one path within a process, but nothing coordinates two `nex`
|
|
1288
|
+
* processes (or a CLI and the VS Code extension) pointed at the same repo, and
|
|
1289
|
+
* a shared fixed suffix would let them clobber each other's staging file and
|
|
1290
|
+
* commit a mix of both payloads.
|
|
1291
|
+
*
|
|
1292
|
+
* `mode` is optional: a brand-new file has no prior permission bits to keep, and
|
|
1293
|
+
* should get the umask default that writeFileSync would have produced.
|
|
1294
|
+
*/
|
|
1295
|
+
function atomicWrite(resolved, data, mode) {
|
|
1296
|
+
// Follow a symlink to its real target before staging.
|
|
1297
|
+
//
|
|
1298
|
+
// rename(2) replaces the NAME, so renaming onto a symlink would delete the link
|
|
1299
|
+
// and leave a regular file in its place — silently breaking the indirection the
|
|
1300
|
+
// user set up (a dotfile linked into a repo, a linked config). Plain
|
|
1301
|
+
// writeFileSync followed the link and wrote through it, and that behaviour must
|
|
1302
|
+
// be preserved. Resolving to the real path also keeps the temp file on the same
|
|
1303
|
+
// filesystem as the actual target, which is what makes the rename atomic (and
|
|
1304
|
+
// not an EXDEV failure).
|
|
1305
|
+
let target = resolved;
|
|
1306
|
+
try {
|
|
1307
|
+
if (fs.lstatSync(resolved).isSymbolicLink())
|
|
1308
|
+
target = fs.realpathSync(resolved);
|
|
1309
|
+
}
|
|
1310
|
+
catch {
|
|
1311
|
+
// Doesn't exist yet (the common create case) or is unreadable — write to the
|
|
1312
|
+
// path as given, exactly as before.
|
|
1313
|
+
}
|
|
1314
|
+
const tmp = `${target}.nexrall_tmp_${process.pid}`;
|
|
1315
|
+
// ── Stage ────────────────────────────────────────────────────────────────
|
|
1316
|
+
// Creating the temp file needs WRITE permission on the containing directory,
|
|
1317
|
+
// which a direct write to an existing file does not. That difference is a real
|
|
1318
|
+
// (if uncommon) compatibility break: a writable file inside a read-only
|
|
1319
|
+
// directory used to be editable and would suddenly stop being. Atomicity is a
|
|
1320
|
+
// safety improvement, so it must not take away a capability the user had —
|
|
1321
|
+
// when staging is impossible for that structural reason, fall back to the
|
|
1322
|
+
// previous direct write rather than failing the tool call.
|
|
1323
|
+
try {
|
|
1324
|
+
fs.writeFileSync(tmp, data, 'utf-8');
|
|
1325
|
+
}
|
|
1326
|
+
catch (err) {
|
|
1327
|
+
const code = err.code;
|
|
1328
|
+
if (code === 'EACCES' || code === 'EPERM' || code === 'EROFS') {
|
|
1329
|
+
fs.writeFileSync(target, data, 'utf-8');
|
|
1330
|
+
if (mode !== undefined) {
|
|
1331
|
+
try {
|
|
1332
|
+
fs.chmodSync(target, mode);
|
|
1333
|
+
}
|
|
1334
|
+
catch { /* non-fatal */ }
|
|
1335
|
+
}
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
// ENOSPC, EDQUOT, EISDIR, … — genuine failures. Nothing was staged, so the
|
|
1339
|
+
// original file is untouched; surface it.
|
|
1340
|
+
try {
|
|
1341
|
+
fs.rmSync(tmp, { force: true });
|
|
1342
|
+
}
|
|
1343
|
+
catch { /* best effort */ }
|
|
1344
|
+
throw err;
|
|
1345
|
+
}
|
|
1346
|
+
// ── Commit ───────────────────────────────────────────────────────────────
|
|
1279
1347
|
try {
|
|
1280
|
-
|
|
1348
|
+
if (mode !== undefined) {
|
|
1349
|
+
try {
|
|
1350
|
+
fs.chmodSync(tmp, mode);
|
|
1351
|
+
}
|
|
1352
|
+
catch { /* non-fatal — keep going */ }
|
|
1353
|
+
}
|
|
1354
|
+
fs.renameSync(tmp, target);
|
|
1355
|
+
}
|
|
1356
|
+
catch (err) {
|
|
1357
|
+
// Never leave staging debris next to the user's source on a failed commit
|
|
1358
|
+
// (a cross-device target, a permissions change mid-write). The original file
|
|
1359
|
+
// is untouched, which is the entire point of staging.
|
|
1360
|
+
try {
|
|
1361
|
+
fs.rmSync(tmp, { force: true });
|
|
1362
|
+
}
|
|
1363
|
+
catch { /* best effort */ }
|
|
1364
|
+
throw err;
|
|
1281
1365
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1366
|
+
}
|
|
1367
|
+
/** Atomic write that preserves the original file's permission bits (mode). */
|
|
1368
|
+
function atomicWritePreservingMode(resolved, data, mode) {
|
|
1369
|
+
atomicWrite(resolved, data, mode);
|
|
1284
1370
|
}
|
|
1285
1371
|
// Normalize CRLF → LF so Windows files match LF old_strings.
|
|
1286
1372
|
function normalizeLF(s) {
|
package/dist/types.d.ts
CHANGED
|
@@ -186,7 +186,23 @@ export interface ToolResult {
|
|
|
186
186
|
exitCode?: number;
|
|
187
187
|
}
|
|
188
188
|
export interface AuthConfig {
|
|
189
|
+
/** Short-lived access token (the backend issues these with a 2-hour TTL). */
|
|
189
190
|
token: string;
|
|
191
|
+
/**
|
|
192
|
+
* Long-lived rotating refresh token, used to mint a new access token without a
|
|
193
|
+
* manual re-login.
|
|
194
|
+
*
|
|
195
|
+
* The backend has issued one on every login path for a long time and exposes
|
|
196
|
+
* POST /api/auth/refresh to exchange it — the web app uses both. The CLI and
|
|
197
|
+
* extension simply discarded it and stored the access token alone, so after
|
|
198
|
+
* exactly two hours every request began failing with "Invalid or expired
|
|
199
|
+
* token", mid-session, with no recovery: a long working session would just die.
|
|
200
|
+
*
|
|
201
|
+
* Optional because it is genuinely absent in two legitimate cases: a token
|
|
202
|
+
* supplied via NEXRALL_TOKEN (CI/headless, no refresh flow), and a config file
|
|
203
|
+
* written by a version that predates this field.
|
|
204
|
+
*/
|
|
205
|
+
refreshToken?: string;
|
|
190
206
|
email?: string;
|
|
191
207
|
}
|
|
192
208
|
export interface PermissionRequest {
|
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":"AAEA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,aAAa,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAKD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9D;AAKD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,iBAAiB,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACzE;AAED,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,YAAY,GAAG,eAAe,GAAG,UAAU,GAAG,aAAa,CAAC;AAInG,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAID,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,UAAU,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,kBAAkB,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IACjB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,0HAA0H;AAC1H,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,gBAAgB,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,QAAQ,GAChB,YAAY,GACZ,eAAe,GACf,uBAAuB,GACvB,aAAa,GACb,YAAY,GACZ,aAAa,GACb,gBAAgB,GAChB,wBAAwB,GACxB,qBAAqB,GACrB,aAAa,GACb,qBAAqB,GACrB,qBAAqB,GACrB,qBAAqB,CAAC;AAI1B,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,gEAAgE;IAChE,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvF,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChH,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACvF,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9E;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/E;;;;;OAKG;IACH,OAAO,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnD;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE,+FAA+F;IAC/F,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B;;;;;;;;;OASG;IACH,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACnE;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAClE;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnG,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,eAAe,EAAE,UAAU,CAAC;IAChD,qFAAqF;IACrF,iBAAiB,CAAC,EAAE,OAAO,sBAAsB,EAAE,iBAAiB,CAAC;IACrE;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,MAAM,EAAE,CAAC;IAClC,+FAA+F;IAC/F,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAC3C;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC7B;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,6CAA6C;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAOzF;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,cAAc,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,EAAE,GAAG,IAAI,CAG7D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.23",
|
|
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)",
|