@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.
Files changed (2) hide show
  1. package/dist/index.js +146 -10
  2. 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
- mkdirSync(join(homedir(), ".4pm"), { recursive: true });
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
- mkdirSync(dir2, { recursive: true });
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("confirmation code: ")).trim();
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 resolved = config.aiProfiles.filter(isUsableCredential).map((cred) => {
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 ${plan.cmd} profile "${label}" (${index + 1}/${total})\u2026`;
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "1.2.4",
3
+ "version": "1.2.7",
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",