@4pm/cli 1.4.3 → 1.4.5
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 +74 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33663,6 +33663,10 @@ var WsChannels = {
|
|
|
33663
33663
|
USAGE_REPORT: "usage.report",
|
|
33664
33664
|
// cli → server: Claude subscription usage snapshot (5h / weekly — ADR-0072)
|
|
33665
33665
|
MACHINE_USAGE: "machine.usage",
|
|
33666
|
+
// cli → server: last-run result of an external tool (claude/codex/gh/glab/git — ADR-0223).
|
|
33667
|
+
// One-way, sent immediately after each direct tool invocation (last-wins per tool); the
|
|
33668
|
+
// server merges it into `MachineLink.usageSnapshot.toolHealth` for the admin pool lists.
|
|
33669
|
+
TOOL_HEALTH: "tool.health",
|
|
33666
33670
|
// cli → server: durable per-command history record on finish (ADR-0072)
|
|
33667
33671
|
COMMAND_HISTORY: "command.history",
|
|
33668
33672
|
// cli → server: periodic upload of the cli's own JSONL log file (ADR-0122) — stored
|
|
@@ -34011,6 +34015,24 @@ async function runCommand(dispatch, emit) {
|
|
|
34011
34015
|
});
|
|
34012
34016
|
}
|
|
34013
34017
|
|
|
34018
|
+
// src/core/tool-health.ts
|
|
34019
|
+
var sink = null;
|
|
34020
|
+
function setToolHealthSink(fn) {
|
|
34021
|
+
sink = fn;
|
|
34022
|
+
}
|
|
34023
|
+
function reportToolResult(tool, ok, message) {
|
|
34024
|
+
try {
|
|
34025
|
+
const reason = (message ?? "").trim().slice(0, 200);
|
|
34026
|
+
sink?.({
|
|
34027
|
+
tool,
|
|
34028
|
+
ok,
|
|
34029
|
+
message: ok ? null : reason || "failed",
|
|
34030
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
34031
|
+
});
|
|
34032
|
+
} catch {
|
|
34033
|
+
}
|
|
34034
|
+
}
|
|
34035
|
+
|
|
34014
34036
|
// src/core/ai-stream.ts
|
|
34015
34037
|
function assistantText(ev) {
|
|
34016
34038
|
const parts = ev.message?.content ?? [];
|
|
@@ -34117,8 +34139,11 @@ function runAttempt(commandId, cmd, args, cwd2, env3, onChunk) {
|
|
|
34117
34139
|
async function runAiFailover(plan, commandId, cwd2, handlers) {
|
|
34118
34140
|
const total = plan.attempts.length;
|
|
34119
34141
|
let finalExit = -1;
|
|
34142
|
+
let lastCmd = plan.attempts[0]?.cmd ?? "claude";
|
|
34143
|
+
let lastCaptured = "";
|
|
34120
34144
|
for (let i = 0; i < total; i++) {
|
|
34121
34145
|
const attempt = plan.attempts[i];
|
|
34146
|
+
lastCmd = attempt.cmd;
|
|
34122
34147
|
if (total > 1) handlers.onAttemptStart(attempt.label, i, total, attempt.cmd);
|
|
34123
34148
|
const parser = createAiStreamParser(attempt.cmd);
|
|
34124
34149
|
let captured = "";
|
|
@@ -34135,17 +34160,26 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
|
|
|
34135
34160
|
captured += tail;
|
|
34136
34161
|
handlers.onChunk(tail);
|
|
34137
34162
|
}
|
|
34163
|
+
lastCaptured = captured;
|
|
34138
34164
|
if (finalExit === 0) {
|
|
34139
34165
|
const usage = parser.usage();
|
|
34140
34166
|
if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
|
|
34167
|
+
reportToolResult(attempt.cmd, true);
|
|
34141
34168
|
return { exitCode: 0, workedDir: attempt.dir, workedKey: attempt.key, workedCmd: attempt.cmd, usage };
|
|
34142
34169
|
}
|
|
34143
34170
|
const reason = isAuthFailure(captured) ? "auth" : isSessionLimit(captured) ? "limit" : null;
|
|
34144
34171
|
if (!reason || i === total - 1) break;
|
|
34145
34172
|
handlers.onAttemptFail(attempt.label, reason);
|
|
34146
34173
|
}
|
|
34174
|
+
reportToolResult(lastCmd, false, summarizeAiFailure(lastCaptured, finalExit));
|
|
34147
34175
|
return { exitCode: finalExit, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
|
|
34148
34176
|
}
|
|
34177
|
+
function summarizeAiFailure(captured, exitCode) {
|
|
34178
|
+
if (isAuthFailure(captured)) return "Not logged in";
|
|
34179
|
+
if (isSessionLimit(captured)) return "Usage limit reached";
|
|
34180
|
+
const firstLine = captured.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
34181
|
+
return firstLine || `exited ${exitCode}`;
|
|
34182
|
+
}
|
|
34149
34183
|
|
|
34150
34184
|
// src/core/ai-profile-state.ts
|
|
34151
34185
|
import { existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
@@ -34872,6 +34906,7 @@ import { createHash as createHash3 } from "crypto";
|
|
|
34872
34906
|
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
34873
34907
|
import { join as join12 } from "path";
|
|
34874
34908
|
import { promisify as promisify2 } from "util";
|
|
34909
|
+
var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
34875
34910
|
var execFileAsync = promisify2(execFile2);
|
|
34876
34911
|
var MAX_CONTEXT_CHARS = 12e4;
|
|
34877
34912
|
var ANSWER_TIMEOUT_MS = 12e4;
|
|
@@ -34958,13 +34993,19 @@ function buildPrompt(docs, question, askerRole) {
|
|
|
34958
34993
|
}
|
|
34959
34994
|
function runClaudeOnce(cmd, profile, prompt, extraEnv) {
|
|
34960
34995
|
return new Promise((resolve8, reject) => {
|
|
34961
|
-
const
|
|
34996
|
+
const isClaude = cmd.includes("claude");
|
|
34997
|
+
const args = [
|
|
34998
|
+
"-p",
|
|
34999
|
+
...profile?.model ? ["--model", profile.model] : [],
|
|
35000
|
+
...isClaude ? ["--output-format", "stream-json", "--verbose"] : []
|
|
35001
|
+
];
|
|
34962
35002
|
const env3 = {
|
|
34963
35003
|
...process.env,
|
|
34964
35004
|
...extraEnv,
|
|
34965
35005
|
...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
|
|
34966
35006
|
};
|
|
34967
35007
|
const child = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
|
|
35008
|
+
const parser = createAiStreamParser(cmd);
|
|
34968
35009
|
let out = "";
|
|
34969
35010
|
let err = "";
|
|
34970
35011
|
const timer = setTimeout(() => {
|
|
@@ -34972,7 +35013,7 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv) {
|
|
|
34972
35013
|
reject(new Error("claude answer timed out"));
|
|
34973
35014
|
}, ANSWER_TIMEOUT_MS);
|
|
34974
35015
|
timer.unref();
|
|
34975
|
-
child.stdout.on("data", (d) => out += d.toString());
|
|
35016
|
+
child.stdout.on("data", (d) => out += parser.push(d.toString()));
|
|
34976
35017
|
child.stderr.on("data", (d) => err += d.toString());
|
|
34977
35018
|
child.on("error", (e) => {
|
|
34978
35019
|
clearTimeout(timer);
|
|
@@ -34980,7 +35021,8 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv) {
|
|
|
34980
35021
|
});
|
|
34981
35022
|
child.on("close", (code) => {
|
|
34982
35023
|
clearTimeout(timer);
|
|
34983
|
-
|
|
35024
|
+
out += parser.flush();
|
|
35025
|
+
resolve8({ code: code ?? -1, out, err, usage: parser.usage() });
|
|
34984
35026
|
});
|
|
34985
35027
|
child.stdin.write(prompt);
|
|
34986
35028
|
child.stdin.end();
|
|
@@ -34990,8 +35032,8 @@ async function runClaudeWithFailover(prompt, ai) {
|
|
|
34990
35032
|
const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
|
|
34991
35033
|
let lastReason = "no attempt";
|
|
34992
35034
|
for (let i = 0; i < attempts.length; i++) {
|
|
34993
|
-
const { code, out, err } = await runClaudeOnce(ai.cmd, attempts[i], prompt, ai.env);
|
|
34994
|
-
if (code === 0 && out.trim()) return out.trim();
|
|
35035
|
+
const { code, out, err, usage } = await runClaudeOnce(ai.cmd, attempts[i], prompt, ai.env);
|
|
35036
|
+
if (code === 0 && out.trim()) return { text: out.trim(), usage };
|
|
34995
35037
|
const combined = err || out;
|
|
34996
35038
|
lastReason = `exited ${code}: ${combined.slice(0, 500)}`;
|
|
34997
35039
|
if (!isAuthFailure(combined) && !isSessionLimit(combined) || i === attempts.length - 1) break;
|
|
@@ -35003,9 +35045,25 @@ async function runSupportAnswer(req, ai, profileDir2) {
|
|
|
35003
35045
|
const dir2 = await ensureRepo(profileDir2, req.repo);
|
|
35004
35046
|
const docs = collectDocs(dir2);
|
|
35005
35047
|
if (!docs.trim()) return { body: "", error: "KB repo has no documentation" };
|
|
35006
|
-
const
|
|
35007
|
-
|
|
35008
|
-
|
|
35048
|
+
const { text, usage } = await runClaudeWithFailover(
|
|
35049
|
+
buildPrompt(docs, req.question, req.askerRole),
|
|
35050
|
+
ai
|
|
35051
|
+
);
|
|
35052
|
+
if (!text) return { body: "", error: "empty answer" };
|
|
35053
|
+
const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens(text) };
|
|
35054
|
+
return {
|
|
35055
|
+
body: text,
|
|
35056
|
+
tokens: u.tokens,
|
|
35057
|
+
...usage.tokens > 0 ? {
|
|
35058
|
+
tokensBreakdown: {
|
|
35059
|
+
input: usage.input,
|
|
35060
|
+
output: usage.output,
|
|
35061
|
+
cacheRead: usage.cacheRead,
|
|
35062
|
+
cacheCreation: usage.cacheCreation
|
|
35063
|
+
}
|
|
35064
|
+
} : {},
|
|
35065
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
35066
|
+
};
|
|
35009
35067
|
} catch (err) {
|
|
35010
35068
|
logger.warn("support.answer.failed", { error: String(err) });
|
|
35011
35069
|
return { body: "", error: String(err) };
|
|
@@ -35068,7 +35126,10 @@ async function runKnowledgeCompose(req, ai, physicRoot) {
|
|
|
35068
35126
|
for (let i = 0; i < attempts.length; i++) {
|
|
35069
35127
|
try {
|
|
35070
35128
|
const { code, out, err } = await runOnce(ai.cmd, attempts[i], prompt, physicRoot, ai.env);
|
|
35071
|
-
if (code === 0 && out.trim())
|
|
35129
|
+
if (code === 0 && out.trim()) {
|
|
35130
|
+
reportToolResult(ai.cmd, true);
|
|
35131
|
+
return { bodyMarkdown: out.trim() };
|
|
35132
|
+
}
|
|
35072
35133
|
const combined = err || out;
|
|
35073
35134
|
lastReason = `exited ${code}: ${combined.slice(0, 500)}`;
|
|
35074
35135
|
if (!isAuthFailure(combined) && !isSessionLimit(combined) || i === attempts.length - 1) break;
|
|
@@ -35077,6 +35138,7 @@ async function runKnowledgeCompose(req, ai, physicRoot) {
|
|
|
35077
35138
|
break;
|
|
35078
35139
|
}
|
|
35079
35140
|
}
|
|
35141
|
+
reportToolResult(ai.cmd, false, lastReason);
|
|
35080
35142
|
return { bodyMarkdown: "", error: lastReason };
|
|
35081
35143
|
}
|
|
35082
35144
|
|
|
@@ -36961,6 +37023,7 @@ var WsClient = class {
|
|
|
36961
37023
|
socket.on("close", (code) => {
|
|
36962
37024
|
this.clearHeartbeat();
|
|
36963
37025
|
this.sessionKey = null;
|
|
37026
|
+
setToolHealthSink(null);
|
|
36964
37027
|
logger.debug("ws.close", { code });
|
|
36965
37028
|
if (code === CLOSE_DRAINING) {
|
|
36966
37029
|
this.bus.log("Server draining \u2014 will reconnect (backoff + jitter).");
|
|
@@ -37002,6 +37065,7 @@ var WsClient = class {
|
|
|
37002
37065
|
this.startUsagePolling();
|
|
37003
37066
|
this.startLogUpload();
|
|
37004
37067
|
this.startKbRefresh();
|
|
37068
|
+
setToolHealthSink((report) => this.send(WsChannels.TOOL_HEALTH, report));
|
|
37005
37069
|
this.connectWaiters.splice(0).forEach((fn) => fn());
|
|
37006
37070
|
return;
|
|
37007
37071
|
}
|
|
@@ -37174,6 +37238,7 @@ var WsClient = class {
|
|
|
37174
37238
|
this.bus.push({ source: "server", kind: "aires", text: `${supportCmd} \u203A` });
|
|
37175
37239
|
this.bus.push({ source: "server", kind: "out", text: reply.body });
|
|
37176
37240
|
}
|
|
37241
|
+
reportToolResult(supportCmd, !reply.error, reply.error);
|
|
37177
37242
|
this.send(WsChannels.SUPPORT_ANSWER, reply, message.id);
|
|
37178
37243
|
}).finally(() => this.bus.endBusy("support-answer"));
|
|
37179
37244
|
break;
|