@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.
- package/dist/index.js +77 -44
- 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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
35041
|
-
|
|
35042
|
-
|
|
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.
|
|
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.
|
|
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 (
|
|
36973
|
-
this.
|
|
36974
|
-
this.bus.log(
|
|
36975
|
-
|
|
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;
|