@4pm/cli 1.2.6 → 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 +99 -1
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28310,6 +28310,8 @@ function defaultProfileConfig() {
28310
28310
  { provider: "codex", profile: ".codex", args: [], model: "" }
28311
28311
  ],
28312
28312
  aiFailoverMode: "remember",
28313
+ // Default to the fully-isolated posture (ADR-0199) — auth lives inside the sandbox.
28314
+ aiCredentialMode: "in-container",
28313
28315
  autoUpdate: true,
28314
28316
  commandHistoryUploadMinutes: 10,
28315
28317
  autoClearIdleMinutes: 10
@@ -34012,6 +34014,29 @@ function startControlServer(bus, profileDir2, info) {
34012
34014
  };
34013
34015
  }
34014
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
+
34015
34040
  // src/ui/console-sink.ts
34016
34041
  function attachConsoleSink(bus) {
34017
34042
  bus.onTranscript((entry) => {
@@ -39927,8 +39952,10 @@ var CONFIG_KEYS = [
39927
39952
  "physicPath",
39928
39953
  "commandHistoryUploadMinutes",
39929
39954
  "autoUpdate",
39930
- "aiEnv"
39955
+ "aiEnv",
39956
+ "aiCredentialMode"
39931
39957
  ];
39958
+ var AI_CREDENTIAL_MODES = ["in-container", "host-mounted", "host-cli"];
39932
39959
  function configPath(profileDir2) {
39933
39960
  return join29(profileDir2, "config.json");
39934
39961
  }
@@ -39960,6 +39987,11 @@ function parseConfigAssignment(key, valueTokens) {
39960
39987
  } catch {
39961
39988
  return { error: `aiEnv must be JSON, e.g. {"KEY":"val"}.` };
39962
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 } };
39963
39995
  default:
39964
39996
  return { error: `Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.` };
39965
39997
  }
@@ -40622,6 +40654,7 @@ async function runStart(profileDir2, profileName) {
40622
40654
  }
40623
40655
  const release = () => releaseInstanceLock(profileDir2);
40624
40656
  let stopControl = null;
40657
+ let stopIdleClear = null;
40625
40658
  process.once("exit", release);
40626
40659
  process.once("SIGINT", () => process.exit(0));
40627
40660
  process.once("SIGTERM", () => process.exit(0));
@@ -40679,10 +40712,12 @@ async function runStart(profileDir2, profileName) {
40679
40712
  await tui;
40680
40713
  } else {
40681
40714
  attachConsoleSink(bus);
40715
+ stopIdleClear = startIdleAutoClear(bus, config.autoClearIdleMinutes ?? 10);
40682
40716
  bus.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
40683
40717
  await runResilient();
40684
40718
  }
40685
40719
  } finally {
40720
+ stopIdleClear?.();
40686
40721
  stopControl?.();
40687
40722
  release();
40688
40723
  }
@@ -40819,6 +40854,59 @@ async function runAttach(profileDir2, profileName) {
40819
40854
  }
40820
40855
  }
40821
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
+
40822
40910
  // src/commands/unlink.ts
40823
40911
  import { rmSync as rmSync8 } from "fs";
40824
40912
  async function runUnlink(profileDir2, profileName) {
@@ -40997,6 +41085,15 @@ async function main() {
40997
41085
  await runAttach(profileDir(name), name);
40998
41086
  break;
40999
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
+ }
41000
41097
  case "unlink": {
41001
41098
  const name = await pickLinkedProfile(explicitProfile, "unlink");
41002
41099
  if (!name) {
@@ -41028,6 +41125,7 @@ async function main() {
41028
41125
  " 4pm link Pair with the server (pairing \u2192 confirmation code)",
41029
41126
  " 4pm start Connect WS, receive commands from the server",
41030
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)",
41031
41129
  " 4pm unlink Delete a link (pick a profile if several)",
41032
41130
  " 4pm version Show the installed version (+ latest from the server)",
41033
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.6",
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",