nexrall-code 0.5.74 → 0.5.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +354 -73
- package/nex-watch.js +117 -0
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -14029,7 +14029,7 @@ ${diff3}${xfile}${sec}` };
|
|
|
14029
14029
|
}
|
|
14030
14030
|
return null;
|
|
14031
14031
|
}
|
|
14032
|
-
async function fetchUrl(input, _workDir2, _redirectCount = 0) {
|
|
14032
|
+
async function fetchUrl(input, _workDir2, _redirectCount = 0, abortSignal) {
|
|
14033
14033
|
const url = typeof input.url === "string" ? input.url : "";
|
|
14034
14034
|
if (!url)
|
|
14035
14035
|
return { error: "Missing required parameter: url" };
|
|
@@ -14073,8 +14073,20 @@ ${diff3}${xfile}${sec}` };
|
|
|
14073
14073
|
if (settled)
|
|
14074
14074
|
return;
|
|
14075
14075
|
settled = true;
|
|
14076
|
+
if (abortPoll !== void 0)
|
|
14077
|
+
clearInterval(abortPoll);
|
|
14076
14078
|
resolve3(result);
|
|
14077
14079
|
};
|
|
14080
|
+
let abortPoll;
|
|
14081
|
+
if (abortSignal) {
|
|
14082
|
+
abortPoll = setInterval(() => {
|
|
14083
|
+
if (abortSignal.aborted && !settled) {
|
|
14084
|
+
selfAborted = true;
|
|
14085
|
+
req.destroy();
|
|
14086
|
+
finish({ error: "Request stopped by user", interrupted: true });
|
|
14087
|
+
}
|
|
14088
|
+
}, 100);
|
|
14089
|
+
}
|
|
14078
14090
|
const req = transport.get(url, {
|
|
14079
14091
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
14080
14092
|
lookup: guardedLookup,
|
|
@@ -14096,7 +14108,7 @@ ${diff3}${xfile}${sec}` };
|
|
|
14096
14108
|
finish({ error: `Invalid redirect location: ${res.headers.location}` });
|
|
14097
14109
|
return;
|
|
14098
14110
|
}
|
|
14099
|
-
fetchUrl({ url: nextUrl }, void 0, _redirectCount + 1).then(finish);
|
|
14111
|
+
fetchUrl({ url: nextUrl }, void 0, _redirectCount + 1, abortSignal).then(finish);
|
|
14100
14112
|
return;
|
|
14101
14113
|
}
|
|
14102
14114
|
const contentType = res.headers["content-type"] ?? "";
|
|
@@ -14434,45 +14446,83 @@ ${lines.join("\n")}`;
|
|
|
14434
14446
|
}
|
|
14435
14447
|
}
|
|
14436
14448
|
var VALID_ASPECT_RATIOS = /* @__PURE__ */ new Set(["1:1", "9:16", "16:9", "3:4", "4:3", "3:2", "2:3", "5:4", "4:5", "21:9", "auto"]);
|
|
14437
|
-
function _httpsPost(url, body, headers, timeoutMs = 3e4) {
|
|
14449
|
+
function _httpsPost(url, body, headers, timeoutMs = 3e4, abortSignal) {
|
|
14438
14450
|
return new Promise((resolve3, reject) => {
|
|
14439
14451
|
const u = new URL(url);
|
|
14440
14452
|
const chunks = [];
|
|
14453
|
+
let settled = false;
|
|
14454
|
+
let abortPoll;
|
|
14455
|
+
const done = (fn) => {
|
|
14456
|
+
if (settled)
|
|
14457
|
+
return;
|
|
14458
|
+
settled = true;
|
|
14459
|
+
if (abortPoll)
|
|
14460
|
+
clearInterval(abortPoll);
|
|
14461
|
+
fn();
|
|
14462
|
+
};
|
|
14441
14463
|
const req = https3.request({ hostname: u.hostname, path: u.pathname + u.search, method: "POST", headers: { ...headers, "Content-Length": Buffer.byteLength(body) }, timeout: timeoutMs }, (res) => {
|
|
14442
14464
|
res.on("data", (c) => chunks.push(c));
|
|
14443
|
-
res.on("end", () => resolve3({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf-8") }));
|
|
14465
|
+
res.on("end", () => done(() => resolve3({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf-8") })));
|
|
14444
14466
|
});
|
|
14445
14467
|
req.on("timeout", () => {
|
|
14446
14468
|
req.destroy();
|
|
14447
|
-
reject(new Error(`Request timed out after ${timeoutMs}ms`));
|
|
14469
|
+
done(() => reject(new Error(`Request timed out after ${timeoutMs}ms`)));
|
|
14448
14470
|
});
|
|
14449
|
-
req.on("error", reject);
|
|
14471
|
+
req.on("error", (err) => done(() => reject(err)));
|
|
14472
|
+
if (abortSignal) {
|
|
14473
|
+
abortPoll = setInterval(() => {
|
|
14474
|
+
if (abortSignal.aborted && !settled) {
|
|
14475
|
+
req.destroy();
|
|
14476
|
+
done(() => reject(Object.assign(new Error("Stopped by user"), { name: "AbortError" })));
|
|
14477
|
+
}
|
|
14478
|
+
}, 100);
|
|
14479
|
+
}
|
|
14450
14480
|
req.write(body);
|
|
14451
14481
|
req.end();
|
|
14452
14482
|
});
|
|
14453
14483
|
}
|
|
14454
|
-
function _httpsGet(url, headers, timeoutMs = 2e4) {
|
|
14484
|
+
function _httpsGet(url, headers, timeoutMs = 2e4, abortSignal) {
|
|
14455
14485
|
return new Promise((resolve3, reject) => {
|
|
14456
14486
|
const u = new URL(url);
|
|
14457
14487
|
const transport = u.protocol === "https:" ? https3 : http3;
|
|
14458
14488
|
const chunks = [];
|
|
14489
|
+
let settled = false;
|
|
14490
|
+
let abortPoll;
|
|
14491
|
+
const done = (fn) => {
|
|
14492
|
+
if (settled)
|
|
14493
|
+
return;
|
|
14494
|
+
settled = true;
|
|
14495
|
+
if (abortPoll)
|
|
14496
|
+
clearInterval(abortPoll);
|
|
14497
|
+
fn();
|
|
14498
|
+
};
|
|
14459
14499
|
const req = transport.get({ hostname: u.hostname, path: u.pathname + u.search, headers, timeout: timeoutMs }, (res) => {
|
|
14460
14500
|
if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location) {
|
|
14461
14501
|
req.destroy();
|
|
14462
|
-
|
|
14502
|
+
done(() => {
|
|
14503
|
+
_httpsGet(new URL(res.headers.location, url).href, headers, timeoutMs, abortSignal).then(resolve3, reject);
|
|
14504
|
+
});
|
|
14463
14505
|
return;
|
|
14464
14506
|
}
|
|
14465
14507
|
res.on("data", (c) => chunks.push(c));
|
|
14466
|
-
res.on("end", () => resolve3({ status: res.statusCode ?? 0, body: Buffer.concat(chunks), contentType: res.headers["content-type"] ?? "" }));
|
|
14508
|
+
res.on("end", () => done(() => resolve3({ status: res.statusCode ?? 0, body: Buffer.concat(chunks), contentType: res.headers["content-type"] ?? "" })));
|
|
14467
14509
|
});
|
|
14468
14510
|
req.on("timeout", () => {
|
|
14469
14511
|
req.destroy();
|
|
14470
|
-
reject(new Error(`Download timed out after ${timeoutMs}ms`));
|
|
14512
|
+
done(() => reject(new Error(`Download timed out after ${timeoutMs}ms`)));
|
|
14471
14513
|
});
|
|
14472
|
-
req.on("error", reject);
|
|
14514
|
+
req.on("error", (err) => done(() => reject(err)));
|
|
14515
|
+
if (abortSignal) {
|
|
14516
|
+
abortPoll = setInterval(() => {
|
|
14517
|
+
if (abortSignal.aborted && !settled) {
|
|
14518
|
+
req.destroy();
|
|
14519
|
+
done(() => reject(Object.assign(new Error("Stopped by user"), { name: "AbortError" })));
|
|
14520
|
+
}
|
|
14521
|
+
}, 100);
|
|
14522
|
+
}
|
|
14473
14523
|
});
|
|
14474
14524
|
}
|
|
14475
|
-
async function generateImage(input, workDir) {
|
|
14525
|
+
async function generateImage(input, workDir, abortSignal) {
|
|
14476
14526
|
const prompt2 = typeof input.prompt === "string" ? input.prompt.trim() : "";
|
|
14477
14527
|
const relPath = typeof input.path === "string" ? input.path.trim() : "";
|
|
14478
14528
|
const aspectRatio = typeof input.aspect_ratio === "string" && VALID_ASPECT_RATIOS.has(input.aspect_ratio) ? input.aspect_ratio : "1:1";
|
|
@@ -14487,7 +14537,7 @@ ${lines.join("\n")}`;
|
|
|
14487
14537
|
let cost;
|
|
14488
14538
|
let balance;
|
|
14489
14539
|
try {
|
|
14490
|
-
const r2 = await _httpsPost(`${client_1.API_BASE}/api/code/assets/generate`, JSON.stringify({ prompt: prompt2, aspect_ratio: aspectRatio }), { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, 17e4);
|
|
14540
|
+
const r2 = await _httpsPost(`${client_1.API_BASE}/api/code/assets/generate`, JSON.stringify({ prompt: prompt2, aspect_ratio: aspectRatio }), { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, 17e4, abortSignal);
|
|
14491
14541
|
let json = {};
|
|
14492
14542
|
try {
|
|
14493
14543
|
json = JSON.parse(r2.body);
|
|
@@ -14501,6 +14551,8 @@ ${lines.join("\n")}`;
|
|
|
14501
14551
|
if (!imageUrl)
|
|
14502
14552
|
return { error: "Image generation succeeded but no URL was returned." };
|
|
14503
14553
|
} catch (err) {
|
|
14554
|
+
if (err.name === "AbortError")
|
|
14555
|
+
return { error: "generate_image stopped by user", interrupted: true };
|
|
14504
14556
|
return { error: `generate_image failed: ${err.message}` };
|
|
14505
14557
|
}
|
|
14506
14558
|
const destPath = resolvePath(relPath, workDir);
|
|
@@ -14509,7 +14561,7 @@ ${lines.join("\n")}`;
|
|
|
14509
14561
|
} catch (_) {
|
|
14510
14562
|
}
|
|
14511
14563
|
try {
|
|
14512
|
-
const dl = await _httpsGet(imageUrl, { "User-Agent": FETCH_USER_AGENT }, 6e4);
|
|
14564
|
+
const dl = await _httpsGet(imageUrl, { "User-Agent": FETCH_USER_AGENT }, 6e4, abortSignal);
|
|
14513
14565
|
if (dl.status !== 200)
|
|
14514
14566
|
return { error: `Failed to download generated image (HTTP ${dl.status}).` };
|
|
14515
14567
|
fs9.writeFileSync(destPath, dl.body);
|
|
@@ -14517,10 +14569,12 @@ ${lines.join("\n")}`;
|
|
|
14517
14569
|
const costNote = cost != null ? ` ($${cost.toFixed(2)} charged` + (balance != null ? `, balance $${balance.toFixed(2)})` : ")") : "";
|
|
14518
14570
|
return { output: `Generated and saved image to ${destPath} (${kb} KB)${costNote}.` };
|
|
14519
14571
|
} catch (err) {
|
|
14572
|
+
if (err.name === "AbortError")
|
|
14573
|
+
return { error: "generate_image stopped by user (image was generated and billed, but the download was interrupted)", interrupted: true };
|
|
14520
14574
|
return { error: `Failed to save image to ${destPath}: ${err.message}` };
|
|
14521
14575
|
}
|
|
14522
14576
|
}
|
|
14523
|
-
async function stockPhoto(input, workDir) {
|
|
14577
|
+
async function stockPhoto(input, workDir, abortSignal) {
|
|
14524
14578
|
const query = typeof input.query === "string" ? input.query.trim() : "";
|
|
14525
14579
|
const relPath = typeof input.path === "string" ? input.path.trim() : "";
|
|
14526
14580
|
const count = typeof input.count === "number" ? Math.min(Math.max(Math.floor(input.count), 1), 10) : 3;
|
|
@@ -14537,7 +14591,7 @@ ${lines.join("\n")}`;
|
|
|
14537
14591
|
u.searchParams.set("count", String(count));
|
|
14538
14592
|
if (orientation)
|
|
14539
14593
|
u.searchParams.set("orientation", orientation);
|
|
14540
|
-
const r2 = await _httpsGet(u.toString(), { Authorization: `Bearer ${token}` });
|
|
14594
|
+
const r2 = await _httpsGet(u.toString(), { Authorization: `Bearer ${token}` }, 2e4, abortSignal);
|
|
14541
14595
|
let json = {};
|
|
14542
14596
|
try {
|
|
14543
14597
|
json = JSON.parse(r2.body.toString("utf-8"));
|
|
@@ -14549,6 +14603,8 @@ ${lines.join("\n")}`;
|
|
|
14549
14603
|
if (!photos.length)
|
|
14550
14604
|
return { output: `No stock photos found for "${query}".` };
|
|
14551
14605
|
} catch (err) {
|
|
14606
|
+
if (err.name === "AbortError")
|
|
14607
|
+
return { error: "stock_photo stopped by user", interrupted: true };
|
|
14552
14608
|
return { error: `stock_photo search failed: ${err.message}` };
|
|
14553
14609
|
}
|
|
14554
14610
|
if (relPath) {
|
|
@@ -14561,7 +14617,7 @@ ${lines.join("\n")}`;
|
|
|
14561
14617
|
} catch (_) {
|
|
14562
14618
|
}
|
|
14563
14619
|
try {
|
|
14564
|
-
const dl = await _httpsGet(top.url, { "User-Agent": FETCH_USER_AGENT });
|
|
14620
|
+
const dl = await _httpsGet(top.url, { "User-Agent": FETCH_USER_AGENT }, 2e4, abortSignal);
|
|
14565
14621
|
if (dl.status !== 200)
|
|
14566
14622
|
return { error: `Failed to download photo (HTTP ${dl.status}).` };
|
|
14567
14623
|
fs9.writeFileSync(destPath, dl.body);
|
|
@@ -14569,6 +14625,8 @@ ${lines.join("\n")}`;
|
|
|
14569
14625
|
const credit = top.credit ? ` \u2014 Photo by ${top.credit} on Unsplash` : "";
|
|
14570
14626
|
return { output: `Downloaded "${top.alt || query}" to ${destPath} (${kb} KB).${credit}` };
|
|
14571
14627
|
} catch (err) {
|
|
14628
|
+
if (err.name === "AbortError")
|
|
14629
|
+
return { error: "stock_photo download stopped by user", interrupted: true };
|
|
14572
14630
|
return { error: `Failed to save photo to ${destPath}: ${err.message}` };
|
|
14573
14631
|
}
|
|
14574
14632
|
}
|
|
@@ -14576,7 +14634,7 @@ ${lines.join("\n")}`;
|
|
|
14576
14634
|
return { output: `Found ${photos.length} photo(s) for "${query}":
|
|
14577
14635
|
${lines.join("\n")}` };
|
|
14578
14636
|
}
|
|
14579
|
-
async function webSearch(input) {
|
|
14637
|
+
async function webSearch(input, _workDir2, abortSignal) {
|
|
14580
14638
|
const query = typeof input.query === "string" ? input.query.trim() : "";
|
|
14581
14639
|
if (!query)
|
|
14582
14640
|
return { error: "Missing required parameter: query" };
|
|
@@ -14584,7 +14642,7 @@ ${lines.join("\n")}` };
|
|
|
14584
14642
|
if (!token)
|
|
14585
14643
|
return { error: "Not authenticated. Run `nexrall-code login` first." };
|
|
14586
14644
|
try {
|
|
14587
|
-
const r2 = await _httpsPost(`${client_1.API_BASE}/api/code/tools/web_search`, JSON.stringify({ query }), { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, 3e4);
|
|
14645
|
+
const r2 = await _httpsPost(`${client_1.API_BASE}/api/code/tools/web_search`, JSON.stringify({ query }), { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, 3e4, abortSignal);
|
|
14588
14646
|
let json = {};
|
|
14589
14647
|
try {
|
|
14590
14648
|
json = JSON.parse(r2.body);
|
|
@@ -14597,6 +14655,8 @@ ${lines.join("\n")}` };
|
|
|
14597
14655
|
return { error: "web_search succeeded but returned no results text." };
|
|
14598
14656
|
return { output: text };
|
|
14599
14657
|
} catch (err) {
|
|
14658
|
+
if (err.name === "AbortError")
|
|
14659
|
+
return { error: "web_search stopped by user", interrupted: true };
|
|
14600
14660
|
return { error: `web_search failed: ${err.message}` };
|
|
14601
14661
|
}
|
|
14602
14662
|
}
|
|
@@ -14731,8 +14791,6 @@ ${expanded}` };
|
|
|
14731
14791
|
"delete_file",
|
|
14732
14792
|
"notebook_read",
|
|
14733
14793
|
"notebook_edit",
|
|
14734
|
-
"generate_image",
|
|
14735
|
-
"stock_photo",
|
|
14736
14794
|
"get_symbols",
|
|
14737
14795
|
"get_workspace_symbols",
|
|
14738
14796
|
"go_to_definition",
|
|
@@ -14752,6 +14810,14 @@ ${expanded}` };
|
|
|
14752
14810
|
return await todoWrite(input, agentScope);
|
|
14753
14811
|
if (name === "todo_read")
|
|
14754
14812
|
return await todoRead(input, agentScope);
|
|
14813
|
+
if (name === "fetch_url")
|
|
14814
|
+
return await fetchUrl(input, workDir, 0, abortSignal);
|
|
14815
|
+
if (name === "web_search")
|
|
14816
|
+
return await webSearch(input, workDir, abortSignal);
|
|
14817
|
+
if (name === "generate_image")
|
|
14818
|
+
return await generateImage(input, workDir, abortSignal);
|
|
14819
|
+
if (name === "stock_photo")
|
|
14820
|
+
return await stockPhoto(input, workDir, abortSignal);
|
|
14755
14821
|
if (WORKDIR_TOOLS.has(name)) {
|
|
14756
14822
|
return await TOOL_MAP[name](input, workDir);
|
|
14757
14823
|
}
|
|
@@ -17791,7 +17857,7 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
|
|
|
17791
17857
|
if (external !== null && external !== void 0) {
|
|
17792
17858
|
result = external;
|
|
17793
17859
|
} else if (options.mcpManager?.isMcpTool(name)) {
|
|
17794
|
-
const mcpOutput = await options.mcpManager.callTool(name, input);
|
|
17860
|
+
const mcpOutput = await options.mcpManager.callTool(name, input, options.abortSignal);
|
|
17795
17861
|
result = { output: mcpOutput ?? "" };
|
|
17796
17862
|
} else {
|
|
17797
17863
|
options.checkpointManager?.recordBeforeMutation(name, input);
|
|
@@ -18555,8 +18621,24 @@ var require_manager = __commonJS({
|
|
|
18555
18621
|
}
|
|
18556
18622
|
return tools;
|
|
18557
18623
|
}
|
|
18558
|
-
/**
|
|
18559
|
-
|
|
18624
|
+
/**
|
|
18625
|
+
* Execute an MCP tool call. Returns null if this tool is not an MCP tool.
|
|
18626
|
+
*
|
|
18627
|
+
* `abortSignal` is optional and best-effort: the underlying transports (stdio
|
|
18628
|
+
* JSON-RPC / HTTP / SSE) have no cancellation message in this client, so an
|
|
18629
|
+
* abort here does NOT stop the server from finishing its work — it only stops
|
|
18630
|
+
* THIS CALL from making the caller wait for it. Before this was added, Ctrl+C
|
|
18631
|
+
* during an MCP tool call was silently swallowed: loop.ts set
|
|
18632
|
+
* `abortSignal.aborted = true` but nothing downstream of `options.mcpManager
|
|
18633
|
+
* .callTool(name, input)` ever looked at it, so the user's only way out was
|
|
18634
|
+
* to wait out the full 60s MCP_TOOL_TIMEOUT_MS (or however long the server
|
|
18635
|
+
* actually took). Racing the same abortSignal the 60s timeout already races
|
|
18636
|
+
* against makes Ctrl+C return control immediately instead of up to a minute
|
|
18637
|
+
* later — matching how the `bash` tool already behaves (its own 200ms abort
|
|
18638
|
+
* poll in executor.ts), just without the ability to also kill a remote
|
|
18639
|
+
* server's in-flight work the way `bash` can kill a local child process.
|
|
18640
|
+
*/
|
|
18641
|
+
async callTool(prefixedName, input, abortSignal) {
|
|
18560
18642
|
const serverName = this._toolMap.get(prefixedName);
|
|
18561
18643
|
if (!serverName)
|
|
18562
18644
|
return null;
|
|
@@ -18566,7 +18648,21 @@ var require_manager = __commonJS({
|
|
|
18566
18648
|
const toolName = prefixedName.slice(serverName.length + 2);
|
|
18567
18649
|
const MCP_TOOL_TIMEOUT_MS = 6e4;
|
|
18568
18650
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP tool "${prefixedName}" timed out after ${MCP_TOOL_TIMEOUT_MS / 1e3}s`)), MCP_TOOL_TIMEOUT_MS));
|
|
18569
|
-
|
|
18651
|
+
const racers = [client.callTool(toolName, input), timeoutPromise];
|
|
18652
|
+
if (abortSignal) {
|
|
18653
|
+
let pollId;
|
|
18654
|
+
const abortPromise = new Promise((_, reject) => {
|
|
18655
|
+
pollId = setInterval(() => {
|
|
18656
|
+
if (abortSignal.aborted) {
|
|
18657
|
+
clearInterval(pollId);
|
|
18658
|
+
reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
|
|
18659
|
+
}
|
|
18660
|
+
}, 100);
|
|
18661
|
+
});
|
|
18662
|
+
racers.push(abortPromise);
|
|
18663
|
+
return Promise.race(racers).finally(() => clearInterval(pollId));
|
|
18664
|
+
}
|
|
18665
|
+
return Promise.race(racers);
|
|
18570
18666
|
}
|
|
18571
18667
|
/** True if this tool name belongs to an MCP server. */
|
|
18572
18668
|
isMcpTool(name) {
|
|
@@ -64732,23 +64828,50 @@ function footerText(info) {
|
|
|
64732
64828
|
}
|
|
64733
64829
|
var IntlWithSegmenter = Intl;
|
|
64734
64830
|
var graphemeSegmenter = typeof IntlWithSegmenter.Segmenter === "function" ? new IntlWithSegmenter.Segmenter(void 0, { granularity: "grapheme" }) : null;
|
|
64735
|
-
function
|
|
64736
|
-
if (!text)
|
|
64737
|
-
return "";
|
|
64831
|
+
function graphemeBoundaries(text) {
|
|
64738
64832
|
if (graphemeSegmenter) {
|
|
64739
|
-
|
|
64740
|
-
for (const { index } of graphemeSegmenter.segment(text))
|
|
64741
|
-
|
|
64742
|
-
|
|
64743
|
-
|
|
64744
|
-
|
|
64745
|
-
|
|
64746
|
-
|
|
64747
|
-
const
|
|
64748
|
-
|
|
64749
|
-
|
|
64750
|
-
|
|
64751
|
-
|
|
64833
|
+
const bounds2 = [0];
|
|
64834
|
+
for (const { index } of graphemeSegmenter.segment(text)) {
|
|
64835
|
+
if (index > 0)
|
|
64836
|
+
bounds2.push(index);
|
|
64837
|
+
}
|
|
64838
|
+
bounds2.push(text.length);
|
|
64839
|
+
return bounds2;
|
|
64840
|
+
}
|
|
64841
|
+
const bounds = [0];
|
|
64842
|
+
let i2 = 0;
|
|
64843
|
+
while (i2 < text.length) {
|
|
64844
|
+
let next = i2 + 1;
|
|
64845
|
+
const code = text.charCodeAt(i2);
|
|
64846
|
+
if (code >= 55296 && code <= 56319 && next < text.length) {
|
|
64847
|
+
const low = text.charCodeAt(next);
|
|
64848
|
+
if (low >= 56320 && low <= 57343)
|
|
64849
|
+
next++;
|
|
64850
|
+
}
|
|
64851
|
+
while (next < text.length && /[\u0300-\u036F\u1AB0-\u1AFF\u20D0-\u20F0]/.test(text[next]))
|
|
64852
|
+
next++;
|
|
64853
|
+
bounds.push(next);
|
|
64854
|
+
i2 = next;
|
|
64855
|
+
}
|
|
64856
|
+
return bounds;
|
|
64857
|
+
}
|
|
64858
|
+
function prevGraphemeBoundary(text, pos) {
|
|
64859
|
+
const bounds = graphemeBoundaries(text);
|
|
64860
|
+
let prev = 0;
|
|
64861
|
+
for (const b of bounds) {
|
|
64862
|
+
if (b >= pos)
|
|
64863
|
+
break;
|
|
64864
|
+
prev = b;
|
|
64865
|
+
}
|
|
64866
|
+
return prev;
|
|
64867
|
+
}
|
|
64868
|
+
function nextGraphemeBoundary(text, pos) {
|
|
64869
|
+
const bounds = graphemeBoundaries(text);
|
|
64870
|
+
for (const b of bounds) {
|
|
64871
|
+
if (b > pos)
|
|
64872
|
+
return b;
|
|
64873
|
+
}
|
|
64874
|
+
return text.length;
|
|
64752
64875
|
}
|
|
64753
64876
|
var handle = null;
|
|
64754
64877
|
var inkInstance = null;
|
|
@@ -64758,8 +64881,9 @@ var App2 = ({ onReady }) => {
|
|
|
64758
64881
|
const [items, setItems] = (0, import_react35.useState)([]);
|
|
64759
64882
|
const [footer, setFooterState] = (0, import_react35.useState)({ mode: "auto", autoApprove: false });
|
|
64760
64883
|
const { columns } = use_window_size_default();
|
|
64761
|
-
const [
|
|
64884
|
+
const [busy, setBusyState] = (0, import_react35.useState)(false);
|
|
64762
64885
|
const [line, setLineState] = (0, import_react35.useState)("");
|
|
64886
|
+
const [cursorPos, setCursorPosState] = (0, import_react35.useState)(0);
|
|
64763
64887
|
const [prompt2, setPrompt] = (0, import_react35.useState)(DEFAULT_PROMPT);
|
|
64764
64888
|
const { exit } = use_app_default();
|
|
64765
64889
|
const lineRef = (0, import_react35.useRef)("");
|
|
@@ -64768,18 +64892,30 @@ var App2 = ({ onReady }) => {
|
|
|
64768
64892
|
lineRef.current = value;
|
|
64769
64893
|
setLineState(value);
|
|
64770
64894
|
}, []);
|
|
64895
|
+
const cursorRef = (0, import_react35.useRef)(0);
|
|
64896
|
+
const setCursorPos = (0, import_react35.useCallback)((next) => {
|
|
64897
|
+
const value = typeof next === "function" ? next(cursorRef.current) : next;
|
|
64898
|
+
cursorRef.current = value;
|
|
64899
|
+
setCursorPosState(value);
|
|
64900
|
+
}, []);
|
|
64771
64901
|
const askResolverRef = (0, import_react35.useRef)(null);
|
|
64772
64902
|
const onLineRef = (0, import_react35.useRef)(null);
|
|
64903
|
+
const onQueuedLineRef = (0, import_react35.useRef)(null);
|
|
64904
|
+
const busyRef = (0, import_react35.useRef)(false);
|
|
64773
64905
|
const [live, setLiveState] = (0, import_react35.useState)("");
|
|
64774
64906
|
const print = (0, import_react35.useCallback)((text) => {
|
|
64775
64907
|
setItems((prev) => [...prev, { id: idCounter++, content: text }]);
|
|
64776
64908
|
}, []);
|
|
64777
64909
|
const setFooter = (0, import_react35.useCallback)((info) => setFooterState(info), []);
|
|
64778
64910
|
const setLive = (0, import_react35.useCallback)((text) => setLiveState(text), []);
|
|
64779
|
-
const
|
|
64911
|
+
const setBusy = (0, import_react35.useCallback)((value) => {
|
|
64912
|
+
busyRef.current = value;
|
|
64913
|
+
setBusyState(value);
|
|
64914
|
+
}, []);
|
|
64780
64915
|
const askLine = (0, import_react35.useCallback)((promptText) => {
|
|
64781
64916
|
setPrompt(promptText || DEFAULT_PROMPT);
|
|
64782
64917
|
setLine("");
|
|
64918
|
+
setCursorPos(0);
|
|
64783
64919
|
return new Promise((resolve3) => {
|
|
64784
64920
|
askResolverRef.current = (answer) => {
|
|
64785
64921
|
setPrompt(DEFAULT_PROMPT);
|
|
@@ -64790,43 +64926,107 @@ var App2 = ({ onReady }) => {
|
|
|
64790
64926
|
const onLine = (0, import_react35.useCallback)((cb) => {
|
|
64791
64927
|
onLineRef.current = cb;
|
|
64792
64928
|
}, []);
|
|
64929
|
+
const onQueuedLine = (0, import_react35.useCallback)((cb) => {
|
|
64930
|
+
onQueuedLineRef.current = cb;
|
|
64931
|
+
}, []);
|
|
64932
|
+
const onInterruptRef = (0, import_react35.useRef)(null);
|
|
64933
|
+
const onInterrupt = (0, import_react35.useCallback)((cb) => {
|
|
64934
|
+
onInterruptRef.current = cb;
|
|
64935
|
+
}, []);
|
|
64793
64936
|
use_input_default((char, key) => {
|
|
64794
|
-
if (!inputEnabled)
|
|
64795
|
-
return;
|
|
64796
64937
|
if (key.ctrl && char === "c") {
|
|
64938
|
+
const askResolve = askResolverRef.current;
|
|
64939
|
+
if (askResolve) {
|
|
64940
|
+
askResolverRef.current = null;
|
|
64941
|
+
setLine("");
|
|
64942
|
+
setCursorPos(0);
|
|
64943
|
+
askResolve("n");
|
|
64944
|
+
return;
|
|
64945
|
+
}
|
|
64946
|
+
if (busyRef.current && onInterruptRef.current) {
|
|
64947
|
+
onInterruptRef.current();
|
|
64948
|
+
return;
|
|
64949
|
+
}
|
|
64797
64950
|
exit();
|
|
64798
64951
|
return;
|
|
64799
64952
|
}
|
|
64800
64953
|
if (key.backspace || key.delete) {
|
|
64801
|
-
|
|
64954
|
+
if (key.delete) {
|
|
64955
|
+
setLine((s2) => {
|
|
64956
|
+
const pos = cursorRef.current;
|
|
64957
|
+
const end = nextGraphemeBoundary(s2, pos);
|
|
64958
|
+
return s2.slice(0, pos) + s2.slice(end);
|
|
64959
|
+
});
|
|
64960
|
+
return;
|
|
64961
|
+
}
|
|
64962
|
+
setLine((s2) => {
|
|
64963
|
+
const pos = cursorRef.current;
|
|
64964
|
+
if (pos === 0)
|
|
64965
|
+
return s2;
|
|
64966
|
+
const start = prevGraphemeBoundary(s2, pos);
|
|
64967
|
+
setCursorPos(start);
|
|
64968
|
+
return s2.slice(0, start) + s2.slice(pos);
|
|
64969
|
+
});
|
|
64970
|
+
return;
|
|
64971
|
+
}
|
|
64972
|
+
if (key.leftArrow) {
|
|
64973
|
+
setCursorPos((pos) => prevGraphemeBoundary(lineRef.current, pos));
|
|
64802
64974
|
return;
|
|
64803
64975
|
}
|
|
64804
|
-
if (key.
|
|
64976
|
+
if (key.rightArrow) {
|
|
64977
|
+
setCursorPos((pos) => nextGraphemeBoundary(lineRef.current, pos));
|
|
64978
|
+
return;
|
|
64979
|
+
}
|
|
64980
|
+
if (key.upArrow || key.downArrow || key.tab) {
|
|
64805
64981
|
return;
|
|
64806
64982
|
}
|
|
64807
64983
|
const newlineIdx = char.search(/[\r\n]/);
|
|
64808
64984
|
const hasNewline = key.return || newlineIdx !== -1;
|
|
64985
|
+
if (hasNewline && key.shift) {
|
|
64986
|
+
const pos = cursorRef.current;
|
|
64987
|
+
setLine((s2) => s2.slice(0, pos) + "\n" + s2.slice(pos));
|
|
64988
|
+
setCursorPos(pos + 1);
|
|
64989
|
+
return;
|
|
64990
|
+
}
|
|
64809
64991
|
if (hasNewline) {
|
|
64810
64992
|
const before = newlineIdx === -1 ? char : char.slice(0, newlineIdx);
|
|
64811
64993
|
const after = newlineIdx === -1 ? "" : char.slice(newlineIdx + 1).replace(/^[\r\n]/, "");
|
|
64812
|
-
const
|
|
64994
|
+
const pos = cursorRef.current;
|
|
64995
|
+
const submitted = lineRef.current.slice(0, pos) + before + lineRef.current.slice(pos);
|
|
64813
64996
|
setLine(after);
|
|
64997
|
+
setCursorPos(0);
|
|
64814
64998
|
print(prompt2 + submitted);
|
|
64815
64999
|
const askResolve = askResolverRef.current;
|
|
64816
65000
|
if (askResolve) {
|
|
64817
65001
|
askResolverRef.current = null;
|
|
64818
65002
|
askResolve(submitted);
|
|
65003
|
+
} else if (busyRef.current) {
|
|
65004
|
+
onQueuedLineRef.current?.(submitted);
|
|
64819
65005
|
} else {
|
|
64820
65006
|
onLineRef.current?.(submitted);
|
|
64821
65007
|
}
|
|
64822
65008
|
return;
|
|
64823
65009
|
}
|
|
64824
|
-
setLine((s2) =>
|
|
65010
|
+
setLine((s2) => {
|
|
65011
|
+
const pos = cursorRef.current;
|
|
65012
|
+
setCursorPos(pos + char.length);
|
|
65013
|
+
return s2.slice(0, pos) + char + s2.slice(pos);
|
|
65014
|
+
});
|
|
64825
65015
|
});
|
|
64826
65016
|
import_react35.default.useEffect(() => {
|
|
64827
|
-
onReady({
|
|
65017
|
+
onReady({
|
|
65018
|
+
print,
|
|
65019
|
+
setFooter,
|
|
65020
|
+
setLive,
|
|
65021
|
+
askLine,
|
|
65022
|
+
onLine,
|
|
65023
|
+
onQueuedLine,
|
|
65024
|
+
onInterrupt,
|
|
65025
|
+
setBusy,
|
|
65026
|
+
close: () => exit()
|
|
65027
|
+
});
|
|
64828
65028
|
}, []);
|
|
64829
|
-
return /* @__PURE__ */ import_react35.default.createElement(Box_default, { flexDirection: "column" }, /* @__PURE__ */ import_react35.default.createElement(Static, { items }, (item) => /* @__PURE__ */ import_react35.default.createElement(Text, { key: item.id }, item.content)), live ? /* @__PURE__ */ import_react35.default.createElement(Text, null, live) : null, /* @__PURE__ */ import_react35.default.createElement(Text, { dimColor: true }, "\u2500".repeat(Math.max(1, columns - 1))),
|
|
65029
|
+
return /* @__PURE__ */ import_react35.default.createElement(Box_default, { flexDirection: "column" }, /* @__PURE__ */ import_react35.default.createElement(Static, { items }, (item) => /* @__PURE__ */ import_react35.default.createElement(Text, { key: item.id }, item.content)), live ? /* @__PURE__ */ import_react35.default.createElement(Text, null, live) : null, /* @__PURE__ */ import_react35.default.createElement(Text, { dimColor: true }, "\u2500".repeat(Math.max(1, columns - 1))), /* @__PURE__ */ import_react35.default.createElement(Box_default, null, /* @__PURE__ */ import_react35.default.createElement(Text, null, prompt2), /* @__PURE__ */ import_react35.default.createElement(Text, null, line.slice(0, cursorPos)), /* @__PURE__ */ import_react35.default.createElement(Text, { inverse: true }, line.slice(cursorPos, cursorPos + 1) || " "), /* @__PURE__ */ import_react35.default.createElement(Text, null, line.slice(cursorPos + 1))), /* @__PURE__ */ import_react35.default.createElement(Text, { dimColor: true }, footerText(footer), busy ? " \xB7 agent working\u2026" : ""));
|
|
64830
65030
|
};
|
|
64831
65031
|
function startInkTerminal() {
|
|
64832
65032
|
if (handle)
|
|
@@ -64842,7 +65042,36 @@ function startInkTerminal() {
|
|
|
64842
65042
|
}
|
|
64843
65043
|
}
|
|
64844
65044
|
),
|
|
64845
|
-
{
|
|
65045
|
+
{
|
|
65046
|
+
exitOnCtrlC: false,
|
|
65047
|
+
// Opt into the Kitty keyboard protocol so `key.shift` is actually
|
|
65048
|
+
// populated for Enter (see the Shift+Enter handling in useInput
|
|
65049
|
+
// above) instead of always being false/undefined.
|
|
65050
|
+
//
|
|
65051
|
+
// MUST be 'enabled', not 'auto'. 'auto' has Ink send a CSI `?u` query
|
|
65052
|
+
// to the terminal and then listen on `stdin`'s 'data' event for the
|
|
65053
|
+
// reply (see ink/build/ink.js confirmKittySupport). That listener
|
|
65054
|
+
// races Ink's own input pipeline, which reads stdin via 'readable' +
|
|
65055
|
+
// `.read()` (paused mode) for its normal keystroke handling — Node
|
|
65056
|
+
// streams do not support mixing 'data' (flowing) and 'readable'
|
|
65057
|
+
// (paused) consumers safely, and in practice the 'readable' consumer
|
|
65058
|
+
// wins the terminal's query response. The reply itself (`\x1b[?1u`)
|
|
65059
|
+
// then gets fed through Ink's normal key parser, which doesn't
|
|
65060
|
+
// recognise it as any special key and inserts it as literal text —
|
|
65061
|
+
// this is exactly the "[?1u" that appears in the input box right
|
|
65062
|
+
// after Ink mounts (e.g. right after confirming the trust prompt).
|
|
65063
|
+
//
|
|
65064
|
+
// 'enabled' skips the query/response dance entirely: Ink just writes
|
|
65065
|
+
// the enable sequence once (`\x1b[>1u`) and never listens for a
|
|
65066
|
+
// reply, so there is nothing to leak into the input box. Terminals
|
|
65067
|
+
// that don't support the protocol silently ignore the unrecognised
|
|
65068
|
+
// escape sequence (the same way they ignore bracketed-paste mode),
|
|
65069
|
+
// so Shift+Enter still safely falls back to the '\' continuation
|
|
65070
|
+
// there — this trades "verify support first" for "assume support,
|
|
65071
|
+
// no-op if absent", which is safe here because the only visible
|
|
65072
|
+
// effect of enabling on an unsupporting terminal is nothing at all.
|
|
65073
|
+
kittyKeyboard: { mode: "enabled", flags: ["disambiguateEscapeCodes"] }
|
|
65074
|
+
}
|
|
64846
65075
|
);
|
|
64847
65076
|
});
|
|
64848
65077
|
}
|
|
@@ -64867,26 +65096,31 @@ var InkReadlineAdapter = class extends EventEmitter3 {
|
|
|
64867
65096
|
line = "";
|
|
64868
65097
|
cursor = 0;
|
|
64869
65098
|
ink;
|
|
64870
|
-
|
|
65099
|
+
busy = false;
|
|
65100
|
+
pendingInput = [];
|
|
64871
65101
|
constructor(ink) {
|
|
64872
65102
|
super();
|
|
64873
65103
|
this.ink = ink;
|
|
64874
65104
|
this.ink.onLine((text) => {
|
|
64875
|
-
if (this.paused)
|
|
64876
|
-
return;
|
|
64877
65105
|
this.emit("line", text);
|
|
64878
65106
|
});
|
|
65107
|
+
this.ink.onQueuedLine((text) => {
|
|
65108
|
+
this.pendingInput.push(text);
|
|
65109
|
+
});
|
|
65110
|
+
this.ink.onInterrupt(() => {
|
|
65111
|
+
this.emit("interrupt");
|
|
65112
|
+
});
|
|
64879
65113
|
}
|
|
64880
65114
|
prompt(_preserveCursor) {
|
|
64881
65115
|
}
|
|
64882
65116
|
pause() {
|
|
64883
|
-
this.
|
|
64884
|
-
this.ink.
|
|
65117
|
+
this.busy = true;
|
|
65118
|
+
this.ink.setBusy(true);
|
|
64885
65119
|
return this;
|
|
64886
65120
|
}
|
|
64887
65121
|
resume() {
|
|
64888
|
-
this.
|
|
64889
|
-
this.ink.
|
|
65122
|
+
this.busy = false;
|
|
65123
|
+
this.ink.setBusy(false);
|
|
64890
65124
|
return this;
|
|
64891
65125
|
}
|
|
64892
65126
|
close() {
|
|
@@ -64896,6 +65130,21 @@ var InkReadlineAdapter = class extends EventEmitter3 {
|
|
|
64896
65130
|
question(query, cb) {
|
|
64897
65131
|
this.ink.askLine(query).then(cb);
|
|
64898
65132
|
}
|
|
65133
|
+
/**
|
|
65134
|
+
* Drains and returns every follow-up message the user typed and sent while
|
|
65135
|
+
* `pause()` was active (i.e. during a running agent turn). Intended to be
|
|
65136
|
+
* passed straight through as `AgentLoopOptions.takePendingInput` — see
|
|
65137
|
+
* loop.ts, and the VS Code panel's `_pendingInjections.splice(0)` for the
|
|
65138
|
+
* pattern this mirrors. Returns `[]` when nothing is queued, and empties
|
|
65139
|
+
* the queue on every call so the same follow-up is never folded in twice.
|
|
65140
|
+
*/
|
|
65141
|
+
takePendingInput() {
|
|
65142
|
+
return this.pendingInput.splice(0);
|
|
65143
|
+
}
|
|
65144
|
+
/** True while a `pause()`/`resume()` pair is in effect (an agent turn is running). */
|
|
65145
|
+
isBusy() {
|
|
65146
|
+
return this.busy;
|
|
65147
|
+
}
|
|
64899
65148
|
};
|
|
64900
65149
|
|
|
64901
65150
|
// src/commands/chat.ts
|
|
@@ -65103,23 +65352,29 @@ var Spinner = class {
|
|
|
65103
65352
|
* No-op when the spinner isn't active: progress events can arrive a tick after
|
|
65104
65353
|
* something else (a tool row, the first text delta) legitimately stopped it, and
|
|
65105
65354
|
* resurrecting the spinner there would fight the printed output for the status line.
|
|
65355
|
+
*
|
|
65356
|
+
* Deliberately does NOT call render() immediately. onThinkingDelta/onThinkingProgress
|
|
65357
|
+
* can fire many times per second while the model streams (one SSE delta each), and
|
|
65358
|
+
* each of those used to trigger its own Ink re-render (setLive -> React state update ->
|
|
65359
|
+
* full-frame clear/redraw) layered on TOP of the interval's own 100ms redraw already
|
|
65360
|
+
* running in start(). Two independent, unsynchronized render sources hitting the same
|
|
65361
|
+
* frame is exactly what produced the visible jitter users reported ("dòng working...
|
|
65362
|
+
* giật giật liên tục") — every fast delta briefly repainted the status line out of
|
|
65363
|
+
* step with the spinner's own tick. Just updating the field here and letting the
|
|
65364
|
+
* existing setInterval pick it up on its next 100ms tick caps the redraw rate at a
|
|
65365
|
+
* steady 10fps with zero perceptible added latency (worst case: one stale frame for
|
|
65366
|
+
* <100ms), and removes the race entirely.
|
|
65106
65367
|
*/
|
|
65107
65368
|
setDetail(detail) {
|
|
65108
65369
|
if (this.interval === null)
|
|
65109
65370
|
return;
|
|
65110
|
-
if (this.detail === detail)
|
|
65111
|
-
return;
|
|
65112
65371
|
this.detail = detail;
|
|
65113
|
-
this.render();
|
|
65114
65372
|
}
|
|
65115
|
-
/** Swap the label without resetting the elapsed timer (phase change within one turn). */
|
|
65373
|
+
/** Swap the label without resetting the elapsed timer (phase change within one turn). Same reasoning as setDetail() above: no immediate render, the interval tick picks it up. */
|
|
65116
65374
|
setText(text) {
|
|
65117
65375
|
if (this.interval === null)
|
|
65118
65376
|
return;
|
|
65119
|
-
if (this.baseText === text)
|
|
65120
|
-
return;
|
|
65121
65377
|
this.baseText = text;
|
|
65122
|
-
this.render();
|
|
65123
65378
|
}
|
|
65124
65379
|
render() {
|
|
65125
65380
|
getInkTerminal()?.setLive(composeStatusLine({
|
|
@@ -65211,7 +65466,7 @@ function formatAgentsList(workDir) {
|
|
|
65211
65466
|
}
|
|
65212
65467
|
return lines.join("\n");
|
|
65213
65468
|
}
|
|
65214
|
-
async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrallMd, mode, effort, checkpointManager, onProgress) {
|
|
65469
|
+
async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrallMd, mode, effort, checkpointManager, onProgress, takePendingInput) {
|
|
65215
65470
|
let lastUsage;
|
|
65216
65471
|
const spinner = new Spinner();
|
|
65217
65472
|
const mdRender = new MarkdownStreamRenderer();
|
|
@@ -65252,6 +65507,14 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
|
|
|
65252
65507
|
// prompt, and sub-agents inherit the lock.
|
|
65253
65508
|
planMode: mode === "plan",
|
|
65254
65509
|
effort,
|
|
65510
|
+
// Claude-Code-style follow-ups: text the user typed and sent WHILE this
|
|
65511
|
+
// turn was already running. inkTerminal.tsx echoes each one into the
|
|
65512
|
+
// transcript itself the moment it's submitted (chronological, matching
|
|
65513
|
+
// the VS Code panel's queueMessage) — so onInjectedInput is intentionally
|
|
65514
|
+
// a no-op here rather than printing it again.
|
|
65515
|
+
takePendingInput,
|
|
65516
|
+
onInjectedInput: () => {
|
|
65517
|
+
},
|
|
65255
65518
|
// The backend streams a cumulative output-token count (routes/code.js's
|
|
65256
65519
|
// sendProgress, throttled to ≤5/s) covering thinking, visible text AND
|
|
65257
65520
|
// tool-argument JSON. This used to be stored in a variable and rendered only
|
|
@@ -65569,6 +65832,8 @@ Set ${TRUST_ENV_VAR}=1 to confirm non-interactively, or run \`nex\` interactivel
|
|
|
65569
65832
|
process.on("exit", () => {
|
|
65570
65833
|
stopInkTerminal();
|
|
65571
65834
|
});
|
|
65835
|
+
if (process.stdin.isTTY)
|
|
65836
|
+
process.stdin.setRawMode(true);
|
|
65572
65837
|
await startInkTerminal();
|
|
65573
65838
|
startRowCount();
|
|
65574
65839
|
}
|
|
@@ -65721,7 +65986,7 @@ ${text}` : "");
|
|
|
65721
65986
|
} else {
|
|
65722
65987
|
console.error(source_default.red("\nError: ") + String(err.message));
|
|
65723
65988
|
if (salvaged) {
|
|
65724
|
-
console.error(source_default.dim(" Progress was saved \u2014 run `nex --
|
|
65989
|
+
console.error(source_default.dim(" Progress was saved \u2014 run `nex --resume` (or `nex -r`) to resume this session."));
|
|
65725
65990
|
}
|
|
65726
65991
|
}
|
|
65727
65992
|
process.exit(1);
|
|
@@ -65752,12 +66017,22 @@ ${text}` : "");
|
|
|
65752
66017
|
}
|
|
65753
66018
|
});
|
|
65754
66019
|
const rl = interactive ? new InkReadlineAdapter(getInkTerminal()) : readline3.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
66020
|
+
if (rl instanceof InkReadlineAdapter) {
|
|
66021
|
+
rl.on("interrupt", () => {
|
|
66022
|
+
if (agentRunning) {
|
|
66023
|
+
console.log("\n" + source_default.yellow(" Interrupted."));
|
|
66024
|
+
abortSignal.aborted = true;
|
|
66025
|
+
agentRunning = false;
|
|
66026
|
+
}
|
|
66027
|
+
});
|
|
66028
|
+
}
|
|
65755
66029
|
setReadlineInterface(rl);
|
|
65756
66030
|
const updateFooter = () => {
|
|
65757
66031
|
if (interactive)
|
|
65758
66032
|
getInkTerminal().setFooter({ mode: agentMode, autoApprove: isYoloMode() });
|
|
65759
66033
|
};
|
|
65760
66034
|
updateFooter();
|
|
66035
|
+
const takePendingInput = rl instanceof InkReadlineAdapter ? () => rl.takePendingInput() : void 0;
|
|
65761
66036
|
let inputBuffer = "";
|
|
65762
66037
|
rl.on("line", async (rawLine) => {
|
|
65763
66038
|
if (rawLine.endsWith("\\")) {
|
|
@@ -65911,7 +66186,10 @@ ${convText}`;
|
|
|
65911
66186
|
env3,
|
|
65912
66187
|
nexrallMd,
|
|
65913
66188
|
agentMode,
|
|
65914
|
-
effortLevel
|
|
66189
|
+
effortLevel,
|
|
66190
|
+
void 0,
|
|
66191
|
+
void 0,
|
|
66192
|
+
takePendingInput
|
|
65915
66193
|
);
|
|
65916
66194
|
agentRunning = false;
|
|
65917
66195
|
const summaryText = [...r2.messages].reverse().find((m2) => m2.role === "assistant")?.content[0]?.text ?? "";
|
|
@@ -65970,7 +66248,10 @@ ${dirList}`;
|
|
|
65970
66248
|
env3,
|
|
65971
66249
|
nexrallMd,
|
|
65972
66250
|
"auto",
|
|
65973
|
-
effortLevel
|
|
66251
|
+
effortLevel,
|
|
66252
|
+
void 0,
|
|
66253
|
+
void 0,
|
|
66254
|
+
takePendingInput
|
|
65974
66255
|
);
|
|
65975
66256
|
agentRunning = false;
|
|
65976
66257
|
const content = [...r2.messages].reverse().find((m2) => m2.role === "assistant")?.content[0]?.text ?? "";
|
|
@@ -66189,7 +66470,7 @@ ${text}
|
|
|
66189
66470
|
abortSignal.aborted = false;
|
|
66190
66471
|
agentRunning = true;
|
|
66191
66472
|
try {
|
|
66192
|
-
const result = await runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrallMd, agentMode, effortLevel, checkpoints, saveProgress);
|
|
66473
|
+
const result = await runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrallMd, agentMode, effortLevel, checkpoints, saveProgress, takePendingInput);
|
|
66193
66474
|
messages = result.messages;
|
|
66194
66475
|
updateTitle();
|
|
66195
66476
|
saveSession(sessionId, sessionTitle, workDir, messages);
|
|
@@ -66309,7 +66590,7 @@ ${text}
|
|
|
66309
66590
|
abortSignal.aborted = false;
|
|
66310
66591
|
agentRunning = true;
|
|
66311
66592
|
try {
|
|
66312
|
-
const result = await runTurn(messages, turnModel, workDir, abortSignal, env3, nexrallMd, turnMode, effortLevel, checkpoints, saveProgress);
|
|
66593
|
+
const result = await runTurn(messages, turnModel, workDir, abortSignal, env3, nexrallMd, turnMode, effortLevel, checkpoints, saveProgress, takePendingInput);
|
|
66313
66594
|
messages = result.messages;
|
|
66314
66595
|
updateTitle();
|
|
66315
66596
|
saveSession(sessionId, sessionTitle, workDir, messages);
|
|
@@ -66333,7 +66614,7 @@ ${text}
|
|
|
66333
66614
|
abortSignal.aborted = false;
|
|
66334
66615
|
agentRunning = true;
|
|
66335
66616
|
try {
|
|
66336
|
-
const result = await runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrallMd, agentMode, effortLevel, checkpoints, saveProgress);
|
|
66617
|
+
const result = await runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrallMd, agentMode, effortLevel, checkpoints, saveProgress, takePendingInput);
|
|
66337
66618
|
messages = result.messages;
|
|
66338
66619
|
updateTitle();
|
|
66339
66620
|
saveSession(sessionId, sessionTitle, workDir, messages);
|
package/nex-watch.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// nex-watch — auto-relaunch `nex` if the OS kills it with SIGKILL.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// nex-watch [any nex flags/prompt...]
|
|
6
|
+
// nex-watch # interactive session, auto-resumes on kill
|
|
7
|
+
// nex-watch -y "long unattended task"
|
|
8
|
+
//
|
|
9
|
+
// Why this exists (not something `nex` itself can do):
|
|
10
|
+
// SIGKILL (signal 9 — what a shell reports as "killed") is the one POSIX
|
|
11
|
+
// signal a process can NEVER intercept, handle, or clean up after — the
|
|
12
|
+
// kernel tears it down before a single line of the process's own code can
|
|
13
|
+
// run. On macOS this is the Jetsam low-memory killer; on Linux it's the
|
|
14
|
+
// OOM killer. Both pick a victim purely from system-wide memory pressure at
|
|
15
|
+
// that instant — a machine with plenty of total RAM is not immune if
|
|
16
|
+
// something else (browser, IDE, other builds) is using nearly all of it
|
|
17
|
+
// right now. No amount of code inside `nex` can catch or prevent this.
|
|
18
|
+
//
|
|
19
|
+
// What this script actually does:
|
|
20
|
+
// `nex` already persists its conversation to ~/.nexrall/cli-sessions on
|
|
21
|
+
// every turn boundary (≤3s debounce), independent of this script. That
|
|
22
|
+
// means a SIGKILL loses at most a few seconds of progress, not the whole
|
|
23
|
+
// session — but resuming still requires a human to notice the crash and
|
|
24
|
+
// type `nex --resume`. This script automates exactly that: it spawns nex
|
|
25
|
+
// as a child process and watches HOW it exits. Node's child_process
|
|
26
|
+
// reports the terminating signal directly (no exit-code-to-signal
|
|
27
|
+
// guesswork needed) — if nex died from SIGKILL or SIGTERM specifically,
|
|
28
|
+
// this relaunches it with `--resume last` so the run continues from the
|
|
29
|
+
// last saved turn instead of silently stopping. Any OTHER exit (success,
|
|
30
|
+
// a real error, Ctrl+C which nex already handles gracefully) stops the
|
|
31
|
+
// watch loop — this is a safety net for the un-catchable kill case, not a
|
|
32
|
+
// generic "keep retrying forever" wrapper.
|
|
33
|
+
//
|
|
34
|
+
// A capped retry count (not infinite) matters: if the machine is so starved
|
|
35
|
+
// of memory that every relaunch gets killed again within seconds, retrying
|
|
36
|
+
// forever would just contribute to the same memory pressure that caused the
|
|
37
|
+
// first kill. Capping means it gives up loudly instead of thrashing.
|
|
38
|
+
// Cross-platform note: on Windows there is no SIGKILL/SIGTERM in the POSIX
|
|
39
|
+
// sense — a killed process there typically just reports a non-zero exit
|
|
40
|
+
// code with signal:null. This tool's relaunch-on-signal logic is therefore
|
|
41
|
+
// a no-op (and harmless) on Windows; the memory-pressure-kill scenario it
|
|
42
|
+
// guards against is a macOS/Linux OOM-killer phenomenon in the first place.
|
|
43
|
+
|
|
44
|
+
import { spawnSync } from 'child_process';
|
|
45
|
+
|
|
46
|
+
const MAX_RETRIES = Number(process.env.NEX_WATCH_MAX_RETRIES) > 0
|
|
47
|
+
? Math.floor(Number(process.env.NEX_WATCH_MAX_RETRIES))
|
|
48
|
+
: 5;
|
|
49
|
+
const NEX_BIN = process.env.NEX_WATCH_BIN || 'nex';
|
|
50
|
+
|
|
51
|
+
const isTTY = process.stderr.isTTY === true;
|
|
52
|
+
const c = isTTY
|
|
53
|
+
? { dim: '\x1b[2m', green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', reset: '\x1b[0m' }
|
|
54
|
+
: { dim: '', green: '', yellow: '', red: '', reset: '' };
|
|
55
|
+
|
|
56
|
+
const info = (msg) => process.stderr.write(`${c.dim}[nex-watch]${c.reset} ${msg}\n`);
|
|
57
|
+
const warn = (msg) => process.stderr.write(`${c.yellow}[nex-watch]${c.reset} ${msg}\n`);
|
|
58
|
+
const ok = (msg) => process.stderr.write(`${c.green}[nex-watch]${c.reset} ${msg}\n`);
|
|
59
|
+
const fatal = (msg) => { process.stderr.write(`${c.red}[nex-watch]${c.reset} ${msg}\n`); process.exit(1); };
|
|
60
|
+
|
|
61
|
+
// Kill-type signals worth auto-relaunching on. SIGKILL is the un-catchable
|
|
62
|
+
// OOM/Jetsam case this tool exists for; SIGTERM is included because some
|
|
63
|
+
// low-memory managers (and `docker stop`) send it first before escalating —
|
|
64
|
+
// treating it the same way costs nothing extra since nex has no in-flight
|
|
65
|
+
// state to lose beyond what's already persisted.
|
|
66
|
+
const KILL_SIGNALS = new Set(['SIGKILL', 'SIGTERM']);
|
|
67
|
+
|
|
68
|
+
const userArgs = process.argv.slice(2);
|
|
69
|
+
|
|
70
|
+
let attempt = 0;
|
|
71
|
+
for (;;) {
|
|
72
|
+
const args = attempt === 0 ? userArgs : ['--resume', 'last', ...userArgs];
|
|
73
|
+
if (attempt === 0) {
|
|
74
|
+
info(`starting: ${NEX_BIN} ${args.join(' ')}`.trim());
|
|
75
|
+
} else {
|
|
76
|
+
info(`relaunch #${attempt}: ${NEX_BIN} ${args.join(' ')}`.trim());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = spawnSync(NEX_BIN, args, { stdio: 'inherit' });
|
|
80
|
+
|
|
81
|
+
if (result.error) {
|
|
82
|
+
// e.g. ENOENT — nex isn't on PATH. Not a kill scenario; fail immediately
|
|
83
|
+
// with a clear message rather than retrying something that can't work.
|
|
84
|
+
fatal(`failed to launch '${NEX_BIN}': ${result.error.message}. Install it first: npm install -g nexrall-code`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (result.signal && KILL_SIGNALS.has(result.signal)) {
|
|
88
|
+
attempt += 1;
|
|
89
|
+
if (attempt > MAX_RETRIES) {
|
|
90
|
+
fatal(
|
|
91
|
+
`nex was killed (${result.signal}) ${MAX_RETRIES} times in a row — giving up. ` +
|
|
92
|
+
`This usually means the machine is critically low on memory; close other applications ` +
|
|
93
|
+
`before retrying. Progress is saved — run 'nex --resume last' manually once memory is free.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
warn(
|
|
97
|
+
`nex was killed (${result.signal}) — this is the OS's low-memory killer, not a nex bug. ` +
|
|
98
|
+
`Progress up to the last saved turn is safe. Retrying (${attempt}/${MAX_RETRIES}) in 2s...`
|
|
99
|
+
);
|
|
100
|
+
// Synchronous sleep via spawnSync on a no-op — avoids pulling in an async
|
|
101
|
+
// main() just for one delay, keeping this script a simple top-level loop.
|
|
102
|
+
spawnSync(process.platform === 'win32' ? 'timeout' : 'sleep', [process.platform === 'win32' ? '/t 2' : '2'], { stdio: 'ignore', shell: process.platform === 'win32' });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Any other outcome — normal exit, a real error, or a signal nex already
|
|
107
|
+
// handles gracefully (e.g. SIGINT from Ctrl+C) — stop here and mirror the
|
|
108
|
+
// child's result so scripts calling nex-watch see the same status nex itself
|
|
109
|
+
// would have produced.
|
|
110
|
+
if (result.status === 0) ok('nex exited normally.');
|
|
111
|
+
if (result.signal) {
|
|
112
|
+
// A non-kill signal (rare) — exit with the conventional 128+n code.
|
|
113
|
+
const SIGNUMS = { SIGINT: 2, SIGHUP: 1, SIGQUIT: 3 };
|
|
114
|
+
process.exit(128 + (SIGNUMS[result.signal] || 0));
|
|
115
|
+
}
|
|
116
|
+
process.exit(result.status ?? 1);
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexrall-code",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.77",
|
|
4
4
|
"description": "Nexrall Code — AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -20,13 +20,15 @@
|
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"type": "module",
|
|
22
22
|
"bin": {
|
|
23
|
-
"nex": "dist/index.js"
|
|
23
|
+
"nex": "dist/index.js",
|
|
24
|
+
"nex-watch": "nex-watch.js"
|
|
24
25
|
},
|
|
25
26
|
"main": "dist/index.js",
|
|
26
27
|
"files": [
|
|
27
28
|
"dist",
|
|
28
29
|
"install.sh",
|
|
29
|
-
"install.ps1"
|
|
30
|
+
"install.ps1",
|
|
31
|
+
"nex-watch.js"
|
|
30
32
|
],
|
|
31
33
|
"engines": {
|
|
32
34
|
"node": ">=18"
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
"release": "node build.js && node scripts/upload-release.cjs"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
42
|
-
"@nexrall/code-core": "^1.4.
|
|
44
|
+
"@nexrall/code-core": "^1.4.45",
|
|
43
45
|
"chalk": "^5.3.0",
|
|
44
46
|
"commander": "^12.0.0",
|
|
45
47
|
"diff": "^5.2.0",
|