@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.
Files changed (2) hide show
  1. package/dist/index.js +74 -9
  2. 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 args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
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
- resolve8({ code: code ?? -1, out, err });
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 body = await runClaudeWithFailover(buildPrompt(docs, req.question, req.askerRole), ai);
35007
- if (!body) return { body: "", error: "empty answer" };
35008
- return { body };
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()) return { bodyMarkdown: 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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "license": "LicenseRef-4PM-Source-Available",