@4pm/cli 1.2.4 → 1.2.7
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 +146 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28211,7 +28211,6 @@ import { join as join2 } from "path";
|
|
|
28211
28211
|
// src/config/profile.ts
|
|
28212
28212
|
import {
|
|
28213
28213
|
existsSync,
|
|
28214
|
-
mkdirSync,
|
|
28215
28214
|
readdirSync,
|
|
28216
28215
|
readFileSync,
|
|
28217
28216
|
rmSync,
|
|
@@ -28219,6 +28218,30 @@ import {
|
|
|
28219
28218
|
} from "fs";
|
|
28220
28219
|
import { homedir } from "os";
|
|
28221
28220
|
import { join } from "path";
|
|
28221
|
+
|
|
28222
|
+
// src/common/io/ensure-dir.ts
|
|
28223
|
+
import { mkdirSync } from "fs";
|
|
28224
|
+
var CLI_IMAGE = "ghcr.io/4azuo/4pm-cli:full";
|
|
28225
|
+
function ensureDir(dir2) {
|
|
28226
|
+
try {
|
|
28227
|
+
mkdirSync(dir2, { recursive: true });
|
|
28228
|
+
} catch (err) {
|
|
28229
|
+
const code = err.code;
|
|
28230
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
28231
|
+
throw new Error(
|
|
28232
|
+
`Cannot write "${dir2}" \u2014 the mounted home is owned by root. A reused Docker volume or a host bind-mount shadows the image's node-owned home, and the cli runs as the unprivileged "node" user (uid 1000), so it cannot chown it.
|
|
28233
|
+
Fix the mount ONCE, then retry:
|
|
28234
|
+
docker run --rm --user 0 --cap-drop=ALL --cap-add=CHOWN --cap-add=DAC_OVERRIDE \\
|
|
28235
|
+
--security-opt=no-new-privileges --entrypoint chown \\
|
|
28236
|
+
-v <volume>:/home/node/.4pm ${CLI_IMAGE} -R node:node /home/node/.4pm
|
|
28237
|
+
(host bind-mount: run \`sudo chown -R 1000:1000 <host-path>\` on the host instead; a FRESH named volume needs none of this.)`
|
|
28238
|
+
);
|
|
28239
|
+
}
|
|
28240
|
+
throw err;
|
|
28241
|
+
}
|
|
28242
|
+
}
|
|
28243
|
+
|
|
28244
|
+
// src/config/profile.ts
|
|
28222
28245
|
var DEFAULT_PROFILE = "default";
|
|
28223
28246
|
function defaultPointerPath() {
|
|
28224
28247
|
return join(homedir(), ".4pm", "default");
|
|
@@ -28232,7 +28255,7 @@ function defaultProfileName() {
|
|
|
28232
28255
|
return DEFAULT_PROFILE;
|
|
28233
28256
|
}
|
|
28234
28257
|
function writeDefaultProfile(userId) {
|
|
28235
|
-
|
|
28258
|
+
ensureDir(join(homedir(), ".4pm"));
|
|
28236
28259
|
writeFileSync(defaultPointerPath(), userId, "utf8");
|
|
28237
28260
|
}
|
|
28238
28261
|
function clearDefaultProfileIf(name) {
|
|
@@ -28269,7 +28292,7 @@ function listProfiles() {
|
|
|
28269
28292
|
}
|
|
28270
28293
|
function profileDir(name) {
|
|
28271
28294
|
const dir2 = join(homedir(), ".4pm", "profiles", name);
|
|
28272
|
-
|
|
28295
|
+
ensureDir(dir2);
|
|
28273
28296
|
return dir2;
|
|
28274
28297
|
}
|
|
28275
28298
|
function defaultProfileConfig() {
|
|
@@ -28287,6 +28310,8 @@ function defaultProfileConfig() {
|
|
|
28287
28310
|
{ provider: "codex", profile: ".codex", args: [], model: "" }
|
|
28288
28311
|
],
|
|
28289
28312
|
aiFailoverMode: "remember",
|
|
28313
|
+
// Default to the fully-isolated posture (ADR-0199) — auth lives inside the sandbox.
|
|
28314
|
+
aiCredentialMode: "in-container",
|
|
28290
28315
|
autoUpdate: true,
|
|
28291
28316
|
commandHistoryUploadMinutes: 10,
|
|
28292
28317
|
autoClearIdleMinutes: 10
|
|
@@ -28492,7 +28517,7 @@ ${INSECURE_URL_WARNING}
|
|
|
28492
28517
|
`);
|
|
28493
28518
|
console.log("2. The web will show a confirmation code \u2014 paste it here.");
|
|
28494
28519
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
28495
|
-
const hashcode2 = (await rl.question("
|
|
28520
|
+
const hashcode2 = (await rl.question("Confirmation code: ")).trim();
|
|
28496
28521
|
rl.close();
|
|
28497
28522
|
result = await confirmPairing(serverUrl, hashcode2, collectFingerprint());
|
|
28498
28523
|
}
|
|
@@ -29803,7 +29828,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
|
|
|
29803
29828
|
let finalExit = -1;
|
|
29804
29829
|
for (let i = 0; i < total; i++) {
|
|
29805
29830
|
const attempt = plan.attempts[i];
|
|
29806
|
-
if (total > 1) handlers.onAttemptStart(attempt.label, i, total);
|
|
29831
|
+
if (total > 1) handlers.onAttemptStart(attempt.label, i, total, attempt.cmd);
|
|
29807
29832
|
const parser = createAiStreamParser(attempt.cmd);
|
|
29808
29833
|
let captured = "";
|
|
29809
29834
|
const emit = (raw) => {
|
|
@@ -29911,6 +29936,10 @@ function resolveClaudeAuthMode(config) {
|
|
|
29911
29936
|
function isUnifiedConfig(config) {
|
|
29912
29937
|
return Array.isArray(config.aiProfiles) && config.aiProfiles.some(isUsableCredential);
|
|
29913
29938
|
}
|
|
29939
|
+
function activeProvider(config) {
|
|
29940
|
+
const v = config.aiCli?.trim();
|
|
29941
|
+
return v && AI_PROVIDERS.includes(v) ? v : null;
|
|
29942
|
+
}
|
|
29914
29943
|
function claudeProfilesOf(config) {
|
|
29915
29944
|
if (isUnifiedConfig(config)) return config.aiProfiles.filter((c) => c.provider === "claude");
|
|
29916
29945
|
return config.claudeHome ?? [];
|
|
@@ -29984,7 +30013,18 @@ function planAiRun(prompt, config, hint = {}) {
|
|
|
29984
30013
|
}
|
|
29985
30014
|
function planUnifiedRun(prompt, config, workingCredential) {
|
|
29986
30015
|
const baseEnv = config.aiEnv ?? {};
|
|
29987
|
-
const
|
|
30016
|
+
const active = activeProvider(config);
|
|
30017
|
+
const usable = config.aiProfiles.filter(isUsableCredential);
|
|
30018
|
+
const scoped = active ? usable.filter((c) => c.provider === active) : usable;
|
|
30019
|
+
if (active && scoped.length === 0) {
|
|
30020
|
+
return {
|
|
30021
|
+
cmd: active,
|
|
30022
|
+
attempts: [
|
|
30023
|
+
{ cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, prompt), env: { ...baseEnv } }
|
|
30024
|
+
]
|
|
30025
|
+
};
|
|
30026
|
+
}
|
|
30027
|
+
const resolved = scoped.map((cred) => {
|
|
29988
30028
|
const cmd = cred.provider;
|
|
29989
30029
|
const dir2 = resolveHomePath(cred.profile);
|
|
29990
30030
|
return { cred, cmd, dir: dir2, key: credentialKey(cmd, dir2), envVar: profileEnvVar(cmd) };
|
|
@@ -33080,8 +33120,8 @@ var WsClient = class {
|
|
|
33080
33120
|
this.bus.push({ source: origin, kind: "out", text: resultBuf });
|
|
33081
33121
|
}
|
|
33082
33122
|
},
|
|
33083
|
-
onAttemptStart: (label, index, total) => {
|
|
33084
|
-
const text = `\u2192 trying ${
|
|
33123
|
+
onAttemptStart: (label, index, total, cmd2) => {
|
|
33124
|
+
const text = `\u2192 trying ${cmd2} profile "${label}" (${index + 1}/${total})\u2026`;
|
|
33085
33125
|
this.bus.push({ source: origin, kind: "log", text });
|
|
33086
33126
|
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
33087
33127
|
`, log: true });
|
|
@@ -33596,7 +33636,7 @@ function isAlive(pid) {
|
|
|
33596
33636
|
function lockHolder(profileDir2) {
|
|
33597
33637
|
try {
|
|
33598
33638
|
const pid = Number.parseInt(readFileSync15(lockPath(profileDir2), "utf8").trim(), 10);
|
|
33599
|
-
if (Number.isInteger(pid) && pid > 0 && isAlive(pid)) return pid;
|
|
33639
|
+
if (Number.isInteger(pid) && pid > 0 && pid !== process.pid && isAlive(pid)) return pid;
|
|
33600
33640
|
} catch {
|
|
33601
33641
|
}
|
|
33602
33642
|
return null;
|
|
@@ -33974,6 +34014,29 @@ function startControlServer(bus, profileDir2, info) {
|
|
|
33974
34014
|
};
|
|
33975
34015
|
}
|
|
33976
34016
|
|
|
34017
|
+
// src/core/idle-auto-clear.ts
|
|
34018
|
+
function startIdleAutoClear(bus, idleMinutes) {
|
|
34019
|
+
const idleMs = Math.max(0, idleMinutes) * 6e4;
|
|
34020
|
+
if (idleMs <= 0) return () => {
|
|
34021
|
+
};
|
|
34022
|
+
let lastActivity = Date.now();
|
|
34023
|
+
const off = bus.onTranscript(() => {
|
|
34024
|
+
lastActivity = Date.now();
|
|
34025
|
+
});
|
|
34026
|
+
const timer = setInterval(() => {
|
|
34027
|
+
if (bus.busy !== null || bus.snapshot().length === 0) return;
|
|
34028
|
+
if (Date.now() - lastActivity < idleMs) return;
|
|
34029
|
+
bus.clear();
|
|
34030
|
+
bus.log(`Transcript auto-cleared after ${Math.round(idleMs / 6e4)} min idle.`);
|
|
34031
|
+
lastActivity = Date.now();
|
|
34032
|
+
}, 6e4);
|
|
34033
|
+
timer.unref?.();
|
|
34034
|
+
return () => {
|
|
34035
|
+
off();
|
|
34036
|
+
clearInterval(timer);
|
|
34037
|
+
};
|
|
34038
|
+
}
|
|
34039
|
+
|
|
33977
34040
|
// src/ui/console-sink.ts
|
|
33978
34041
|
function attachConsoleSink(bus) {
|
|
33979
34042
|
bus.onTranscript((entry) => {
|
|
@@ -39889,8 +39952,10 @@ var CONFIG_KEYS = [
|
|
|
39889
39952
|
"physicPath",
|
|
39890
39953
|
"commandHistoryUploadMinutes",
|
|
39891
39954
|
"autoUpdate",
|
|
39892
|
-
"aiEnv"
|
|
39955
|
+
"aiEnv",
|
|
39956
|
+
"aiCredentialMode"
|
|
39893
39957
|
];
|
|
39958
|
+
var AI_CREDENTIAL_MODES = ["in-container", "host-mounted", "host-cli"];
|
|
39894
39959
|
function configPath(profileDir2) {
|
|
39895
39960
|
return join29(profileDir2, "config.json");
|
|
39896
39961
|
}
|
|
@@ -39922,6 +39987,11 @@ function parseConfigAssignment(key, valueTokens) {
|
|
|
39922
39987
|
} catch {
|
|
39923
39988
|
return { error: `aiEnv must be JSON, e.g. {"KEY":"val"}.` };
|
|
39924
39989
|
}
|
|
39990
|
+
case "aiCredentialMode":
|
|
39991
|
+
if (!AI_CREDENTIAL_MODES.includes(value)) {
|
|
39992
|
+
return { error: `Use ${AI_CREDENTIAL_MODES.join("|")}, not "${value}".` };
|
|
39993
|
+
}
|
|
39994
|
+
return { patch: { aiCredentialMode: value } };
|
|
39925
39995
|
default:
|
|
39926
39996
|
return { error: `Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.` };
|
|
39927
39997
|
}
|
|
@@ -40584,6 +40654,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
40584
40654
|
}
|
|
40585
40655
|
const release = () => releaseInstanceLock(profileDir2);
|
|
40586
40656
|
let stopControl = null;
|
|
40657
|
+
let stopIdleClear = null;
|
|
40587
40658
|
process.once("exit", release);
|
|
40588
40659
|
process.once("SIGINT", () => process.exit(0));
|
|
40589
40660
|
process.once("SIGTERM", () => process.exit(0));
|
|
@@ -40641,10 +40712,12 @@ async function runStart(profileDir2, profileName) {
|
|
|
40641
40712
|
await tui;
|
|
40642
40713
|
} else {
|
|
40643
40714
|
attachConsoleSink(bus);
|
|
40715
|
+
stopIdleClear = startIdleAutoClear(bus, config.autoClearIdleMinutes ?? 10);
|
|
40644
40716
|
bus.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
|
|
40645
40717
|
await runResilient();
|
|
40646
40718
|
}
|
|
40647
40719
|
} finally {
|
|
40720
|
+
stopIdleClear?.();
|
|
40648
40721
|
stopControl?.();
|
|
40649
40722
|
release();
|
|
40650
40723
|
}
|
|
@@ -40781,6 +40854,59 @@ async function runAttach(profileDir2, profileName) {
|
|
|
40781
40854
|
}
|
|
40782
40855
|
}
|
|
40783
40856
|
|
|
40857
|
+
// src/commands/ai-login.ts
|
|
40858
|
+
import { spawn as spawn9 } from "child_process";
|
|
40859
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
40860
|
+
import { basename as basename5 } from "path";
|
|
40861
|
+
function runProviderLogin(cmd, envVar, dir2) {
|
|
40862
|
+
return new Promise((resolve8) => {
|
|
40863
|
+
const child = spawn9(cmd, [], {
|
|
40864
|
+
stdio: "inherit",
|
|
40865
|
+
env: { ...process.env, [envVar]: dir2 }
|
|
40866
|
+
});
|
|
40867
|
+
child.on("error", (err) => {
|
|
40868
|
+
console.error(` \u2716 Could not launch \`${cmd}\` \u2014 is it installed? (${err.message})`);
|
|
40869
|
+
resolve8(1);
|
|
40870
|
+
});
|
|
40871
|
+
child.on("exit", (code) => resolve8(code ?? 0));
|
|
40872
|
+
});
|
|
40873
|
+
}
|
|
40874
|
+
async function runAiLogin(profileDir2, profileName, aiFilter) {
|
|
40875
|
+
const config = readProfileConfig(profileDir2);
|
|
40876
|
+
const credentials = (config.aiProfiles ?? []).filter(isUsableCredential);
|
|
40877
|
+
if (credentials.length === 0) {
|
|
40878
|
+
console.log(
|
|
40879
|
+
`Profile "${profileName}" has no usable AI profiles configured \u2014 add one in the web Worker configs or config.json.`
|
|
40880
|
+
);
|
|
40881
|
+
return;
|
|
40882
|
+
}
|
|
40883
|
+
const selected = aiFilter ? credentials.filter(
|
|
40884
|
+
(c, i) => c.profile === aiFilter || basename5(resolveHomePath(c.profile)) === aiFilter || String(i + 1) === aiFilter
|
|
40885
|
+
) : credentials;
|
|
40886
|
+
if (selected.length === 0) {
|
|
40887
|
+
console.log(`No AI profile matches --ai "${aiFilter}". Available: ${credentials.map((c) => c.profile).join(", ")}`);
|
|
40888
|
+
return;
|
|
40889
|
+
}
|
|
40890
|
+
for (const cred of selected) {
|
|
40891
|
+
const envVar = profileEnvVar(cred.provider);
|
|
40892
|
+
if (!envVar) {
|
|
40893
|
+
console.log(`- Skipping ${cred.provider} "${cred.profile}" \u2014 no known credential dir for this provider.`);
|
|
40894
|
+
continue;
|
|
40895
|
+
}
|
|
40896
|
+
const dir2 = resolveHomePath(cred.profile);
|
|
40897
|
+
mkdirSync6(dir2, { recursive: true });
|
|
40898
|
+
console.log(`
|
|
40899
|
+
\u25B6 Logging in ${cred.provider} \xB7 ${cred.profile} (${envVar}=${dir2})`);
|
|
40900
|
+
console.log(` Complete the provider's own sign-in (claude: type \`/login\`; codex: follow its prompt), then exit.`);
|
|
40901
|
+
const code = await runProviderLogin(cred.provider, envVar, dir2);
|
|
40902
|
+
console.log(code === 0 ? ` \u2714 ${cred.provider} "${cred.profile}" session updated.` : ` \u26A0 ${cred.provider} exited with code ${code}.`);
|
|
40903
|
+
}
|
|
40904
|
+
console.log(
|
|
40905
|
+
`
|
|
40906
|
+
Tip: for the credentials to survive \`docker rm\`/recreate, keep each profile path under the mounted volume (e.g. \`~/.4pm/ai/<name>\`) \u2014 ADR-0199.`
|
|
40907
|
+
);
|
|
40908
|
+
}
|
|
40909
|
+
|
|
40784
40910
|
// src/commands/unlink.ts
|
|
40785
40911
|
import { rmSync as rmSync8 } from "fs";
|
|
40786
40912
|
async function runUnlink(profileDir2, profileName) {
|
|
@@ -40959,6 +41085,15 @@ async function main() {
|
|
|
40959
41085
|
await runAttach(profileDir(name), name);
|
|
40960
41086
|
break;
|
|
40961
41087
|
}
|
|
41088
|
+
case "ai-login": {
|
|
41089
|
+
const name = await pickLinkedProfile(explicitProfile, "ai-login");
|
|
41090
|
+
if (!name) {
|
|
41091
|
+
process.exitCode = 1;
|
|
41092
|
+
break;
|
|
41093
|
+
}
|
|
41094
|
+
await runAiLogin(profileDir(name), name, flagValue(rest, "ai") ?? null);
|
|
41095
|
+
break;
|
|
41096
|
+
}
|
|
40962
41097
|
case "unlink": {
|
|
40963
41098
|
const name = await pickLinkedProfile(explicitProfile, "unlink");
|
|
40964
41099
|
if (!name) {
|
|
@@ -40990,6 +41125,7 @@ async function main() {
|
|
|
40990
41125
|
" 4pm link Pair with the server (pairing \u2192 confirmation code)",
|
|
40991
41126
|
" 4pm start Connect WS, receive commands from the server",
|
|
40992
41127
|
" 4pm attach Open the TUI against a running headless daemon (ADR-0192)",
|
|
41128
|
+
" 4pm ai-login Log into a profile's AI CLI (claude/codex) in place (ADR-0199)",
|
|
40993
41129
|
" 4pm unlink Delete a link (pick a profile if several)",
|
|
40994
41130
|
" 4pm version Show the installed version (+ latest from the server)",
|
|
40995
41131
|
" 4pm update Update to the latest version now",
|