@4pm/cli 1.4.4 → 1.4.6

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 +77 -44
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28420,6 +28420,31 @@ function collectFingerprint() {
28420
28420
  return { fingerprint, hostname: host };
28421
28421
  }
28422
28422
 
28423
+ // src/utils/secure-url.ts
28424
+ function isLocalHost(hostname2) {
28425
+ return hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "::1" || hostname2.endsWith(".localhost");
28426
+ }
28427
+ function isInsecureRemoteUrl(url) {
28428
+ let parsed;
28429
+ try {
28430
+ parsed = new URL(url);
28431
+ } catch {
28432
+ return false;
28433
+ }
28434
+ if (parsed.protocol === "https:" || parsed.protocol === "wss:") return false;
28435
+ return !isLocalHost(parsed.hostname);
28436
+ }
28437
+ function insecureTransportAllowed() {
28438
+ const flag = (process.env.FOURPM_ALLOW_INSECURE_TRANSPORT ?? "").trim().toLowerCase();
28439
+ return flag === "1" || flag === "true" || flag === "yes";
28440
+ }
28441
+ var INSECURE_URL_BLOCKED = "\u2717 Refusing to connect over plaintext (http/ws) to a non-local host: credentials (pairing codes / ws_token) would travel in the clear to an unauthenticated server. Use an https://server URL. To override on a trusted private network only, set FOURPM_ALLOW_INSECURE_TRANSPORT=1.";
28442
+ function assertSecureRemoteUrl(url) {
28443
+ if (isInsecureRemoteUrl(url) && !insecureTransportAllowed()) {
28444
+ throw new Error(INSECURE_URL_BLOCKED);
28445
+ }
28446
+ }
28447
+
28423
28448
  // src/services/api.ts
28424
28449
  var CliApiError = class extends Error {
28425
28450
  constructor(errorCode, message, status) {
@@ -28443,6 +28468,7 @@ async function unwrap(res) {
28443
28468
  return envelope.data;
28444
28469
  }
28445
28470
  async function post(serverUrl, path, body) {
28471
+ assertSecureRemoteUrl(serverUrl);
28446
28472
  const res = await fetch(`${serverUrl}${path}`, {
28447
28473
  method: "POST",
28448
28474
  headers: { "Content-Type": "application/json" },
@@ -28478,30 +28504,12 @@ async function fetchCliVersion(serverUrl) {
28478
28504
  return unwrap(res);
28479
28505
  }
28480
28506
 
28481
- // src/utils/secure-url.ts
28482
- function isLocalHost(hostname2) {
28483
- return hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "::1" || hostname2.endsWith(".localhost");
28484
- }
28485
- function isInsecureRemoteUrl(url) {
28486
- let parsed;
28487
- try {
28488
- parsed = new URL(url);
28489
- } catch {
28490
- return false;
28491
- }
28492
- if (parsed.protocol === "https:" || parsed.protocol === "wss:") return false;
28493
- return !isLocalHost(parsed.hostname);
28494
- }
28495
- var INSECURE_URL_WARNING = "\u26A0 Insecure transport: connecting over plaintext (http/ws) to a non-local host. Pairing codes and the ws_token are sent in cleartext and the server is not authenticated (the app-layer encryption does not protect against a man-in-the-middle). Use an https:// server URL in production.";
28496
-
28497
28507
  // src/commands/link.ts
28498
28508
  function sanitizeProfileName(name) {
28499
28509
  return name.replace(/[^a-zA-Z0-9._-]/g, "_") || "profile";
28500
28510
  }
28501
28511
  async function runLink(serverUrl, explicitProfile, token = null) {
28502
- if (isInsecureRemoteUrl(serverUrl)) console.warn(`
28503
- ${INSECURE_URL_WARNING}
28504
- `);
28512
+ assertSecureRemoteUrl(serverUrl);
28505
28513
  let result;
28506
28514
  if (token) {
28507
28515
  console.log(`\u2500\u2500 4PM cli headless pairing (profile: ${explicitProfile ?? "default"}) \u2500\u2500`);
@@ -28782,17 +28790,17 @@ function isValidRoleSet(roles) {
28782
28790
 
28783
28791
  // vendor/@4pm/constants/worker-tools.ts
28784
28792
  var WORKER_TOOL_CATALOG = [
28785
- { id: "git", label: "Git", category: "vcs", versionArg: "--version", installPackage: null },
28786
- { id: "node", label: "Node.js", category: "runtime", versionArg: "--version", installPackage: null },
28787
- { id: "npm", label: "npm", category: "runtime", versionArg: "--version", installPackage: null },
28788
- { id: "pnpm", label: "pnpm", category: "runtime", versionArg: "--version", installPackage: "pnpm" },
28789
- { id: "claude", label: "Claude Code", category: "ai-cli", versionArg: "--version", installPackage: "@anthropic-ai/claude-code" },
28790
- { id: "codex", label: "Codex", category: "ai-cli", versionArg: "--version", installPackage: "@openai/codex" },
28791
- { id: "gh", label: "GitHub CLI", category: "vcs", versionArg: "--version", installPackage: null },
28792
- { id: "glab", label: "GitLab CLI", category: "vcs", versionArg: "--version", installPackage: null }
28793
+ { id: "git", label: "Git", category: "vcs", versionArg: "--version", installPackage: null, installable: false },
28794
+ { id: "node", label: "Node.js", category: "runtime", versionArg: "--version", installPackage: null, installable: false },
28795
+ { id: "npm", label: "npm", category: "runtime", versionArg: "--version", installPackage: null, installable: false },
28796
+ { id: "pnpm", label: "pnpm", category: "runtime", versionArg: "--version", installPackage: "pnpm", installable: false },
28797
+ { id: "claude", label: "Claude Code", category: "ai-cli", versionArg: "--version", installPackage: "@anthropic-ai/claude-code", installable: false },
28798
+ { id: "codex", label: "Codex", category: "ai-cli", versionArg: "--version", installPackage: "@openai/codex", installable: false },
28799
+ { id: "gh", label: "GitHub CLI", category: "vcs", versionArg: "--version", installPackage: null, installable: false },
28800
+ { id: "glab", label: "GitLab CLI", category: "vcs", versionArg: "--version", installPackage: null, installable: false }
28793
28801
  ];
28794
28802
  var WORKER_TOOL_PREREQUISITE_IDS = WORKER_TOOL_CATALOG.filter(
28795
- (t) => t.installPackage === null
28803
+ (t) => !t.installable
28796
28804
  ).map((t) => t.id);
28797
28805
  var WORKER_TOOL_BUNDLED_GLOBALS = ["npm", "corepack"];
28798
28806
 
@@ -34906,6 +34914,7 @@ import { createHash as createHash3 } from "crypto";
34906
34914
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
34907
34915
  import { join as join12 } from "path";
34908
34916
  import { promisify as promisify2 } from "util";
34917
+ var NO_USAGE = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
34909
34918
  var execFileAsync = promisify2(execFile2);
34910
34919
  var MAX_CONTEXT_CHARS = 12e4;
34911
34920
  var ANSWER_TIMEOUT_MS = 12e4;
@@ -34992,13 +35001,19 @@ function buildPrompt(docs, question, askerRole) {
34992
35001
  }
34993
35002
  function runClaudeOnce(cmd, profile, prompt, extraEnv) {
34994
35003
  return new Promise((resolve8, reject) => {
34995
- const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
35004
+ const isClaude = cmd.includes("claude");
35005
+ const args = [
35006
+ "-p",
35007
+ ...profile?.model ? ["--model", profile.model] : [],
35008
+ ...isClaude ? ["--output-format", "stream-json", "--verbose"] : []
35009
+ ];
34996
35010
  const env3 = {
34997
35011
  ...process.env,
34998
35012
  ...extraEnv,
34999
35013
  ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
35000
35014
  };
35001
35015
  const child = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
35016
+ const parser = createAiStreamParser(cmd);
35002
35017
  let out = "";
35003
35018
  let err = "";
35004
35019
  const timer = setTimeout(() => {
@@ -35006,7 +35021,7 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv) {
35006
35021
  reject(new Error("claude answer timed out"));
35007
35022
  }, ANSWER_TIMEOUT_MS);
35008
35023
  timer.unref();
35009
- child.stdout.on("data", (d) => out += d.toString());
35024
+ child.stdout.on("data", (d) => out += parser.push(d.toString()));
35010
35025
  child.stderr.on("data", (d) => err += d.toString());
35011
35026
  child.on("error", (e) => {
35012
35027
  clearTimeout(timer);
@@ -35014,7 +35029,8 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv) {
35014
35029
  });
35015
35030
  child.on("close", (code) => {
35016
35031
  clearTimeout(timer);
35017
- resolve8({ code: code ?? -1, out, err });
35032
+ out += parser.flush();
35033
+ resolve8({ code: code ?? -1, out, err, usage: parser.usage() });
35018
35034
  });
35019
35035
  child.stdin.write(prompt);
35020
35036
  child.stdin.end();
@@ -35024,8 +35040,8 @@ async function runClaudeWithFailover(prompt, ai) {
35024
35040
  const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
35025
35041
  let lastReason = "no attempt";
35026
35042
  for (let i = 0; i < attempts.length; i++) {
35027
- const { code, out, err } = await runClaudeOnce(ai.cmd, attempts[i], prompt, ai.env);
35028
- if (code === 0 && out.trim()) return out.trim();
35043
+ const { code, out, err, usage } = await runClaudeOnce(ai.cmd, attempts[i], prompt, ai.env);
35044
+ if (code === 0 && out.trim()) return { text: out.trim(), usage };
35029
35045
  const combined = err || out;
35030
35046
  lastReason = `exited ${code}: ${combined.slice(0, 500)}`;
35031
35047
  if (!isAuthFailure(combined) && !isSessionLimit(combined) || i === attempts.length - 1) break;
@@ -35037,9 +35053,25 @@ async function runSupportAnswer(req, ai, profileDir2) {
35037
35053
  const dir2 = await ensureRepo(profileDir2, req.repo);
35038
35054
  const docs = collectDocs(dir2);
35039
35055
  if (!docs.trim()) return { body: "", error: "KB repo has no documentation" };
35040
- const body = await runClaudeWithFailover(buildPrompt(docs, req.question, req.askerRole), ai);
35041
- if (!body) return { body: "", error: "empty answer" };
35042
- return { body };
35056
+ const { text, usage } = await runClaudeWithFailover(
35057
+ buildPrompt(docs, req.question, req.askerRole),
35058
+ ai
35059
+ );
35060
+ if (!text) return { body: "", error: "empty answer" };
35061
+ const u = usage.tokens > 0 ? usage : { ...NO_USAGE, tokens: estimateTokens(text) };
35062
+ return {
35063
+ body: text,
35064
+ tokens: u.tokens,
35065
+ ...usage.tokens > 0 ? {
35066
+ tokensBreakdown: {
35067
+ input: usage.input,
35068
+ output: usage.output,
35069
+ cacheRead: usage.cacheRead,
35070
+ cacheCreation: usage.cacheCreation
35071
+ }
35072
+ } : {},
35073
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
35074
+ };
35043
35075
  } catch (err) {
35044
35076
  logger.warn("support.answer.failed", { error: String(err) });
35045
35077
  return { body: "", error: String(err) };
@@ -36636,7 +36668,7 @@ async function detectWorkerTools() {
36636
36668
  category: t.category,
36637
36669
  installed: version !== null,
36638
36670
  version,
36639
- installable: t.installPackage !== null
36671
+ installable: t.installable
36640
36672
  };
36641
36673
  })
36642
36674
  );
@@ -36646,10 +36678,10 @@ async function detectWorkerTools() {
36646
36678
  function resolvePackage(name, op) {
36647
36679
  const entry = WORKER_TOOL_CATALOG.find((t) => t.id === name);
36648
36680
  if (entry) {
36649
- if (entry.installPackage === null) {
36681
+ if (!entry.installable) {
36650
36682
  return { error: `"${name}" is a prerequisite and cannot be ${op}ed from here.` };
36651
36683
  }
36652
- return { pkg: entry.installPackage };
36684
+ return { pkg: entry.installPackage ?? name };
36653
36685
  }
36654
36686
  if (WORKER_TOOL_PREREQUISITE_IDS.includes(name)) {
36655
36687
  return { error: `"${name}" is a prerequisite and cannot be ${op}ed from here.` };
@@ -36725,7 +36757,6 @@ var WsClient = class {
36725
36757
  ecdh = null;
36726
36758
  wsToken = "";
36727
36759
  /** Warn about a plaintext-remote server URL only once per process (ADR-0194 finding #1). */
36728
- insecureWarned = false;
36729
36760
  heartbeat = null;
36730
36761
  usageTimer = null;
36731
36762
  logUploadTimer = null;
@@ -36969,10 +37000,12 @@ var WsClient = class {
36969
37000
  }
36970
37001
  const wsBase = this.credential.wsUrl ?? this.credential.serverUrl;
36971
37002
  const wsUrl = wsBase.replace(/^http/, "ws") + "/ws";
36972
- if (!this.insecureWarned && isInsecureRemoteUrl(wsUrl)) {
36973
- this.insecureWarned = true;
36974
- this.bus.log(INSECURE_URL_WARNING, "warn");
36975
- logger.warn("ws.insecure-transport", { url: wsBase });
37003
+ if (isInsecureRemoteUrl(wsUrl) && !insecureTransportAllowed()) {
37004
+ this.stopped = true;
37005
+ this.bus.log(INSECURE_URL_BLOCKED, "error");
37006
+ this.bus.setStatus("stopped");
37007
+ logger.error("ws.insecure-transport-blocked", { url: wsBase });
37008
+ throw new Error(INSECURE_URL_BLOCKED);
36976
37009
  }
36977
37010
  const socket = new wrapper_default(wsUrl);
36978
37011
  this.socket = socket;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
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",