@mrciphersmith/keryx 0.2.107 → 0.2.108
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/cli.js +308 -65
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -48579,7 +48579,7 @@ import path47 from "path";
|
|
|
48579
48579
|
// package.json
|
|
48580
48580
|
var package_default = {
|
|
48581
48581
|
name: "@mrciphersmith/keryx",
|
|
48582
|
-
version: "0.2.
|
|
48582
|
+
version: "0.2.108",
|
|
48583
48583
|
description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
48584
48584
|
private: false,
|
|
48585
48585
|
publishConfig: {
|
|
@@ -80462,6 +80462,36 @@ var MAX_BACKGROUND_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
|
80462
80462
|
var DEFAULT_INITIAL_BUFFER_MS = 500;
|
|
80463
80463
|
var MAX_TRACKED_JOBS = 50;
|
|
80464
80464
|
var TERMINATED_OUTPUT_TAIL_BYTES = 4000;
|
|
80465
|
+
var TASK_OUTPUT_HEAD_BYTES = 24000;
|
|
80466
|
+
var ENV_SHELL_IDLE_MS = "KERYX_SHELL_IDLE_MS";
|
|
80467
|
+
var ENV_SHELL_TIMEOUT_MS_FALLBACK = "KERYX_SHELL_TIMEOUT_MS";
|
|
80468
|
+
var DEFAULT_SHELL_IDLE_MS = 120000;
|
|
80469
|
+
var MIN_TASK_IDLE_TIMEOUT_MS = 1000;
|
|
80470
|
+
var MAX_TASK_IDLE_TIMEOUT_MS = 1800000;
|
|
80471
|
+
function parseEnvMs(raw) {
|
|
80472
|
+
if (raw === undefined || raw.trim().length === 0)
|
|
80473
|
+
return;
|
|
80474
|
+
const n = Number.parseInt(raw.trim(), 10);
|
|
80475
|
+
if (!Number.isFinite(n) || n < 0)
|
|
80476
|
+
return Number.NaN;
|
|
80477
|
+
return n;
|
|
80478
|
+
}
|
|
80479
|
+
function resolveShellIdleMs(env = process.env) {
|
|
80480
|
+
const idle = parseEnvMs(env[ENV_SHELL_IDLE_MS]);
|
|
80481
|
+
if (idle !== undefined) {
|
|
80482
|
+
return Number.isNaN(idle) ? DEFAULT_SHELL_IDLE_MS : idle;
|
|
80483
|
+
}
|
|
80484
|
+
const legacy = parseEnvMs(env[ENV_SHELL_TIMEOUT_MS_FALLBACK]);
|
|
80485
|
+
if (legacy !== undefined) {
|
|
80486
|
+
return Number.isNaN(legacy) ? DEFAULT_SHELL_IDLE_MS : legacy;
|
|
80487
|
+
}
|
|
80488
|
+
return DEFAULT_SHELL_IDLE_MS;
|
|
80489
|
+
}
|
|
80490
|
+
function clampTaskIdleTimeoutMs(ms) {
|
|
80491
|
+
if (!Number.isFinite(ms))
|
|
80492
|
+
return MIN_TASK_IDLE_TIMEOUT_MS;
|
|
80493
|
+
return Math.min(MAX_TASK_IDLE_TIMEOUT_MS, Math.max(MIN_TASK_IDLE_TIMEOUT_MS, Math.trunc(ms)));
|
|
80494
|
+
}
|
|
80465
80495
|
function sleep(ms) {
|
|
80466
80496
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
80467
80497
|
}
|
|
@@ -80539,11 +80569,34 @@ function createJobRegistry(options) {
|
|
|
80539
80569
|
const initialBufferMs = options?.initialBufferMs ?? DEFAULT_INITIAL_BUFFER_MS;
|
|
80540
80570
|
const onEvent = options?.onEvent;
|
|
80541
80571
|
const maxTrackedJobs = options?.maxTrackedJobs ?? MAX_TRACKED_JOBS;
|
|
80572
|
+
const idleMs = options?.idleMs ?? resolveShellIdleMs();
|
|
80542
80573
|
let nextId = 0;
|
|
80543
80574
|
const jobs = new Map;
|
|
80544
80575
|
function runningJobs() {
|
|
80545
80576
|
return [...jobs.values()].filter((j) => j.info.status === "running");
|
|
80546
80577
|
}
|
|
80578
|
+
function backgroundRunningJobs() {
|
|
80579
|
+
return runningJobs().filter((j) => j.info.phase === "background");
|
|
80580
|
+
}
|
|
80581
|
+
function clearIdleTimer(job) {
|
|
80582
|
+
if (job.idleTimer === undefined)
|
|
80583
|
+
return;
|
|
80584
|
+
clearTimeout(job.idleTimer);
|
|
80585
|
+
job.idleTimer = undefined;
|
|
80586
|
+
}
|
|
80587
|
+
function armIdleTimer(job) {
|
|
80588
|
+
clearIdleTimer(job);
|
|
80589
|
+
if (job.info.status !== "running")
|
|
80590
|
+
return;
|
|
80591
|
+
const timeout = job.info.idleTimeoutMs;
|
|
80592
|
+
if (timeout <= 0)
|
|
80593
|
+
return;
|
|
80594
|
+
const timer = setTimeout(() => {
|
|
80595
|
+
requestKill(job, "idle");
|
|
80596
|
+
}, timeout);
|
|
80597
|
+
timer.unref?.();
|
|
80598
|
+
job.idleTimer = timer;
|
|
80599
|
+
}
|
|
80547
80600
|
function evictOldestTerminatedIfOverCap() {
|
|
80548
80601
|
if (jobs.size <= maxTrackedJobs)
|
|
80549
80602
|
return;
|
|
@@ -80564,20 +80617,32 @@ function createJobRegistry(options) {
|
|
|
80564
80617
|
}
|
|
80565
80618
|
function appendOutput(job, chunk, stream) {
|
|
80566
80619
|
job.outputBuffer += chunk;
|
|
80620
|
+
if (job.outputHead.length < TASK_OUTPUT_HEAD_BYTES) {
|
|
80621
|
+
job.outputHead = (job.outputHead + chunk).slice(0, TASK_OUTPUT_HEAD_BYTES);
|
|
80622
|
+
job.info.outputHead = job.outputHead;
|
|
80623
|
+
}
|
|
80624
|
+
armIdleTimer(job);
|
|
80567
80625
|
onEvent?.({ type: "output", jobId: job.info.jobId, chunk, stream });
|
|
80568
80626
|
if (job.outputBuffer.length > MAX_BACKGROUND_OUTPUT_BYTES) {
|
|
80569
80627
|
const dropped = job.outputBuffer.length - MAX_BACKGROUND_OUTPUT_BYTES;
|
|
80570
80628
|
job.outputBuffer = job.outputBuffer.slice(dropped);
|
|
80571
80629
|
job.readCursor = Math.max(0, job.readCursor - dropped);
|
|
80572
|
-
|
|
80573
|
-
job.killRequested = true;
|
|
80574
|
-
terminateJob(job, killGraceMs);
|
|
80575
|
-
}
|
|
80630
|
+
requestKill(job, "output-cap");
|
|
80576
80631
|
}
|
|
80577
80632
|
}
|
|
80633
|
+
function requestKill(job, reason) {
|
|
80634
|
+
if (job.info.status !== "running")
|
|
80635
|
+
return Promise.resolve();
|
|
80636
|
+
if (job.killRequested)
|
|
80637
|
+
return job.exitedPromise;
|
|
80638
|
+
job.killRequested = true;
|
|
80639
|
+
job.pendingKillReason = reason;
|
|
80640
|
+
return terminateJob(job, killGraceMs);
|
|
80641
|
+
}
|
|
80578
80642
|
async function terminateJob(job, graceMs) {
|
|
80579
80643
|
if (job.info.status !== "running")
|
|
80580
80644
|
return;
|
|
80645
|
+
clearIdleTimer(job);
|
|
80581
80646
|
job.killRequested = true;
|
|
80582
80647
|
job.handle.kill("SIGTERM");
|
|
80583
80648
|
const exited = await Promise.race([job.exitedPromise.then(() => true), sleep(graceMs).then(() => false)]);
|
|
@@ -80586,14 +80651,17 @@ function createJobRegistry(options) {
|
|
|
80586
80651
|
}
|
|
80587
80652
|
}
|
|
80588
80653
|
return {
|
|
80589
|
-
async start(command) {
|
|
80590
|
-
const
|
|
80591
|
-
if (
|
|
80592
|
-
const
|
|
80593
|
-
|
|
80594
|
-
|
|
80595
|
-
|
|
80596
|
-
|
|
80654
|
+
async start(command, opts) {
|
|
80655
|
+
const phase = opts?.phase ?? "background";
|
|
80656
|
+
if (phase === "background") {
|
|
80657
|
+
const running = backgroundRunningJobs();
|
|
80658
|
+
if (running.length >= maxConcurrent) {
|
|
80659
|
+
const names = running.map((j) => j.info.command).join(", ");
|
|
80660
|
+
return {
|
|
80661
|
+
ok: false,
|
|
80662
|
+
error: `background task limit reached (${maxConcurrent} running: ${names}); wait for one to finish or kill it with shell_job_kill first`
|
|
80663
|
+
};
|
|
80664
|
+
}
|
|
80597
80665
|
}
|
|
80598
80666
|
let handle;
|
|
80599
80667
|
try {
|
|
@@ -80604,7 +80672,7 @@ function createJobRegistry(options) {
|
|
|
80604
80672
|
error: `command failed to start: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
80605
80673
|
};
|
|
80606
80674
|
}
|
|
80607
|
-
const jobId = `
|
|
80675
|
+
const jobId = `task-${++nextId}-${handle.pid}`;
|
|
80608
80676
|
let resolveExited;
|
|
80609
80677
|
const exitedPromise = new Promise((resolve) => {
|
|
80610
80678
|
resolveExited = resolve;
|
|
@@ -80615,26 +80683,46 @@ function createJobRegistry(options) {
|
|
|
80615
80683
|
pid: handle.pid,
|
|
80616
80684
|
command,
|
|
80617
80685
|
status: "running",
|
|
80686
|
+
phase,
|
|
80687
|
+
idleTimeoutMs: opts?.idleTimeoutMs ?? idleMs,
|
|
80618
80688
|
startedAt: nowIso3()
|
|
80619
80689
|
},
|
|
80620
80690
|
handle,
|
|
80621
80691
|
outputBuffer: "",
|
|
80692
|
+
outputHead: "",
|
|
80622
80693
|
readCursor: 0,
|
|
80623
80694
|
exited: false,
|
|
80624
80695
|
killRequested: false,
|
|
80696
|
+
phaseEmitted: false,
|
|
80625
80697
|
resolveExited,
|
|
80626
80698
|
exitedPromise
|
|
80627
80699
|
};
|
|
80700
|
+
if (opts?.description !== undefined)
|
|
80701
|
+
job.info.description = opts.description;
|
|
80628
80702
|
jobs.set(jobId, job);
|
|
80629
80703
|
evictOldestTerminatedIfOverCap();
|
|
80630
|
-
onEvent?.({
|
|
80704
|
+
onEvent?.({
|
|
80705
|
+
type: "start",
|
|
80706
|
+
jobId,
|
|
80707
|
+
pid: handle.pid,
|
|
80708
|
+
command,
|
|
80709
|
+
startedAt: job.info.startedAt,
|
|
80710
|
+
...opts?.description !== undefined ? { description: opts.description } : {}
|
|
80711
|
+
});
|
|
80712
|
+
if (phase === "background") {
|
|
80713
|
+
job.phaseEmitted = true;
|
|
80714
|
+
onEvent?.({ type: "phase", jobId, phase: "background" });
|
|
80715
|
+
}
|
|
80631
80716
|
handle.onOutput((chunk, stream) => appendOutput(job, chunk, stream));
|
|
80632
80717
|
handle.onExit((info) => {
|
|
80633
80718
|
if (job.exited)
|
|
80634
80719
|
return;
|
|
80635
80720
|
job.exited = true;
|
|
80636
|
-
|
|
80637
|
-
|
|
80721
|
+
clearIdleTimer(job);
|
|
80722
|
+
const terminal = job.killRequested ? "killed" : info.exitCode === 0 ? "completed" : "failed";
|
|
80723
|
+
job.info.status = terminal;
|
|
80724
|
+
if (job.killRequested) {
|
|
80725
|
+
job.info.killReason = job.pendingKillReason ?? "model";
|
|
80638
80726
|
}
|
|
80639
80727
|
job.info.exitCode = info.exitCode;
|
|
80640
80728
|
job.info.endedAt = nowIso3();
|
|
@@ -80642,13 +80730,15 @@ function createJobRegistry(options) {
|
|
|
80642
80730
|
onEvent?.({
|
|
80643
80731
|
type: "exit",
|
|
80644
80732
|
jobId,
|
|
80645
|
-
status:
|
|
80733
|
+
status: terminal,
|
|
80734
|
+
...job.info.killReason !== undefined ? { killReason: job.info.killReason } : {},
|
|
80646
80735
|
exitCode: info.exitCode,
|
|
80647
80736
|
endedAt: job.info.endedAt
|
|
80648
80737
|
});
|
|
80649
80738
|
shrinkTerminatedOutput(job);
|
|
80650
80739
|
});
|
|
80651
|
-
|
|
80740
|
+
armIdleTimer(job);
|
|
80741
|
+
if (phase === "background" && initialBufferMs > 0) {
|
|
80652
80742
|
await sleep(initialBufferMs);
|
|
80653
80743
|
}
|
|
80654
80744
|
const output = job.outputBuffer.slice(job.readCursor);
|
|
@@ -80670,7 +80760,41 @@ function createJobRegistry(options) {
|
|
|
80670
80760
|
job.readCursor = job.outputBuffer.length;
|
|
80671
80761
|
return { ok: true, output };
|
|
80672
80762
|
},
|
|
80673
|
-
async
|
|
80763
|
+
async waitForExit(jobId, ms) {
|
|
80764
|
+
const job = jobs.get(jobId);
|
|
80765
|
+
if (job === undefined)
|
|
80766
|
+
return "unknown";
|
|
80767
|
+
if (job.exited || job.info.status !== "running")
|
|
80768
|
+
return "exited";
|
|
80769
|
+
let timer;
|
|
80770
|
+
const timedOut = await Promise.race([
|
|
80771
|
+
job.exitedPromise.then(() => false),
|
|
80772
|
+
new Promise((resolve) => {
|
|
80773
|
+
timer = setTimeout(() => resolve(true), ms);
|
|
80774
|
+
timer.unref?.();
|
|
80775
|
+
})
|
|
80776
|
+
]);
|
|
80777
|
+
if (timer !== undefined)
|
|
80778
|
+
clearTimeout(timer);
|
|
80779
|
+
return timedOut ? "timeout" : "exited";
|
|
80780
|
+
},
|
|
80781
|
+
promote(jobId) {
|
|
80782
|
+
const job = jobs.get(jobId);
|
|
80783
|
+
if (job === undefined) {
|
|
80784
|
+
return { ok: false, error: `unknown job_id: ${jobId}` };
|
|
80785
|
+
}
|
|
80786
|
+
job.info.phase = "background";
|
|
80787
|
+
if (!job.phaseEmitted) {
|
|
80788
|
+
job.phaseEmitted = true;
|
|
80789
|
+
onEvent?.({ type: "phase", jobId, phase: "background" });
|
|
80790
|
+
}
|
|
80791
|
+
const others = backgroundRunningJobs().filter((j) => j.info.jobId !== jobId);
|
|
80792
|
+
if (job.info.status === "running" && others.length >= maxConcurrent) {
|
|
80793
|
+
return { ok: true, overCap: others.map((j) => j.info.command).join(", ") };
|
|
80794
|
+
}
|
|
80795
|
+
return { ok: true };
|
|
80796
|
+
},
|
|
80797
|
+
async kill(jobId, reason = "model") {
|
|
80674
80798
|
const job = jobs.get(jobId);
|
|
80675
80799
|
if (job === undefined) {
|
|
80676
80800
|
return { ok: false, error: `unknown job_id: ${jobId}` };
|
|
@@ -80678,11 +80802,11 @@ function createJobRegistry(options) {
|
|
|
80678
80802
|
if (job.info.status !== "running") {
|
|
80679
80803
|
return { ok: false, error: `job ${jobId} is not running (status: ${job.info.status})` };
|
|
80680
80804
|
}
|
|
80681
|
-
await
|
|
80805
|
+
await requestKill(job, reason);
|
|
80682
80806
|
return { ok: true };
|
|
80683
80807
|
},
|
|
80684
80808
|
async sweepAll() {
|
|
80685
|
-
await Promise.all(runningJobs().map((job) =>
|
|
80809
|
+
await Promise.all(runningJobs().map((job) => requestKill(job, "session-exit")));
|
|
80686
80810
|
}
|
|
80687
80811
|
};
|
|
80688
80812
|
}
|
|
@@ -80690,7 +80814,7 @@ function shellJobOutputTool(registry) {
|
|
|
80690
80814
|
return {
|
|
80691
80815
|
definition: {
|
|
80692
80816
|
name: "shell_job_output",
|
|
80693
|
-
description: "Return output produced by a
|
|
80817
|
+
description: "Return output produced by a shell task SINCE the previous call for that id \u2014 never the full transcript " + "again. Input: { job_id: string } \u2014 pass the task_id shell_exec returned for a command that outlived its " + "yield (or a background:true job). Poll this instead of re-running shell_exec to check on a long command.",
|
|
80694
80818
|
inputSchema: {
|
|
80695
80819
|
type: "object",
|
|
80696
80820
|
properties: { job_id: { type: "string" } },
|
|
@@ -80716,7 +80840,7 @@ function shellJobKillTool(registry) {
|
|
|
80716
80840
|
return {
|
|
80717
80841
|
definition: {
|
|
80718
80842
|
name: "shell_job_kill",
|
|
80719
|
-
description: "
|
|
80843
|
+
description: "Stop a running shell task \u2014 its entire process group, including any descendant it backgrounded. Input: " + "{ job_id: string } \u2014 the task_id shell_exec returned for a command still running after its yield (or a " + "background:true job). Only tasks in this session's own registry can be targeted.",
|
|
80720
80844
|
inputSchema: {
|
|
80721
80845
|
type: "object",
|
|
80722
80846
|
properties: { job_id: { type: "string" } },
|
|
@@ -80921,6 +81045,37 @@ function resolveShellTimeoutMs(env = process.env) {
|
|
|
80921
81045
|
}
|
|
80922
81046
|
return n;
|
|
80923
81047
|
}
|
|
81048
|
+
var DEFAULT_SHELL_YIELD_MS = 1e4;
|
|
81049
|
+
var ENV_SHELL_YIELD_MS = "KERYX_SHELL_YIELD_MS";
|
|
81050
|
+
function resolveShellYieldMs(env = process.env) {
|
|
81051
|
+
const raw = env[ENV_SHELL_YIELD_MS];
|
|
81052
|
+
if (raw === undefined || raw.trim().length === 0) {
|
|
81053
|
+
return DEFAULT_SHELL_YIELD_MS;
|
|
81054
|
+
}
|
|
81055
|
+
const n = Number.parseInt(raw.trim(), 10);
|
|
81056
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
81057
|
+
return DEFAULT_SHELL_YIELD_MS;
|
|
81058
|
+
}
|
|
81059
|
+
return n;
|
|
81060
|
+
}
|
|
81061
|
+
function boundOutput2(text) {
|
|
81062
|
+
return text.length > MAX_OUTPUT_BYTES3 ? `${text.slice(0, MAX_OUTPUT_BYTES3)}
|
|
81063
|
+
\u2026(truncated)` : text;
|
|
81064
|
+
}
|
|
81065
|
+
function taskHandleResult(args) {
|
|
81066
|
+
return {
|
|
81067
|
+
output: JSON.stringify({
|
|
81068
|
+
task_id: args.taskId,
|
|
81069
|
+
job_id: args.taskId,
|
|
81070
|
+
pid: args.pid,
|
|
81071
|
+
status: "running",
|
|
81072
|
+
output: boundOutput2(args.output),
|
|
81073
|
+
notice: args.notice,
|
|
81074
|
+
...args.overCap !== undefined ? { over_cap: args.overCap } : {}
|
|
81075
|
+
}),
|
|
81076
|
+
isError: false
|
|
81077
|
+
};
|
|
81078
|
+
}
|
|
80924
81079
|
function makeCommandRunner(root) {
|
|
80925
81080
|
return async (command) => {
|
|
80926
81081
|
let netClose = async () => {};
|
|
@@ -81014,14 +81169,19 @@ ${notice}` : notice,
|
|
|
81014
81169
|
}
|
|
81015
81170
|
};
|
|
81016
81171
|
}
|
|
81017
|
-
function shellExecTool(root, run = makeCommandRunner(root), jobRegistry) {
|
|
81172
|
+
function shellExecTool(root, run = makeCommandRunner(root), jobRegistry, options) {
|
|
81018
81173
|
return {
|
|
81019
81174
|
definition: {
|
|
81020
81175
|
name: "shell_exec",
|
|
81021
|
-
description: "Run a shell command in the project root (e.g. `git status`, `bun test`). Requires the user's approval " + "before it runs. Input: { command: string, background?: boolean }.
|
|
81176
|
+
description: "Run a shell command in the project root (e.g. `git status`, `bun test`). Requires the user's approval " + "before it runs. Input: { command: string, background?: boolean, description?: string, " + "idle_timeout_ms?: integer }. EVERY command returns within a bounded wait \u2014 a short one returns its " + "output directly, and one still running when the wait elapses keeps running as a task: you get " + "{task_id, pid, status, output} instead of its final output, and read the rest with " + "shell_job_output(task_id) and stop it with shell_job_kill(task_id). So a slow command never freezes " + "the session and never needs to be guessed at in advance. background:true only SKIPS the wait (use it " + "for a dev server or a watch build you know you will not read inline); it changes nothing else. " + "description is a short label for the task list. idle_timeout_ms is for a command that is deliberately " + "SILENT for a long time (a sleep, a slow poll) \u2014 a task that produces no output for its idle timeout " + "is killed, so raise it rather than lose the command. Combined stdout+stderr is CAPPED at 20,000 bytes " + "from the start of output \u2014 do not use sed/grep/cat/awk here to locate code (they can silently " + "truncate before reaching what you need, and repeating the command returns the same truncated head); " + "use search_code or graph_symbol instead, which are built for that and stay within the cap.",
|
|
81022
81177
|
inputSchema: {
|
|
81023
81178
|
type: "object",
|
|
81024
|
-
properties: {
|
|
81179
|
+
properties: {
|
|
81180
|
+
command: { type: "string" },
|
|
81181
|
+
background: { type: "boolean" },
|
|
81182
|
+
description: { type: "string" },
|
|
81183
|
+
idle_timeout_ms: { type: "integer" }
|
|
81184
|
+
},
|
|
81025
81185
|
required: ["command"],
|
|
81026
81186
|
additionalProperties: false
|
|
81027
81187
|
},
|
|
@@ -81032,20 +81192,66 @@ function shellExecTool(root, run = makeCommandRunner(root), jobRegistry) {
|
|
|
81032
81192
|
if (command.length === 0) {
|
|
81033
81193
|
return { output: "shell_exec requires a non-empty 'command'", isError: true };
|
|
81034
81194
|
}
|
|
81035
|
-
|
|
81036
|
-
|
|
81195
|
+
const background = input.background === true;
|
|
81196
|
+
if (jobRegistry === undefined) {
|
|
81197
|
+
if (background) {
|
|
81037
81198
|
return { output: "shell_exec: background jobs are not available in this session", isError: true };
|
|
81038
81199
|
}
|
|
81039
|
-
|
|
81040
|
-
|
|
81041
|
-
|
|
81200
|
+
return run(command);
|
|
81201
|
+
}
|
|
81202
|
+
const startOpts = { phase: background ? "background" : "foreground" };
|
|
81203
|
+
if (typeof input.description === "string" && input.description.length > 0) {
|
|
81204
|
+
startOpts.description = input.description;
|
|
81205
|
+
}
|
|
81206
|
+
if (typeof input.idle_timeout_ms === "number") {
|
|
81207
|
+
startOpts.idleTimeoutMs = clampTaskIdleTimeoutMs(input.idle_timeout_ms);
|
|
81208
|
+
}
|
|
81209
|
+
const started = await jobRegistry.start(command, startOpts);
|
|
81210
|
+
if (!started.ok) {
|
|
81211
|
+
return { output: started.error, isError: true };
|
|
81212
|
+
}
|
|
81213
|
+
const taskId = started.jobId;
|
|
81214
|
+
if (background) {
|
|
81215
|
+
return taskHandleResult({
|
|
81216
|
+
taskId,
|
|
81217
|
+
pid: started.pid,
|
|
81218
|
+
output: started.output,
|
|
81219
|
+
notice: `running as a background task; read new output with shell_job_output("${taskId}") and stop it with shell_job_kill("${taskId}")`
|
|
81220
|
+
});
|
|
81221
|
+
}
|
|
81222
|
+
const yieldMs = options?.yieldMs ?? resolveShellYieldMs();
|
|
81223
|
+
const outcome = await jobRegistry.waitForExit(taskId, yieldMs);
|
|
81224
|
+
let collected = started.output;
|
|
81225
|
+
const pending = jobRegistry.readOutput(taskId);
|
|
81226
|
+
if (pending.ok)
|
|
81227
|
+
collected += pending.output;
|
|
81228
|
+
if (outcome === "timeout") {
|
|
81229
|
+
const promoted = jobRegistry.promote(taskId);
|
|
81230
|
+
if (!promoted.ok) {
|
|
81231
|
+
return { output: promoted.error, isError: true };
|
|
81042
81232
|
}
|
|
81043
|
-
return {
|
|
81044
|
-
|
|
81045
|
-
|
|
81046
|
-
|
|
81233
|
+
return taskHandleResult({
|
|
81234
|
+
taskId,
|
|
81235
|
+
pid: started.pid,
|
|
81236
|
+
output: collected,
|
|
81237
|
+
notice: `still running after ${yieldMs}ms, so it is now a background task; read new output with shell_job_output("${taskId}") and stop it with shell_job_kill("${taskId}")`,
|
|
81238
|
+
...promoted.overCap !== undefined ? { overCap: promoted.overCap } : {}
|
|
81239
|
+
});
|
|
81240
|
+
}
|
|
81241
|
+
const info = jobRegistry.get(taskId);
|
|
81242
|
+
if (outcome === "unknown" || info === undefined) {
|
|
81243
|
+
const so_far = boundOutput2(collected.trim());
|
|
81244
|
+
const lost = `shell_exec: task ${taskId} is no longer tracked, so its exit status is unknown`;
|
|
81245
|
+
return { output: so_far.length > 0 ? `${so_far}
|
|
81246
|
+
${lost}` : lost, isError: true };
|
|
81047
81247
|
}
|
|
81048
|
-
|
|
81248
|
+
const exitCode = info.exitCode ?? 0;
|
|
81249
|
+
const bounded = boundOutput2((info.outputHead ?? collected).trim());
|
|
81250
|
+
const body = bounded.length > 0 ? bounded : `(no output; exit ${exitCode})`;
|
|
81251
|
+
const idleKilled = info.status === "killed" && info.killReason === "idle";
|
|
81252
|
+
const output = idleKilled ? `${body}
|
|
81253
|
+
shell_exec: no output for ${info.idleTimeoutMs}ms, so the command was killed (raise it for this command with idle_timeout_ms, or change the default with ${ENV_SHELL_IDLE_MS})` : body;
|
|
81254
|
+
return { output, isError: info.status !== "completed" };
|
|
81049
81255
|
}
|
|
81050
81256
|
};
|
|
81051
81257
|
}
|
|
@@ -94455,9 +94661,14 @@ function openExternalInspector(otui, chrome, options) {
|
|
|
94455
94661
|
var MAX_BACKGROUND_JOB_OUTPUT_CHARS = 20000;
|
|
94456
94662
|
var STATUS_GLYPH4 = {
|
|
94457
94663
|
running: "\u25D0",
|
|
94458
|
-
|
|
94664
|
+
completed: "\u25CF",
|
|
94665
|
+
failed: "\u25B2",
|
|
94459
94666
|
killed: "\u2717"
|
|
94460
94667
|
};
|
|
94668
|
+
function appendBounded(output, chunk) {
|
|
94669
|
+
const combined = output + chunk;
|
|
94670
|
+
return combined.length > MAX_BACKGROUND_JOB_OUTPUT_CHARS ? combined.slice(-MAX_BACKGROUND_JOB_OUTPUT_CHARS) : combined;
|
|
94671
|
+
}
|
|
94461
94672
|
function clip7(s, max) {
|
|
94462
94673
|
if (max <= 0) {
|
|
94463
94674
|
return "";
|
|
@@ -94486,6 +94697,9 @@ function formatJobMeta(entry, now = Date.now()) {
|
|
|
94486
94697
|
["Ended", entry.endedAt ?? "\u2014"],
|
|
94487
94698
|
["Exit code", entry.exitCode !== undefined ? String(entry.exitCode) : "\u2014"]
|
|
94488
94699
|
];
|
|
94700
|
+
if (entry.killReason !== undefined) {
|
|
94701
|
+
rows.push(["Kill reason", entry.killReason]);
|
|
94702
|
+
}
|
|
94489
94703
|
const width = rows.reduce((max, [label]) => Math.max(max, label.length), 0);
|
|
94490
94704
|
return rows.map(([label, value]) => `${label.padEnd(width)} ${value}`).join(`
|
|
94491
94705
|
`);
|
|
@@ -94496,36 +94710,64 @@ function formatJobOutput(entry) {
|
|
|
94496
94710
|
|
|
94497
94711
|
class BackgroundJobStore {
|
|
94498
94712
|
jobs = new Map;
|
|
94713
|
+
pending = new Map;
|
|
94499
94714
|
listeners = new Set;
|
|
94500
94715
|
apply(event) {
|
|
94501
|
-
|
|
94502
|
-
|
|
94503
|
-
|
|
94504
|
-
|
|
94505
|
-
|
|
94506
|
-
|
|
94507
|
-
|
|
94508
|
-
|
|
94509
|
-
|
|
94510
|
-
|
|
94511
|
-
|
|
94512
|
-
|
|
94513
|
-
|
|
94514
|
-
|
|
94515
|
-
|
|
94516
|
-
|
|
94517
|
-
|
|
94518
|
-
|
|
94519
|
-
|
|
94520
|
-
|
|
94521
|
-
|
|
94522
|
-
|
|
94523
|
-
|
|
94524
|
-
|
|
94525
|
-
|
|
94526
|
-
|
|
94716
|
+
switch (event.type) {
|
|
94717
|
+
case "start":
|
|
94718
|
+
this.pending.set(event.jobId, {
|
|
94719
|
+
jobId: event.jobId,
|
|
94720
|
+
command: event.command,
|
|
94721
|
+
pid: event.pid,
|
|
94722
|
+
status: "running",
|
|
94723
|
+
startedAt: event.startedAt,
|
|
94724
|
+
output: ""
|
|
94725
|
+
});
|
|
94726
|
+
return;
|
|
94727
|
+
case "phase": {
|
|
94728
|
+
const entry = this.pending.get(event.jobId);
|
|
94729
|
+
if (entry === undefined) {
|
|
94730
|
+
return;
|
|
94731
|
+
}
|
|
94732
|
+
this.pending.delete(event.jobId);
|
|
94733
|
+
this.jobs.set(event.jobId, entry);
|
|
94734
|
+
this.emit({ id: event.jobId, kind: "start" });
|
|
94735
|
+
return;
|
|
94736
|
+
}
|
|
94737
|
+
case "output": {
|
|
94738
|
+
const hidden = this.pending.get(event.jobId);
|
|
94739
|
+
if (hidden !== undefined) {
|
|
94740
|
+
hidden.output = appendBounded(hidden.output, event.chunk);
|
|
94741
|
+
return;
|
|
94742
|
+
}
|
|
94743
|
+
const current = this.jobs.get(event.jobId);
|
|
94744
|
+
if (current === undefined) {
|
|
94745
|
+
return;
|
|
94746
|
+
}
|
|
94747
|
+
current.output = appendBounded(current.output, event.chunk);
|
|
94748
|
+
this.emit({ id: event.jobId, kind: "output" });
|
|
94749
|
+
return;
|
|
94750
|
+
}
|
|
94751
|
+
case "exit": {
|
|
94752
|
+
if (this.pending.delete(event.jobId)) {
|
|
94753
|
+
return;
|
|
94754
|
+
}
|
|
94755
|
+
const current = this.jobs.get(event.jobId);
|
|
94756
|
+
if (current === undefined) {
|
|
94757
|
+
return;
|
|
94758
|
+
}
|
|
94759
|
+
current.status = event.status;
|
|
94760
|
+
current.endedAt = event.endedAt;
|
|
94761
|
+
if (event.exitCode !== undefined) {
|
|
94762
|
+
current.exitCode = event.exitCode;
|
|
94763
|
+
}
|
|
94764
|
+
if (event.killReason !== undefined) {
|
|
94765
|
+
current.killReason = event.killReason;
|
|
94766
|
+
}
|
|
94767
|
+
this.emit({ id: event.jobId, kind: "exit" });
|
|
94768
|
+
return;
|
|
94769
|
+
}
|
|
94527
94770
|
}
|
|
94528
|
-
this.emit({ id: event.jobId, kind: "exit" });
|
|
94529
94771
|
}
|
|
94530
94772
|
get(jobId) {
|
|
94531
94773
|
return this.jobs.get(jobId);
|
|
@@ -94540,6 +94782,7 @@ class BackgroundJobStore {
|
|
|
94540
94782
|
};
|
|
94541
94783
|
}
|
|
94542
94784
|
removeAll() {
|
|
94785
|
+
this.pending.clear();
|
|
94543
94786
|
if (this.jobs.size === 0) {
|
|
94544
94787
|
return;
|
|
94545
94788
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.108",
|
|
4
4
|
"description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|