@4pm/cli 1.4.12 → 1.4.20
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 +62 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28312,7 +28312,10 @@ function defaultProfileConfig() {
|
|
|
28312
28312
|
aiFailoverMode: "remember",
|
|
28313
28313
|
autoUpdate: true,
|
|
28314
28314
|
commandHistoryUploadMinutes: 10,
|
|
28315
|
-
autoClearIdleMinutes: 10
|
|
28315
|
+
autoClearIdleMinutes: 10,
|
|
28316
|
+
// Terminate a single AI run after 5 min by default (ADR-0243) so a hung/looping AI CLI
|
|
28317
|
+
// (e.g. a heavy spec review/compose) can't spin the dispatcher forever; 0 = no limit.
|
|
28318
|
+
aiRunTimeoutSec: 300
|
|
28316
28319
|
};
|
|
28317
28320
|
}
|
|
28318
28321
|
function ensureProfileConfig(dir2) {
|
|
@@ -28615,6 +28618,9 @@ function readInstalledVersion() {
|
|
|
28615
28618
|
}
|
|
28616
28619
|
}
|
|
28617
28620
|
function updateViaNpm(version) {
|
|
28621
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
28622
|
+
throw new Error(`Refusing to update: invalid version string "${version}".`);
|
|
28623
|
+
}
|
|
28618
28624
|
execSync2(`npm i -g @4pm/cli@${version}`, { stdio: "inherit" });
|
|
28619
28625
|
}
|
|
28620
28626
|
async function updateViaDownload(tarballUrl, checksum, signature) {
|
|
@@ -33973,7 +33979,8 @@ var UpdateScheduler = class {
|
|
|
33973
33979
|
|
|
33974
33980
|
// src/core/executor.ts
|
|
33975
33981
|
import { spawn as spawn2 } from "child_process";
|
|
33976
|
-
|
|
33982
|
+
var KILL_GRACE_MS = 5e3;
|
|
33983
|
+
async function runCommand(dispatch, emit, opts) {
|
|
33977
33984
|
let seq = 0;
|
|
33978
33985
|
const child = spawn2(dispatch.cmd, dispatch.args ?? [], {
|
|
33979
33986
|
cwd: dispatch.path,
|
|
@@ -33981,6 +33988,27 @@ async function runCommand(dispatch, emit) {
|
|
|
33981
33988
|
shell: false
|
|
33982
33989
|
});
|
|
33983
33990
|
child.stdin?.end();
|
|
33991
|
+
const timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
|
|
33992
|
+
let timedOut = false;
|
|
33993
|
+
let hardKillTimer = null;
|
|
33994
|
+
const timeoutTimer = timeoutMs > 0 ? setTimeout(() => {
|
|
33995
|
+
timedOut = true;
|
|
33996
|
+
emit({
|
|
33997
|
+
commandId: dispatch.commandId,
|
|
33998
|
+
seq: seq++,
|
|
33999
|
+
chunk: `
|
|
34000
|
+
[4PM] AI run exceeded the ${Math.round(timeoutMs / 1e3)}s time limit \u2014 terminating.
|
|
34001
|
+
`
|
|
34002
|
+
});
|
|
34003
|
+
child.kill("SIGTERM");
|
|
34004
|
+
hardKillTimer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
|
|
34005
|
+
hardKillTimer.unref?.();
|
|
34006
|
+
}, timeoutMs) : null;
|
|
34007
|
+
timeoutTimer?.unref?.();
|
|
34008
|
+
const clearTimers = () => {
|
|
34009
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
34010
|
+
if (hardKillTimer) clearTimeout(hardKillTimer);
|
|
34011
|
+
};
|
|
33984
34012
|
const batcher = new OutputBatcher({
|
|
33985
34013
|
onFlush: (chunk, truncated) => {
|
|
33986
34014
|
emit({ commandId: dispatch.commandId, seq: seq++, chunk, truncated });
|
|
@@ -33999,17 +34027,21 @@ async function runCommand(dispatch, emit) {
|
|
|
33999
34027
|
child.stderr?.on("data", (data) => batcher.push(data.toString("utf8")));
|
|
34000
34028
|
await new Promise((resolve8) => {
|
|
34001
34029
|
child.on("close", (exitCode) => {
|
|
34030
|
+
clearTimers();
|
|
34002
34031
|
batcher.flush();
|
|
34003
34032
|
emit({
|
|
34004
34033
|
commandId: dispatch.commandId,
|
|
34005
34034
|
seq: seq++,
|
|
34006
34035
|
chunk: "",
|
|
34007
34036
|
done: true,
|
|
34008
|
-
|
|
34037
|
+
// 124 (the conventional timeout code) when we killed it for exceeding the limit, so the
|
|
34038
|
+
// server settles the command as failed and the web surfaces the note instead of spinning.
|
|
34039
|
+
exitCode: timedOut ? 124 : exitCode ?? -1
|
|
34009
34040
|
});
|
|
34010
34041
|
resolve8();
|
|
34011
34042
|
});
|
|
34012
34043
|
child.on("error", (err) => {
|
|
34044
|
+
clearTimers();
|
|
34013
34045
|
emit({
|
|
34014
34046
|
commandId: dispatch.commandId,
|
|
34015
34047
|
seq: seq++,
|
|
@@ -34136,15 +34168,15 @@ function isSessionLimit(text) {
|
|
|
34136
34168
|
text
|
|
34137
34169
|
);
|
|
34138
34170
|
}
|
|
34139
|
-
function runAttempt(commandId, cmd, args, cwd2, env3, onChunk) {
|
|
34171
|
+
function runAttempt(commandId, cmd, args, cwd2, env3, onChunk, timeoutMs) {
|
|
34140
34172
|
return new Promise((resolve8) => {
|
|
34141
34173
|
void runCommand({ commandId, cmd, args, path: cwd2, env: env3 }, (out) => {
|
|
34142
34174
|
if (out.chunk) onChunk(out.chunk);
|
|
34143
34175
|
if (out.done) resolve8(out.exitCode ?? -1);
|
|
34144
|
-
});
|
|
34176
|
+
}, timeoutMs > 0 ? { timeoutMs } : void 0);
|
|
34145
34177
|
});
|
|
34146
34178
|
}
|
|
34147
|
-
async function runAiFailover(plan, commandId, cwd2, handlers) {
|
|
34179
|
+
async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
34148
34180
|
const total = plan.attempts.length;
|
|
34149
34181
|
let finalExit = -1;
|
|
34150
34182
|
let lastCmd = plan.attempts[0]?.cmd ?? "claude";
|
|
@@ -34162,7 +34194,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
|
|
|
34162
34194
|
handlers.onChunk(text);
|
|
34163
34195
|
}
|
|
34164
34196
|
};
|
|
34165
|
-
finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, cwd2, attempt.env, emit);
|
|
34197
|
+
finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, cwd2, attempt.env, emit, timeoutMs);
|
|
34166
34198
|
const tail = parser.flush();
|
|
34167
34199
|
if (tail) {
|
|
34168
34200
|
captured += tail;
|
|
@@ -34175,8 +34207,8 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
|
|
|
34175
34207
|
reportToolResult(attempt.cmd, true);
|
|
34176
34208
|
return { exitCode: 0, workedDir: attempt.dir, workedKey: attempt.key, workedCmd: attempt.cmd, usage };
|
|
34177
34209
|
}
|
|
34178
|
-
const reason = isAuthFailure(captured) ? "auth" : isSessionLimit(captured) ? "limit" :
|
|
34179
|
-
if (
|
|
34210
|
+
const reason = isAuthFailure(captured) ? "auth" : isSessionLimit(captured) ? "limit" : "other";
|
|
34211
|
+
if (i === total - 1) break;
|
|
34180
34212
|
handlers.onAttemptFail(attempt.label, reason);
|
|
34181
34213
|
}
|
|
34182
34214
|
reportToolResult(lastCmd, false, summarizeAiFailure(lastCaptured, finalExit));
|
|
@@ -35075,7 +35107,7 @@ async function runClaudeWithFailover(prompt, ai) {
|
|
|
35075
35107
|
if (code === 0 && out.trim()) return { text: out.trim(), usage };
|
|
35076
35108
|
const combined = err || out;
|
|
35077
35109
|
lastReason = `exited ${code}: ${combined.slice(0, 500)}`;
|
|
35078
|
-
if (
|
|
35110
|
+
if (i === attempts.length - 1) break;
|
|
35079
35111
|
}
|
|
35080
35112
|
throw new Error(`claude ${lastReason}`);
|
|
35081
35113
|
}
|
|
@@ -35174,7 +35206,7 @@ async function runKnowledgeCompose(req, ai, physicRoot) {
|
|
|
35174
35206
|
}
|
|
35175
35207
|
const combined = err || out;
|
|
35176
35208
|
lastReason = `exited ${code}: ${combined.slice(0, 500)}`;
|
|
35177
|
-
if (
|
|
35209
|
+
if (i === attempts.length - 1) break;
|
|
35178
35210
|
} catch (e) {
|
|
35179
35211
|
lastReason = String(e);
|
|
35180
35212
|
break;
|
|
@@ -36177,6 +36209,9 @@ async function ragQuery(root, queryText, k = 8) {
|
|
|
36177
36209
|
}
|
|
36178
36210
|
}
|
|
36179
36211
|
async function ragInstall(root, model) {
|
|
36212
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(model)) {
|
|
36213
|
+
return { ok: false, started: false, error: "invalid model name" };
|
|
36214
|
+
}
|
|
36180
36215
|
const py = await detectPython();
|
|
36181
36216
|
if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
|
|
36182
36217
|
if (existsSync10(join20(root, RAG_REL, ".installing"))) return { ok: true, started: true };
|
|
@@ -36590,7 +36625,14 @@ async function addProject(payload, profileDir2, onProgress) {
|
|
|
36590
36625
|
// src/core/config-sync.ts
|
|
36591
36626
|
import { existsSync as existsSync11, readFileSync as readFileSync15 } from "fs";
|
|
36592
36627
|
import { join as join23 } from "path";
|
|
36593
|
-
var SERVER_MANAGED_KEYS = [
|
|
36628
|
+
var SERVER_MANAGED_KEYS = [
|
|
36629
|
+
"physicPath",
|
|
36630
|
+
"sessionSwitchPct",
|
|
36631
|
+
"perPromptTokenLimit",
|
|
36632
|
+
// Project AI-run timeout override mirror (ADR-0243) — the ws_token refreshes it; a Worker-config
|
|
36633
|
+
// edit must not fight it. NOTE: the machine-user's own `aiRunTimeoutSec` is NOT here (operator-editable).
|
|
36634
|
+
"projectAiRunTimeoutSec"
|
|
36635
|
+
];
|
|
36594
36636
|
function readConfigText(profileDir2) {
|
|
36595
36637
|
const path = join23(profileDir2, "config.json");
|
|
36596
36638
|
if (existsSync11(path)) {
|
|
@@ -37015,7 +37057,9 @@ var WsClient = class {
|
|
|
37015
37057
|
}
|
|
37016
37058
|
writeProfileConfig(this.context.profileDir, {
|
|
37017
37059
|
sessionSwitchPct: token.projectTokens?.sessionSwitchPct ?? 0,
|
|
37018
|
-
perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0
|
|
37060
|
+
perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0,
|
|
37061
|
+
// Project AI-run timeout override (ADR-0243) — 0 ⇒ inherit the machine-user setting.
|
|
37062
|
+
projectAiRunTimeoutSec: token.projectTokens?.aiRunTimeoutSec ?? 0
|
|
37019
37063
|
});
|
|
37020
37064
|
this.outboundReviewEnabled = token.outboundReview?.enabled === true;
|
|
37021
37065
|
this.restrictToFolder = token.aiRestrictToFolder === true;
|
|
@@ -37636,6 +37680,10 @@ var WsClient = class {
|
|
|
37636
37680
|
*/
|
|
37637
37681
|
async runAiPrompt(prompt, commandId, origin) {
|
|
37638
37682
|
const config = readProfileConfig(this.context.profileDir);
|
|
37683
|
+
const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
|
|
37684
|
+
const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
|
|
37685
|
+
const timeoutSec = projectTimeoutSec > 0 ? projectTimeoutSec : machineTimeoutSec;
|
|
37686
|
+
const aiRunTimeoutMs = timeoutSec > 0 ? timeoutSec * 1e3 : 0;
|
|
37639
37687
|
const perPromptLimit = config.perPromptTokenLimit ?? 0;
|
|
37640
37688
|
if (perPromptLimit > 0) {
|
|
37641
37689
|
const estimated = estimateTokens(prompt);
|
|
@@ -37754,7 +37802,7 @@ var WsClient = class {
|
|
|
37754
37802
|
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
37755
37803
|
`, log: true });
|
|
37756
37804
|
}
|
|
37757
|
-
});
|
|
37805
|
+
}, aiRunTimeoutMs);
|
|
37758
37806
|
this.bus.endBusy(plan.cmd);
|
|
37759
37807
|
const profileLabel = result.workedDir ? profileDisplayLabel(result.workedDir) : null;
|
|
37760
37808
|
if (result.workedDir) {
|