@4pm/cli 0.4.1 → 0.4.2

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 +385 -183
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1345,7 +1345,7 @@ var require_react_development = __commonJS({
1345
1345
  }
1346
1346
  return dispatcher.useContext(Context);
1347
1347
  }
1348
- function useState5(initialState) {
1348
+ function useState6(initialState) {
1349
1349
  var dispatcher = resolveDispatcher();
1350
1350
  return dispatcher.useState(initialState);
1351
1351
  }
@@ -1373,7 +1373,7 @@ var require_react_development = __commonJS({
1373
1373
  var dispatcher = resolveDispatcher();
1374
1374
  return dispatcher.useCallback(callback, deps);
1375
1375
  }
1376
- function useMemo3(create2, deps) {
1376
+ function useMemo4(create2, deps) {
1377
1377
  var dispatcher = resolveDispatcher();
1378
1378
  return dispatcher.useMemo(create2, deps);
1379
1379
  }
@@ -2145,10 +2145,10 @@ var require_react_development = __commonJS({
2145
2145
  exports.useImperativeHandle = useImperativeHandle;
2146
2146
  exports.useInsertionEffect = useInsertionEffect;
2147
2147
  exports.useLayoutEffect = useLayoutEffect2;
2148
- exports.useMemo = useMemo3;
2148
+ exports.useMemo = useMemo4;
2149
2149
  exports.useReducer = useReducer;
2150
2150
  exports.useRef = useRef;
2151
- exports.useState = useState5;
2151
+ exports.useState = useState6;
2152
2152
  exports.useSyncExternalStore = useSyncExternalStore;
2153
2153
  exports.useTransition = useTransition;
2154
2154
  exports.version = ReactVersion;
@@ -7843,9 +7843,9 @@ var require_react_reconciler_development = __commonJS({
7843
7843
  module.exports = function $$$reconciler($$$hostConfig) {
7844
7844
  var exports2 = {};
7845
7845
  "use strict";
7846
- var React16 = require_react();
7846
+ var React17 = require_react();
7847
7847
  var Scheduler = require_scheduler();
7848
- var ReactSharedInternals = React16.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
7848
+ var ReactSharedInternals = React17.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
7849
7849
  var suppressWarning = false;
7850
7850
  function setSuppressWarning(newSuppressWarning) {
7851
7851
  {
@@ -23490,7 +23490,7 @@ var require_react_jsx_runtime_development = __commonJS({
23490
23490
  if (process.env.NODE_ENV !== "production") {
23491
23491
  (function() {
23492
23492
  "use strict";
23493
- var React16 = require_react();
23493
+ var React17 = require_react();
23494
23494
  var REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.element");
23495
23495
  var REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for("react.portal");
23496
23496
  var REACT_FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for("react.fragment");
@@ -23516,7 +23516,7 @@ var require_react_jsx_runtime_development = __commonJS({
23516
23516
  }
23517
23517
  return null;
23518
23518
  }
23519
- var ReactSharedInternals = React16.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
23519
+ var ReactSharedInternals = React17.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
23520
23520
  function error(format) {
23521
23521
  {
23522
23522
  {
@@ -24366,11 +24366,11 @@ var require_react_jsx_runtime_development = __commonJS({
24366
24366
  return jsxWithValidation(type, props, key, false);
24367
24367
  }
24368
24368
  }
24369
- var jsx7 = jsxWithValidationDynamic;
24370
- var jsxs5 = jsxWithValidationStatic;
24369
+ var jsx8 = jsxWithValidationDynamic;
24370
+ var jsxs6 = jsxWithValidationStatic;
24371
24371
  exports.Fragment = REACT_FRAGMENT_TYPE;
24372
- exports.jsx = jsx7;
24373
- exports.jsxs = jsxs5;
24372
+ exports.jsx = jsx8;
24373
+ exports.jsxs = jsxs6;
24374
24374
  })();
24375
24375
  }
24376
24376
  }
@@ -24404,7 +24404,14 @@ import { homedir as homedir2 } from "os";
24404
24404
  import { join as join2 } from "path";
24405
24405
 
24406
24406
  // src/config/profile.ts
24407
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
24407
+ import {
24408
+ existsSync,
24409
+ mkdirSync,
24410
+ readdirSync,
24411
+ readFileSync,
24412
+ rmSync,
24413
+ writeFileSync
24414
+ } from "fs";
24408
24415
  import { homedir } from "os";
24409
24416
  import { join } from "path";
24410
24417
  var DEFAULT_PROFILE = "default";
@@ -24443,6 +24450,18 @@ function resolveProfileArg(args) {
24443
24450
  }
24444
24451
  return [process.env.FOURPM_PROFILE ?? null, args];
24445
24452
  }
24453
+ function listProfiles() {
24454
+ const base = join(homedir(), ".4pm", "profiles");
24455
+ if (!existsSync(base)) return [];
24456
+ try {
24457
+ return readdirSync(base, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => ({
24458
+ name: d.name,
24459
+ linked: existsSync(join(base, d.name, "credential.cre"))
24460
+ })).sort((a, b) => a.name.localeCompare(b.name));
24461
+ } catch {
24462
+ return [];
24463
+ }
24464
+ }
24446
24465
  function profileDir(name) {
24447
24466
  const dir = join(homedir(), ".4pm", "profiles", name);
24448
24467
  mkdirSync(dir, { recursive: true });
@@ -24590,15 +24609,10 @@ async function fetchCliVersion(serverUrl) {
24590
24609
  }
24591
24610
 
24592
24611
  // src/commands/link.ts
24612
+ function sanitizeProfileName(name) {
24613
+ return name.replace(/[^a-zA-Z0-9._-]/g, "_") || "profile";
24614
+ }
24593
24615
  async function runLink(serverUrl, explicitProfile) {
24594
- const currentName = explicitProfile ?? defaultProfileName();
24595
- if (readCredential(profileDir(currentName), currentName)) {
24596
- const flag = explicitProfile ? ` --profile ${explicitProfile}` : "";
24597
- console.log(
24598
- `Profile "${currentName}" is already linked (.cre). Run \`4pm unlink${flag}\` first if you want to pair again.`
24599
- );
24600
- return;
24601
- }
24602
24616
  const hashcode1 = randomBytes(32).toString("hex");
24603
24617
  console.log(`\u2500\u2500 4PM cli pairing (profile: ${explicitProfile ?? "default"}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
24604
24618
  console.log("1. Open the 4PM web \u2192 /machines, enter hashcode (1):");
@@ -24609,12 +24623,13 @@ async function runLink(serverUrl, explicitProfile) {
24609
24623
  const rl = createInterface({ input: process.stdin, output: process.stdout });
24610
24624
  const hashcode2 = (await rl.question("hashcode (2): ")).trim();
24611
24625
  rl.close();
24612
- const { hashcode3, userId, username, scope } = await confirmPairing(
24626
+ const { hashcode3, username, scope } = await confirmPairing(
24613
24627
  serverUrl,
24614
24628
  hashcode2,
24615
24629
  collectFingerprint()
24616
24630
  );
24617
- const profileName = explicitProfile ?? userId;
24631
+ const profileName = explicitProfile ?? sanitizeProfileName(username);
24632
+ const existed = readCredential(profileDir(profileName), profileName) != null;
24618
24633
  writeCredential(profileDir(profileName), {
24619
24634
  serverUrl,
24620
24635
  hashcode3,
@@ -24622,10 +24637,16 @@ async function runLink(serverUrl, explicitProfile) {
24622
24637
  // decided by the server from the pairing user (ADR-0010); shown in the TUI
24623
24638
  pairedAt: (/* @__PURE__ */ new Date()).toISOString()
24624
24639
  });
24625
- if (!explicitProfile) writeDefaultProfile(userId);
24640
+ writeDefaultProfile(profileName);
24641
+ const verb = existed ? "Renewed" : "Added";
24626
24642
  console.log(
24627
- `\u2714 Linked successfully as "${username}" \u2014 saved .cre. Run \`4pm start\` to connect.`
24643
+ `\u2714 ${verb} profile "${profileName}" as "${username}" \xB7 scope: ${scope} \u2014 saved .cre. Run \`4pm start\` to connect.`
24628
24644
  );
24645
+ if (scope === "orchestrator") {
24646
+ console.log(
24647
+ " (This is the orchestrator/admin link. To pair a worker instead, pick the MACHINE user in the web pairing wizard before entering hashcode (1).)"
24648
+ );
24649
+ }
24629
24650
  }
24630
24651
 
24631
24652
  // src/commands/start.ts
@@ -24801,6 +24822,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
24801
24822
 
24802
24823
  // src/core/ws-client.ts
24803
24824
  import { randomUUID } from "crypto";
24825
+ import { basename as basename3 } from "path";
24804
24826
 
24805
24827
  // ../../22-packages/11-shared/51-utils/src/helpers.ts
24806
24828
  function sleep(ms) {
@@ -25375,6 +25397,12 @@ function profileEnvVar(cmd) {
25375
25397
  if (cmd.includes("codex")) return "CODEX_HOME";
25376
25398
  return null;
25377
25399
  }
25400
+ function profileLabels(config) {
25401
+ const cmd = config.aiCli ?? DEFAULT_AI_CLI;
25402
+ const envVar = profileEnvVar(cmd);
25403
+ const dirs = envVar === "CLAUDE_CONFIG_DIR" ? resolveDirs(config.claudeConfigDir) : envVar === "CODEX_HOME" ? resolveDirs(config.codexHome) : [];
25404
+ return dirs.map((dir) => basename(dir));
25405
+ }
25378
25406
  function planAiRun(prompt, config, workingDir) {
25379
25407
  const cmd = config.aiCli ?? DEFAULT_AI_CLI;
25380
25408
  const args = [...config.aiArgs ?? DEFAULT_AI_ARGS[cmd] ?? [], prompt];
@@ -25858,6 +25886,13 @@ var WsClient = class {
25858
25886
  this.bus.setStatus("connecting");
25859
25887
  const token = await requestWsToken(this.credential.serverUrl, this.credential.hashcode3);
25860
25888
  this.wsToken = token.wsToken;
25889
+ if (token.scope && token.scope !== this.credential.scope) {
25890
+ this.credential.scope = token.scope;
25891
+ writeCredential(this.context.profileDir, this.credential);
25892
+ this.bus.setScope(token.scope);
25893
+ } else if (token.scope) {
25894
+ this.bus.setScope(token.scope);
25895
+ }
25861
25896
  const wsUrl = this.credential.serverUrl.replace(/^http/, "ws") + "/ws";
25862
25897
  const socket = new wrapper_default(wsUrl);
25863
25898
  this.socket = socket;
@@ -26088,7 +26123,10 @@ var WsClient = class {
26088
26123
  })
26089
26124
  });
26090
26125
  this.bus.endBusy(plan.cmd);
26091
- if (result.workedDir) setWorkingProfile(this.context.profileDir, plan.cmd, result.workedDir);
26126
+ if (result.workedDir) {
26127
+ setWorkingProfile(this.context.profileDir, plan.cmd, result.workedDir);
26128
+ this.bus.setActiveProfile(basename3(result.workedDir));
26129
+ }
26092
26130
  this.send(WsChannels.COMMAND_OUTPUT, {
26093
26131
  commandId,
26094
26132
  seq: seq++,
@@ -26265,6 +26303,9 @@ function releaseInstanceLock(profileDir2) {
26265
26303
  }
26266
26304
  }
26267
26305
 
26306
+ // src/commands/start.ts
26307
+ import { basename as basename4 } from "path";
26308
+
26268
26309
  // src/core/session-bus.ts
26269
26310
  import { EventEmitter } from "events";
26270
26311
  import { randomUUID as randomUUID2 } from "crypto";
@@ -26276,6 +26317,10 @@ var SessionBus = class {
26276
26317
  history = [];
26277
26318
  /** Labels of in-flight commands (tool names) — top = the one shown as "processing". */
26278
26319
  busyStack = [];
26320
+ /** The AI-CLI profile that last authenticated (shown in the header). */
26321
+ currentActiveProfile = null;
26322
+ /** Link scope learned from the server at connect time (heals the header/.cre). */
26323
+ currentScope = null;
26279
26324
  constructor() {
26280
26325
  this.emitter.setMaxListeners(50);
26281
26326
  }
@@ -26329,6 +26374,34 @@ var SessionBus = class {
26329
26374
  this.emitter.on("busy", fn);
26330
26375
  return () => this.emitter.off("busy", fn);
26331
26376
  }
26377
+ /** The AI-CLI profile currently in use (last authenticated), or null. */
26378
+ get activeProfile() {
26379
+ return this.currentActiveProfile;
26380
+ }
26381
+ /** Set the active AI-CLI profile (shown in the header). */
26382
+ setActiveProfile(label) {
26383
+ this.currentActiveProfile = label;
26384
+ this.emitter.emit("active-profile", label);
26385
+ }
26386
+ /** Subscribe to active-profile changes. */
26387
+ onActiveProfile(fn) {
26388
+ this.emitter.on("active-profile", fn);
26389
+ return () => this.emitter.off("active-profile", fn);
26390
+ }
26391
+ /** The link scope learned from the server, or null (fall back to the `.cre` value). */
26392
+ get scope() {
26393
+ return this.currentScope;
26394
+ }
26395
+ /** Set the link scope (learned at connect — heals the header). */
26396
+ setScope(scope) {
26397
+ this.currentScope = scope;
26398
+ this.emitter.emit("scope", scope);
26399
+ }
26400
+ /** Subscribe to scope changes. */
26401
+ onScope(fn) {
26402
+ this.emitter.on("scope", fn);
26403
+ return () => this.emitter.off("scope", fn);
26404
+ }
26332
26405
  /**
26333
26406
  * Subscribe to transcript entries. Any entries emitted before this subscription
26334
26407
  * are replayed first (so a consumer that mounts a tick late — e.g. React effects —
@@ -31691,25 +31764,6 @@ var getInstance = (stdout, createInstance) => {
31691
31764
 
31692
31765
  // ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/components/Static.js
31693
31766
  var import_react11 = __toESM(require_react(), 1);
31694
- function Static(props) {
31695
- const { items, children: render2, style: customStyle } = props;
31696
- const [index, setIndex] = (0, import_react11.useState)(0);
31697
- const itemsToRender = (0, import_react11.useMemo)(() => {
31698
- return items.slice(index);
31699
- }, [items, index]);
31700
- (0, import_react11.useLayoutEffect)(() => {
31701
- setIndex(items.length);
31702
- }, [items.length]);
31703
- const children = itemsToRender.map((item, itemIndex) => {
31704
- return render2(item, index + itemIndex);
31705
- });
31706
- const style = (0, import_react11.useMemo)(() => ({
31707
- position: "absolute",
31708
- flexDirection: "column",
31709
- ...customStyle
31710
- }), [customStyle]);
31711
- return import_react11.default.createElement("ink-box", { internal_static: true, style }, children);
31712
- }
31713
31767
 
31714
31768
  // ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/components/Transform.js
31715
31769
  var import_react12 = __toESM(require_react(), 1);
@@ -32035,12 +32089,22 @@ function appendInputHistory(profileDir2, line, current) {
32035
32089
  // src/ui/banner.tsx
32036
32090
  var import_react22 = __toESM(require_react(), 1);
32037
32091
  var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
32092
+ function dotColor(status) {
32093
+ if (status === "connected") return "green";
32094
+ if (status === "stopped") return "red";
32095
+ return "yellow";
32096
+ }
32038
32097
  function scopeLabel(scope) {
32039
32098
  if (scope === "project") return "project (MACHINE)";
32040
32099
  if (scope === "orchestrator") return "orchestrator (root)";
32041
32100
  return scope;
32042
32101
  }
32043
- function Banner({ info }) {
32102
+ function Banner({
32103
+ info,
32104
+ scope,
32105
+ status,
32106
+ activeProfile
32107
+ }) {
32044
32108
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { flexDirection: "row", paddingX: 1, paddingTop: 1, children: [
32045
32109
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { marginRight: 2, flexDirection: "column", children: [
32046
32110
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "cyan", bold: true, children: "\u250C\u2500\u2510" }),
@@ -32057,20 +32121,32 @@ function Banner({ info }) {
32057
32121
  ] }),
32058
32122
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { dimColor: true, children: [
32059
32123
  "scope: ",
32060
- scopeLabel(info.scope),
32124
+ scopeLabel(scope),
32061
32125
  " \xB7 profile: ",
32062
32126
  info.profile
32063
32127
  ] }),
32064
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { dimColor: true, children: [
32128
+ scope === "orchestrator" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { dimColor: true, children: [
32065
32129
  "server: ",
32066
- info.serverUrl
32067
- ] }),
32068
- info.scope === "orchestrator" ? (
32069
- // Orchestrator cli is not tied to a physic project (ADR-0010) — no serving line.
32070
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { dimColor: true, children: "mode: orchestrator (short-lived AI requests)" })
32071
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { dimColor: true, children: [
32072
- "serving: ",
32130
+ info.serverUrl,
32131
+ " \xB7 mode: orchestrator (short-lived AI)"
32132
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { dimColor: true, children: [
32133
+ "server: ",
32134
+ info.serverUrl,
32135
+ " \xB7 serving: ",
32073
32136
  info.physicPath ?? "(no physic project)"
32137
+ ] }),
32138
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
32139
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: dotColor(status), children: "\u25CF" }),
32140
+ " ",
32141
+ status,
32142
+ " \xB7 using",
32143
+ " ",
32144
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "cyan", children: info.aiCli }),
32145
+ activeProfile ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
32146
+ " ",
32147
+ "\xB7 profile ",
32148
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "cyan", children: activeProfile })
32149
+ ] }) : null
32074
32150
  ] })
32075
32151
  ] })
32076
32152
  ] });
@@ -32123,93 +32199,6 @@ function TranscriptLine({ line }) {
32123
32199
 
32124
32200
  // src/ui/command-input.tsx
32125
32201
  var import_react24 = __toESM(require_react(), 1);
32126
- var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
32127
- function dotColor(status) {
32128
- if (status === "connected") return "green";
32129
- if (status === "stopped") return "red";
32130
- return "yellow";
32131
- }
32132
- function StatusHint({
32133
- status,
32134
- aiCli: aiCli2
32135
- }) {
32136
- const connected = status === "connected";
32137
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { dimColor: true, children: [
32138
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { color: dotColor(status), children: "\u25CF" }),
32139
- " ",
32140
- status,
32141
- connected ? ` \xB7 prompt \u2192 ${aiCli2} \xB7 /help \xB7 \u2191\u2193 history \xB7 Ctrl+C quit` : " \xB7 waiting for connection\u2026"
32142
- ] });
32143
- }
32144
- function CommandInput({
32145
- status,
32146
- history,
32147
- onSubmit
32148
- }) {
32149
- const { exit } = use_app_default();
32150
- const [value, setValue] = (0, import_react24.useState)("");
32151
- const [navIndex, setNavIndex] = (0, import_react24.useState)(null);
32152
- const connected = status === "connected";
32153
- use_input_default((input, key) => {
32154
- if (key.ctrl && (input === "c" || input === "d")) {
32155
- exit();
32156
- return;
32157
- }
32158
- if (key.upArrow) {
32159
- if (history.length === 0) return;
32160
- const next = navIndex === null ? history.length - 1 : Math.max(0, navIndex - 1);
32161
- setNavIndex(next);
32162
- setValue(history[next] ?? "");
32163
- return;
32164
- }
32165
- if (key.downArrow) {
32166
- if (navIndex === null) return;
32167
- const next = navIndex + 1;
32168
- if (next >= history.length) {
32169
- setNavIndex(null);
32170
- setValue("");
32171
- } else {
32172
- setNavIndex(next);
32173
- setValue(history[next] ?? "");
32174
- }
32175
- return;
32176
- }
32177
- if (key.return) {
32178
- const trimmed = value.trim();
32179
- if (trimmed.length > 0) onSubmit(trimmed);
32180
- setValue("");
32181
- setNavIndex(null);
32182
- return;
32183
- }
32184
- if (key.backspace || key.delete) {
32185
- setValue((v) => v.slice(0, -1));
32186
- setNavIndex(null);
32187
- return;
32188
- }
32189
- if (!key.ctrl && !key.meta && input) {
32190
- setValue((v) => v + input);
32191
- setNavIndex(null);
32192
- }
32193
- });
32194
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Box_default, { children: [
32195
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { color: connected ? "cyan" : "gray", children: "\u203A " }),
32196
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { children: value }),
32197
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { inverse: true, children: " " })
32198
- ] });
32199
- }
32200
-
32201
- // src/ui/spinner.tsx
32202
- var import_react25 = __toESM(require_react(), 1);
32203
- var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1);
32204
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
32205
- function Spinner() {
32206
- const [index, setIndex] = (0, import_react25.useState)(0);
32207
- (0, import_react25.useEffect)(() => {
32208
- const timer = setInterval(() => setIndex((i) => (i + 1) % FRAMES.length), 90);
32209
- return () => clearInterval(timer);
32210
- }, []);
32211
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", children: FRAMES[index] });
32212
- }
32213
32202
 
32214
32203
  // src/ui/slash-commands.ts
32215
32204
  import { existsSync as existsSync9 } from "fs";
@@ -32354,6 +32343,19 @@ var COMMANDS = [
32354
32343
  description: "View / init / update / delete profile config",
32355
32344
  run: runConfig
32356
32345
  },
32346
+ {
32347
+ name: "claude-cmd",
32348
+ usage: "/claude-cmd /context",
32349
+ description: "Run an AI-CLI slash command (e.g. /context, /usage, /status)",
32350
+ run: (ctx) => {
32351
+ const rest = ctx.args.join(" ").trim();
32352
+ if (!rest) {
32353
+ ctx.print("usage: /claude-cmd /context (forwards to the AI CLI)", "error");
32354
+ return;
32355
+ }
32356
+ ctx.submitAi(rest);
32357
+ }
32358
+ },
32357
32359
  {
32358
32360
  name: "clear",
32359
32361
  usage: "/clear",
@@ -32368,6 +32370,7 @@ var COMMANDS = [
32368
32370
  }
32369
32371
  ];
32370
32372
  var ALIASES = { exit: "quit" };
32373
+ var SLASH_COMMANDS = COMMANDS.map((c) => ({ name: c.name, usage: c.usage, description: c.description }));
32371
32374
  function runSlashCommand(line, base) {
32372
32375
  const tokens = line.replace(/^\//, "").trim().split(/\s+/);
32373
32376
  const rawName = tokens[0] ?? "";
@@ -32381,10 +32384,121 @@ function runSlashCommand(line, base) {
32381
32384
  command.run(ctx);
32382
32385
  }
32383
32386
 
32387
+ // src/ui/command-input.tsx
32388
+ var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
32389
+ function Hint() {
32390
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: " /help \xB7 Tab complete \xB7 \u2191\u2193 history \xB7 PgUp/PgDn scroll \xB7 Ctrl+C quit" });
32391
+ }
32392
+ function CommandInput({
32393
+ status,
32394
+ history,
32395
+ onSubmit
32396
+ }) {
32397
+ const { exit } = use_app_default();
32398
+ const [value, setValue] = (0, import_react24.useState)("");
32399
+ const [navIndex, setNavIndex] = (0, import_react24.useState)(null);
32400
+ const [sugIndex, setSugIndex] = (0, import_react24.useState)(0);
32401
+ const connected = status === "connected";
32402
+ const matching = value.startsWith("/") && !value.includes(" ") ? SLASH_COMMANDS.filter((c) => c.name.startsWith(value.slice(1))) : [];
32403
+ const menuOpen = matching.length > 0;
32404
+ const selected = matching[Math.min(sugIndex, matching.length - 1)];
32405
+ use_input_default((input, key) => {
32406
+ if (key.ctrl && (input === "c" || input === "d")) {
32407
+ exit();
32408
+ return;
32409
+ }
32410
+ if (key.tab && selected) {
32411
+ setValue(`/${selected.name} `);
32412
+ setSugIndex(0);
32413
+ setNavIndex(null);
32414
+ return;
32415
+ }
32416
+ if (key.upArrow) {
32417
+ if (menuOpen) {
32418
+ setSugIndex((i) => (i - 1 + matching.length) % matching.length);
32419
+ return;
32420
+ }
32421
+ if (history.length === 0) return;
32422
+ const next = navIndex === null ? history.length - 1 : Math.max(0, navIndex - 1);
32423
+ setNavIndex(next);
32424
+ setValue(history[next] ?? "");
32425
+ return;
32426
+ }
32427
+ if (key.downArrow) {
32428
+ if (menuOpen) {
32429
+ setSugIndex((i) => (i + 1) % matching.length);
32430
+ return;
32431
+ }
32432
+ if (navIndex === null) return;
32433
+ const next = navIndex + 1;
32434
+ if (next >= history.length) {
32435
+ setNavIndex(null);
32436
+ setValue("");
32437
+ } else {
32438
+ setNavIndex(next);
32439
+ setValue(history[next] ?? "");
32440
+ }
32441
+ return;
32442
+ }
32443
+ if (key.return) {
32444
+ const trimmed = value.trim();
32445
+ if (trimmed.length > 0) onSubmit(trimmed);
32446
+ setValue("");
32447
+ setNavIndex(null);
32448
+ setSugIndex(0);
32449
+ return;
32450
+ }
32451
+ if (key.backspace || key.delete) {
32452
+ setValue((v) => v.slice(0, -1));
32453
+ setNavIndex(null);
32454
+ setSugIndex(0);
32455
+ return;
32456
+ }
32457
+ if (!key.ctrl && !key.meta && input) {
32458
+ setValue((v) => v + input);
32459
+ setNavIndex(null);
32460
+ setSugIndex(0);
32461
+ }
32462
+ });
32463
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Box_default, { flexDirection: "column", children: [
32464
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { children: menuOpen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { children: [
32465
+ matching.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { color: i === sugIndex ? "cyan" : "gray", inverse: i === sugIndex, children: [
32466
+ " ",
32467
+ "/",
32468
+ c.name,
32469
+ " "
32470
+ ] }, c.name)),
32471
+ selected ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { dimColor: true, children: [
32472
+ " \u2014 ",
32473
+ selected.description,
32474
+ " (Tab)"
32475
+ ] }) : null
32476
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { children: " " }) }),
32477
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Box_default, { children: [
32478
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { color: connected ? "cyan" : "gray", children: "\u203A " }),
32479
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { children: value }),
32480
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { inverse: true, children: " " })
32481
+ ] })
32482
+ ] });
32483
+ }
32484
+
32485
+ // src/ui/spinner.tsx
32486
+ var import_react25 = __toESM(require_react(), 1);
32487
+ var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1);
32488
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
32489
+ function Spinner() {
32490
+ const [index, setIndex] = (0, import_react25.useState)(0);
32491
+ (0, import_react25.useEffect)(() => {
32492
+ const timer = setInterval(() => setIndex((i) => (i + 1) % FRAMES.length), 90);
32493
+ return () => clearInterval(timer);
32494
+ }, []);
32495
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", children: FRAMES[index] });
32496
+ }
32497
+
32384
32498
  // src/ui/app.tsx
32385
32499
  var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
32386
- var BANNER_ROWS = 5;
32387
- var COMMAND_BLOCK_ROWS = 6;
32500
+ var CHROME_ROWS = 12;
32501
+ var ENTRY_CAP = 3e3;
32388
32502
  function Rule({ width }) {
32389
32503
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: "\u2500".repeat(Math.max(10, width)) });
32390
32504
  }
@@ -32410,24 +32524,30 @@ function App2({
32410
32524
  const [entries2, setEntries] = (0, import_react26.useState)([]);
32411
32525
  const [status, setStatus] = (0, import_react26.useState)(bus.status);
32412
32526
  const [busy, setBusy] = (0, import_react26.useState)(bus.busy);
32527
+ const [activeProfile, setActiveProfile] = (0, import_react26.useState)(bus.activeProfile);
32528
+ const [scope, setScope] = (0, import_react26.useState)(bus.scope ?? info.scope);
32529
+ const [scrollBack, setScrollBack] = (0, import_react26.useState)(0);
32413
32530
  const [history, setHistory] = (0, import_react26.useState)(() => loadInputHistory(info.profileDir));
32414
- const [staticKey, setStaticKey] = (0, import_react26.useState)(0);
32415
- const clearTranscript = () => {
32416
- setEntries([]);
32417
- setStaticKey((k) => k + 1);
32418
- stdout.write("\x1B[2J\x1B[3J\x1B[H");
32419
- };
32420
32531
  (0, import_react26.useEffect)(() => {
32421
- const offTranscript = bus.onTranscript((entry) => setEntries((prev) => [...prev, entry]));
32532
+ const offTranscript = bus.onTranscript(
32533
+ (entry) => setEntries(
32534
+ (prev) => prev.length >= ENTRY_CAP ? [...prev.slice(prev.length - ENTRY_CAP + 1), entry] : [...prev, entry]
32535
+ )
32536
+ );
32422
32537
  const offStatus = bus.onStatus(setStatus);
32423
32538
  const offBusy = bus.onBusy(setBusy);
32539
+ const offProfile = bus.onActiveProfile(setActiveProfile);
32540
+ const offScope = bus.onScope(setScope);
32424
32541
  return () => {
32425
32542
  offTranscript();
32426
32543
  offStatus();
32427
32544
  offBusy();
32545
+ offProfile();
32546
+ offScope();
32428
32547
  };
32429
32548
  }, [bus]);
32430
32549
  const handleSubmit = (input) => {
32550
+ setScrollBack(0);
32431
32551
  setHistory((prev) => appendInputHistory(info.profileDir, input, prev));
32432
32552
  const trimmed = input.trim();
32433
32553
  if (trimmed.startsWith("/")) {
@@ -32435,36 +32555,47 @@ function App2({
32435
32555
  runSlashCommand(trimmed, {
32436
32556
  info,
32437
32557
  print: (text, level) => bus.push({ source: "local", kind: "log", text, level }),
32438
- clear: clearTranscript,
32439
- quit: () => exit()
32558
+ clear: () => {
32559
+ setEntries([]);
32560
+ setScrollBack(0);
32561
+ stdout.write("\x1B[2J\x1B[3J\x1B[H");
32562
+ },
32563
+ quit: () => exit(),
32564
+ submitAi: (aiInput) => bus.submitLocal(aiInput)
32440
32565
  });
32441
32566
  return;
32442
32567
  }
32443
32568
  bus.submitLocal(input);
32444
32569
  };
32445
- const lines = flattenEntries(entries2);
32446
- const items = [
32447
- { type: "banner" },
32448
- ...lines.map((line) => ({ type: "line", line }))
32449
- ];
32570
+ const bodyHeight = Math.max(3, rows - CHROME_ROWS);
32571
+ const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2), [entries2]);
32572
+ const maxScroll = Math.max(0, lines.length - bodyHeight);
32573
+ const clampedBack = Math.min(scrollBack, maxScroll);
32574
+ const lineRows = clampedBack > 0 ? bodyHeight - 1 : bodyHeight;
32575
+ const start = Math.max(0, lines.length - lineRows - clampedBack);
32576
+ const visible = lines.slice(start, start + lineRows);
32450
32577
  const width = cols - 1;
32451
- const spacer = Math.max(0, rows - (BANNER_ROWS + lines.length) - COMMAND_BLOCK_ROWS);
32452
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Box_default, { flexDirection: "column", children: [
32453
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Static, { items, children: (item, index) => item.type === "banner" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Banner, { info }, "banner") : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TranscriptLine, { line: item.line }, item.line.key || index) }, staticKey),
32454
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Box_default, { flexDirection: "column", children: [
32455
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Box_default, { height: spacer }),
32456
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Box_default, { justifyContent: "flex-end", children: busy ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { color: "yellow", children: [
32457
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Spinner, {}),
32458
- " processing \xB7 ",
32459
- busy,
32460
- "\u2026"
32461
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { children: " " }) }),
32462
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Rule, { width }),
32463
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CommandInput, { status, history, onSubmit: handleSubmit }),
32464
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Rule, { width }),
32465
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(StatusHint, { status, aiCli: info.aiCli }),
32466
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: " tip: mouse wheel = scroll \xB7 drag to select \u2192 right-click to copy" })
32467
- ] })
32578
+ use_input_default((_input, key) => {
32579
+ if (key.pageUp) setScrollBack((b) => Math.min(maxScroll, b + bodyHeight));
32580
+ else if (key.pageDown) setScrollBack((b) => Math.max(0, b - bodyHeight));
32581
+ });
32582
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Box_default, { flexDirection: "column", height: rows, children: [
32583
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Banner, { info, scope, status, activeProfile }),
32584
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Rule, { width }),
32585
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Box_default, { flexDirection: "column", height: bodyHeight, overflow: "hidden", children: [
32586
+ visible.map((line) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TranscriptLine, { line }, line.key)),
32587
+ clampedBack > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: ` \u2191 ${clampedBack} newer line(s) hidden \xB7 PageDown` }) : null
32588
+ ] }),
32589
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Box_default, { justifyContent: "flex-end", children: busy ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { color: "yellow", children: [
32590
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Spinner, {}),
32591
+ " processing \xB7 ",
32592
+ busy,
32593
+ "\u2026"
32594
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { children: " " }) }),
32595
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Rule, { width }),
32596
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CommandInput, { status, history, onSubmit: handleSubmit }),
32597
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Rule, { width }),
32598
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Hint, {})
32468
32599
  ] });
32469
32600
  }
32470
32601
 
@@ -32550,6 +32681,10 @@ async function runStart(profileDir2, profileName) {
32550
32681
  physicPath: config.physicPath ?? null,
32551
32682
  aiCli: config.aiCli ?? "claude"
32552
32683
  };
32684
+ const aiCli2 = config.aiCli ?? "claude";
32685
+ const workingDir = getWorkingProfile(profileDir2, aiCli2);
32686
+ const labels = profileLabels(config);
32687
+ bus.setActiveProfile(workingDir ? basename4(workingDir) : labels[0] ?? null);
32553
32688
  const tui = runTui(bus, info);
32554
32689
  bus.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
32555
32690
  void client.run();
@@ -32616,11 +32751,68 @@ async function runVersion(profileDir2, profileName, serverUrlFlag) {
32616
32751
  }
32617
32752
  }
32618
32753
 
32754
+ // src/ui/select-profile.tsx
32755
+ var import_react28 = __toESM(require_react(), 1);
32756
+ var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
32757
+ function Picker({
32758
+ items,
32759
+ title,
32760
+ onDone
32761
+ }) {
32762
+ const { exit } = use_app_default();
32763
+ const [index, setIndex] = (0, import_react28.useState)(0);
32764
+ use_input_default((input, key) => {
32765
+ if (key.upArrow) setIndex((i) => (i - 1 + items.length) % items.length);
32766
+ else if (key.downArrow) setIndex((i) => (i + 1) % items.length);
32767
+ else if (key.return) {
32768
+ onDone(items[index]?.value ?? null);
32769
+ exit();
32770
+ } else if (key.escape || key.ctrl && input === "c") {
32771
+ onDone(null);
32772
+ exit();
32773
+ }
32774
+ });
32775
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Box_default, { flexDirection: "column", paddingY: 1, children: [
32776
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Text, { bold: true, children: title }),
32777
+ items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Text, { color: i === index ? "cyan" : void 0, children: [
32778
+ i === index ? "\u276F " : " ",
32779
+ item.label
32780
+ ] }, item.value)),
32781
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Text, { dimColor: true, children: "\u2191\u2193 move \xB7 Enter select \xB7 Esc cancel" })
32782
+ ] });
32783
+ }
32784
+ function selectFromList(items, title) {
32785
+ return new Promise((resolve5) => {
32786
+ let chosen = null;
32787
+ const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Picker, { items, title, onDone: (v) => chosen = v }));
32788
+ void app.waitUntilExit().then(() => resolve5(chosen));
32789
+ });
32790
+ }
32791
+
32619
32792
  // src/index.ts
32620
32793
  function flagValue(args, name) {
32621
32794
  const index = args.indexOf(`--${name}`);
32622
32795
  return index >= 0 ? args[index + 1] : void 0;
32623
32796
  }
32797
+ async function pickLinkedProfile(explicit, action) {
32798
+ if (explicit) return explicit;
32799
+ const linked = listProfiles().filter((p) => p.linked);
32800
+ if (linked.length === 0) {
32801
+ console.error("No linked profile \u2014 run `4pm link` first.");
32802
+ return null;
32803
+ }
32804
+ if (linked.length === 1) return linked[0].name;
32805
+ if (!process.stdout.isTTY) {
32806
+ console.error(
32807
+ `Multiple profiles \u2014 pass --profile <name> (or run \`4pm ${action}\` in a terminal to pick).`
32808
+ );
32809
+ return null;
32810
+ }
32811
+ return selectFromList(
32812
+ linked.map((p) => ({ label: p.name, value: p.name })),
32813
+ `Select a profile to ${action}:`
32814
+ );
32815
+ }
32624
32816
  async function main() {
32625
32817
  const [explicitProfile, args] = resolveProfileArg(process.argv.slice(2));
32626
32818
  const [command, ...rest] = args;
@@ -32631,12 +32823,20 @@ async function main() {
32631
32823
  break;
32632
32824
  }
32633
32825
  case "start": {
32634
- const name = explicitProfile ?? defaultProfileName();
32826
+ const name = await pickLinkedProfile(explicitProfile, "start");
32827
+ if (!name) {
32828
+ process.exitCode = 1;
32829
+ break;
32830
+ }
32635
32831
  await runStart(profileDir(name), name);
32636
32832
  break;
32637
32833
  }
32638
32834
  case "unlink": {
32639
- const name = explicitProfile ?? defaultProfileName();
32835
+ const name = await pickLinkedProfile(explicitProfile, "unlink");
32836
+ if (!name) {
32837
+ process.exitCode = 1;
32838
+ break;
32839
+ }
32640
32840
  runUnlink(profileDir(name), name);
32641
32841
  break;
32642
32842
  }
@@ -32659,13 +32859,15 @@ async function main() {
32659
32859
  "4pm \u2014 the 4PM worker-machine CLI",
32660
32860
  "",
32661
32861
  "Usage:",
32662
- " 4pm [--profile <name>] link [--server URL] Pair with the server (hashcode 1\u21922\u21923)",
32663
- " 4pm [--profile <name>] start Connect WS, receive commands from the server",
32664
- " 4pm [--profile <name>] unlink Delete the local credential (.cre)",
32665
- " 4pm version [--server URL] Show the installed version (+ latest from the server)",
32666
- " 4pm update [--server URL] Update to the latest version now",
32862
+ " 4pm link Pair with the server (hashcode 1\u21922\u21923)",
32863
+ " 4pm start Connect WS, receive commands from the server",
32864
+ " 4pm unlink Delete a link (pick a profile if several)",
32865
+ " 4pm version Show the installed version (+ latest from the server)",
32866
+ " 4pm update Update to the latest version now",
32667
32867
  "",
32668
- "Without --profile \u21D2 the `default` profile (ADR-0014)."
32868
+ "Profiles: named after the paired account (e.g. mcacc1); when several exist,",
32869
+ "start/unlink show a picker (\u2191\u2193 + Enter) \u2014 no --profile needed.",
32870
+ "Server URL: from FOURPM_SERVER (default http://localhost:3001)."
32669
32871
  ].join("\n")
32670
32872
  );
32671
32873
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "type": "module",