@4pm/cli 0.4.1 → 0.4.3

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 +408 -184
  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;
@@ -25904,6 +25939,7 @@ var WsClient = class {
25904
25939
  if ("type" in message) {
25905
25940
  if (message.type === "hello_ack" && this.ecdh) {
25906
25941
  this.sessionKey = deriveSessionKey(this.ecdh, message.pubkey);
25942
+ if (message.workerName) this.bus.setWorker(message.workerName);
25907
25943
  logger.info("ws.connected", { version: CLI_VERSION });
25908
25944
  this.bus.setStatus("connected");
25909
25945
  this.bus.log("\u2714 Connected to the server (session encryption ready).");
@@ -26088,7 +26124,10 @@ var WsClient = class {
26088
26124
  })
26089
26125
  });
26090
26126
  this.bus.endBusy(plan.cmd);
26091
- if (result.workedDir) setWorkingProfile(this.context.profileDir, plan.cmd, result.workedDir);
26127
+ if (result.workedDir) {
26128
+ setWorkingProfile(this.context.profileDir, plan.cmd, result.workedDir);
26129
+ this.bus.setActiveProfile(basename3(result.workedDir));
26130
+ }
26092
26131
  this.send(WsChannels.COMMAND_OUTPUT, {
26093
26132
  commandId,
26094
26133
  seq: seq++,
@@ -26265,6 +26304,9 @@ function releaseInstanceLock(profileDir2) {
26265
26304
  }
26266
26305
  }
26267
26306
 
26307
+ // src/commands/start.ts
26308
+ import { basename as basename4 } from "path";
26309
+
26268
26310
  // src/core/session-bus.ts
26269
26311
  import { EventEmitter } from "events";
26270
26312
  import { randomUUID as randomUUID2 } from "crypto";
@@ -26276,6 +26318,12 @@ var SessionBus = class {
26276
26318
  history = [];
26277
26319
  /** Labels of in-flight commands (tool names) — top = the one shown as "processing". */
26278
26320
  busyStack = [];
26321
+ /** The AI-CLI profile that last authenticated (shown in the header). */
26322
+ currentActiveProfile = null;
26323
+ /** Link scope learned from the server at connect time (heals the header/.cre). */
26324
+ currentScope = null;
26325
+ /** Worker (physical machine) name learned from the server at connect time. */
26326
+ currentWorker = null;
26279
26327
  constructor() {
26280
26328
  this.emitter.setMaxListeners(50);
26281
26329
  }
@@ -26329,6 +26377,48 @@ var SessionBus = class {
26329
26377
  this.emitter.on("busy", fn);
26330
26378
  return () => this.emitter.off("busy", fn);
26331
26379
  }
26380
+ /** The AI-CLI profile currently in use (last authenticated), or null. */
26381
+ get activeProfile() {
26382
+ return this.currentActiveProfile;
26383
+ }
26384
+ /** Set the active AI-CLI profile (shown in the header). */
26385
+ setActiveProfile(label) {
26386
+ this.currentActiveProfile = label;
26387
+ this.emitter.emit("active-profile", label);
26388
+ }
26389
+ /** Subscribe to active-profile changes. */
26390
+ onActiveProfile(fn) {
26391
+ this.emitter.on("active-profile", fn);
26392
+ return () => this.emitter.off("active-profile", fn);
26393
+ }
26394
+ /** The link scope learned from the server, or null (fall back to the `.cre` value). */
26395
+ get scope() {
26396
+ return this.currentScope;
26397
+ }
26398
+ /** Set the link scope (learned at connect — heals the header). */
26399
+ setScope(scope) {
26400
+ this.currentScope = scope;
26401
+ this.emitter.emit("scope", scope);
26402
+ }
26403
+ /** Subscribe to scope changes. */
26404
+ onScope(fn) {
26405
+ this.emitter.on("scope", fn);
26406
+ return () => this.emitter.off("scope", fn);
26407
+ }
26408
+ /** The worker (physical machine) name learned from the server at connect, or null. */
26409
+ get worker() {
26410
+ return this.currentWorker;
26411
+ }
26412
+ /** Set the worker name (learned from the hello_ack — heals the header). */
26413
+ setWorker(worker) {
26414
+ this.currentWorker = worker;
26415
+ this.emitter.emit("worker", worker);
26416
+ }
26417
+ /** Subscribe to worker-name changes. */
26418
+ onWorker(fn) {
26419
+ this.emitter.on("worker", fn);
26420
+ return () => this.emitter.off("worker", fn);
26421
+ }
26332
26422
  /**
26333
26423
  * Subscribe to transcript entries. Any entries emitted before this subscription
26334
26424
  * are replayed first (so a consumer that mounts a tick late — e.g. React effects —
@@ -31691,25 +31781,6 @@ var getInstance = (stdout, createInstance) => {
31691
31781
 
31692
31782
  // ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/components/Static.js
31693
31783
  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
31784
 
31714
31785
  // ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/components/Transform.js
31715
31786
  var import_react12 = __toESM(require_react(), 1);
@@ -32035,12 +32106,23 @@ function appendInputHistory(profileDir2, line, current) {
32035
32106
  // src/ui/banner.tsx
32036
32107
  var import_react22 = __toESM(require_react(), 1);
32037
32108
  var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
32109
+ function dotColor(status) {
32110
+ if (status === "connected") return "green";
32111
+ if (status === "stopped") return "red";
32112
+ return "yellow";
32113
+ }
32038
32114
  function scopeLabel(scope) {
32039
32115
  if (scope === "project") return "project (MACHINE)";
32040
32116
  if (scope === "orchestrator") return "orchestrator (root)";
32041
32117
  return scope;
32042
32118
  }
32043
- function Banner({ info }) {
32119
+ function Banner({
32120
+ info,
32121
+ scope,
32122
+ worker,
32123
+ status,
32124
+ activeProfile
32125
+ }) {
32044
32126
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { flexDirection: "row", paddingX: 1, paddingTop: 1, children: [
32045
32127
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { marginRight: 2, flexDirection: "column", children: [
32046
32128
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "cyan", bold: true, children: "\u250C\u2500\u2510" }),
@@ -32057,20 +32139,33 @@ function Banner({ info }) {
32057
32139
  ] }),
32058
32140
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { dimColor: true, children: [
32059
32141
  "scope: ",
32060
- scopeLabel(info.scope),
32142
+ scopeLabel(scope),
32061
32143
  " \xB7 profile: ",
32062
- info.profile
32144
+ info.profile,
32145
+ worker ? ` \xB7 worker: ${worker}` : ""
32063
32146
  ] }),
32064
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { dimColor: true, children: [
32147
+ scope === "orchestrator" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { dimColor: true, children: [
32065
32148
  "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: ",
32149
+ info.serverUrl,
32150
+ " \xB7 mode: orchestrator (short-lived AI)"
32151
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { dimColor: true, children: [
32152
+ "server: ",
32153
+ info.serverUrl,
32154
+ " \xB7 serving: ",
32073
32155
  info.physicPath ?? "(no physic project)"
32156
+ ] }),
32157
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
32158
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: dotColor(status), children: "\u25CF" }),
32159
+ " ",
32160
+ status,
32161
+ " \xB7 using",
32162
+ " ",
32163
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "cyan", children: info.aiCli }),
32164
+ activeProfile ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [
32165
+ " ",
32166
+ "\xB7 profile ",
32167
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "cyan", children: activeProfile })
32168
+ ] }) : null
32074
32169
  ] })
32075
32170
  ] })
32076
32171
  ] });
@@ -32123,93 +32218,6 @@ function TranscriptLine({ line }) {
32123
32218
 
32124
32219
  // src/ui/command-input.tsx
32125
32220
  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
32221
 
32214
32222
  // src/ui/slash-commands.ts
32215
32223
  import { existsSync as existsSync9 } from "fs";
@@ -32354,6 +32362,19 @@ var COMMANDS = [
32354
32362
  description: "View / init / update / delete profile config",
32355
32363
  run: runConfig
32356
32364
  },
32365
+ {
32366
+ name: "claude-cmd",
32367
+ usage: "/claude-cmd /context",
32368
+ description: "Run an AI-CLI slash command (e.g. /context, /usage, /status)",
32369
+ run: (ctx) => {
32370
+ const rest = ctx.args.join(" ").trim();
32371
+ if (!rest) {
32372
+ ctx.print("usage: /claude-cmd /context (forwards to the AI CLI)", "error");
32373
+ return;
32374
+ }
32375
+ ctx.submitAi(rest);
32376
+ }
32377
+ },
32357
32378
  {
32358
32379
  name: "clear",
32359
32380
  usage: "/clear",
@@ -32368,6 +32389,7 @@ var COMMANDS = [
32368
32389
  }
32369
32390
  ];
32370
32391
  var ALIASES = { exit: "quit" };
32392
+ var SLASH_COMMANDS = COMMANDS.map((c) => ({ name: c.name, usage: c.usage, description: c.description }));
32371
32393
  function runSlashCommand(line, base) {
32372
32394
  const tokens = line.replace(/^\//, "").trim().split(/\s+/);
32373
32395
  const rawName = tokens[0] ?? "";
@@ -32381,10 +32403,121 @@ function runSlashCommand(line, base) {
32381
32403
  command.run(ctx);
32382
32404
  }
32383
32405
 
32406
+ // src/ui/command-input.tsx
32407
+ var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
32408
+ function Hint() {
32409
+ 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" });
32410
+ }
32411
+ function CommandInput({
32412
+ status,
32413
+ history,
32414
+ onSubmit
32415
+ }) {
32416
+ const { exit } = use_app_default();
32417
+ const [value, setValue] = (0, import_react24.useState)("");
32418
+ const [navIndex, setNavIndex] = (0, import_react24.useState)(null);
32419
+ const [sugIndex, setSugIndex] = (0, import_react24.useState)(0);
32420
+ const connected = status === "connected";
32421
+ const matching = value.startsWith("/") && !value.includes(" ") ? SLASH_COMMANDS.filter((c) => c.name.startsWith(value.slice(1))) : [];
32422
+ const menuOpen = matching.length > 0;
32423
+ const selected = matching[Math.min(sugIndex, matching.length - 1)];
32424
+ use_input_default((input, key) => {
32425
+ if (key.ctrl && (input === "c" || input === "d")) {
32426
+ exit();
32427
+ return;
32428
+ }
32429
+ if (key.tab && selected) {
32430
+ setValue(`/${selected.name} `);
32431
+ setSugIndex(0);
32432
+ setNavIndex(null);
32433
+ return;
32434
+ }
32435
+ if (key.upArrow) {
32436
+ if (menuOpen) {
32437
+ setSugIndex((i) => (i - 1 + matching.length) % matching.length);
32438
+ return;
32439
+ }
32440
+ if (history.length === 0) return;
32441
+ const next = navIndex === null ? history.length - 1 : Math.max(0, navIndex - 1);
32442
+ setNavIndex(next);
32443
+ setValue(history[next] ?? "");
32444
+ return;
32445
+ }
32446
+ if (key.downArrow) {
32447
+ if (menuOpen) {
32448
+ setSugIndex((i) => (i + 1) % matching.length);
32449
+ return;
32450
+ }
32451
+ if (navIndex === null) return;
32452
+ const next = navIndex + 1;
32453
+ if (next >= history.length) {
32454
+ setNavIndex(null);
32455
+ setValue("");
32456
+ } else {
32457
+ setNavIndex(next);
32458
+ setValue(history[next] ?? "");
32459
+ }
32460
+ return;
32461
+ }
32462
+ if (key.return) {
32463
+ const trimmed = value.trim();
32464
+ if (trimmed.length > 0) onSubmit(trimmed);
32465
+ setValue("");
32466
+ setNavIndex(null);
32467
+ setSugIndex(0);
32468
+ return;
32469
+ }
32470
+ if (key.backspace || key.delete) {
32471
+ setValue((v) => v.slice(0, -1));
32472
+ setNavIndex(null);
32473
+ setSugIndex(0);
32474
+ return;
32475
+ }
32476
+ if (!key.ctrl && !key.meta && input) {
32477
+ setValue((v) => v + input);
32478
+ setNavIndex(null);
32479
+ setSugIndex(0);
32480
+ }
32481
+ });
32482
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Box_default, { flexDirection: "column", children: [
32483
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Box_default, { children: menuOpen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { children: [
32484
+ matching.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { color: i === sugIndex ? "cyan" : "gray", inverse: i === sugIndex, children: [
32485
+ " ",
32486
+ "/",
32487
+ c.name,
32488
+ " "
32489
+ ] }, c.name)),
32490
+ selected ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Text, { dimColor: true, children: [
32491
+ " \u2014 ",
32492
+ selected.description,
32493
+ " (Tab)"
32494
+ ] }) : null
32495
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { children: " " }) }),
32496
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Box_default, { children: [
32497
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { color: connected ? "cyan" : "gray", children: "\u203A " }),
32498
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { children: value }),
32499
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { inverse: true, children: " " })
32500
+ ] })
32501
+ ] });
32502
+ }
32503
+
32504
+ // src/ui/spinner.tsx
32505
+ var import_react25 = __toESM(require_react(), 1);
32506
+ var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1);
32507
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
32508
+ function Spinner() {
32509
+ const [index, setIndex] = (0, import_react25.useState)(0);
32510
+ (0, import_react25.useEffect)(() => {
32511
+ const timer = setInterval(() => setIndex((i) => (i + 1) % FRAMES.length), 90);
32512
+ return () => clearInterval(timer);
32513
+ }, []);
32514
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", children: FRAMES[index] });
32515
+ }
32516
+
32384
32517
  // src/ui/app.tsx
32385
32518
  var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
32386
- var BANNER_ROWS = 5;
32387
- var COMMAND_BLOCK_ROWS = 6;
32519
+ var CHROME_ROWS = 12;
32520
+ var ENTRY_CAP = 3e3;
32388
32521
  function Rule({ width }) {
32389
32522
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: "\u2500".repeat(Math.max(10, width)) });
32390
32523
  }
@@ -32410,24 +32543,33 @@ function App2({
32410
32543
  const [entries2, setEntries] = (0, import_react26.useState)([]);
32411
32544
  const [status, setStatus] = (0, import_react26.useState)(bus.status);
32412
32545
  const [busy, setBusy] = (0, import_react26.useState)(bus.busy);
32546
+ const [activeProfile, setActiveProfile] = (0, import_react26.useState)(bus.activeProfile);
32547
+ const [scope, setScope] = (0, import_react26.useState)(bus.scope ?? info.scope);
32548
+ const [worker, setWorker] = (0, import_react26.useState)(bus.worker);
32549
+ const [scrollBack, setScrollBack] = (0, import_react26.useState)(0);
32413
32550
  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
32551
  (0, import_react26.useEffect)(() => {
32421
- const offTranscript = bus.onTranscript((entry) => setEntries((prev) => [...prev, entry]));
32552
+ const offTranscript = bus.onTranscript(
32553
+ (entry) => setEntries(
32554
+ (prev) => prev.length >= ENTRY_CAP ? [...prev.slice(prev.length - ENTRY_CAP + 1), entry] : [...prev, entry]
32555
+ )
32556
+ );
32422
32557
  const offStatus = bus.onStatus(setStatus);
32423
32558
  const offBusy = bus.onBusy(setBusy);
32559
+ const offProfile = bus.onActiveProfile(setActiveProfile);
32560
+ const offScope = bus.onScope(setScope);
32561
+ const offWorker = bus.onWorker(setWorker);
32424
32562
  return () => {
32425
32563
  offTranscript();
32426
32564
  offStatus();
32427
32565
  offBusy();
32566
+ offProfile();
32567
+ offScope();
32568
+ offWorker();
32428
32569
  };
32429
32570
  }, [bus]);
32430
32571
  const handleSubmit = (input) => {
32572
+ setScrollBack(0);
32431
32573
  setHistory((prev) => appendInputHistory(info.profileDir, input, prev));
32432
32574
  const trimmed = input.trim();
32433
32575
  if (trimmed.startsWith("/")) {
@@ -32435,36 +32577,47 @@ function App2({
32435
32577
  runSlashCommand(trimmed, {
32436
32578
  info,
32437
32579
  print: (text, level) => bus.push({ source: "local", kind: "log", text, level }),
32438
- clear: clearTranscript,
32439
- quit: () => exit()
32580
+ clear: () => {
32581
+ setEntries([]);
32582
+ setScrollBack(0);
32583
+ stdout.write("\x1B[2J\x1B[3J\x1B[H");
32584
+ },
32585
+ quit: () => exit(),
32586
+ submitAi: (aiInput) => bus.submitLocal(aiInput)
32440
32587
  });
32441
32588
  return;
32442
32589
  }
32443
32590
  bus.submitLocal(input);
32444
32591
  };
32445
- const lines = flattenEntries(entries2);
32446
- const items = [
32447
- { type: "banner" },
32448
- ...lines.map((line) => ({ type: "line", line }))
32449
- ];
32592
+ const bodyHeight = Math.max(3, rows - CHROME_ROWS);
32593
+ const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2), [entries2]);
32594
+ const maxScroll = Math.max(0, lines.length - bodyHeight);
32595
+ const clampedBack = Math.min(scrollBack, maxScroll);
32596
+ const lineRows = clampedBack > 0 ? bodyHeight - 1 : bodyHeight;
32597
+ const start = Math.max(0, lines.length - lineRows - clampedBack);
32598
+ const visible = lines.slice(start, start + lineRows);
32450
32599
  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
- ] })
32600
+ use_input_default((_input, key) => {
32601
+ if (key.pageUp) setScrollBack((b) => Math.min(maxScroll, b + bodyHeight));
32602
+ else if (key.pageDown) setScrollBack((b) => Math.max(0, b - bodyHeight));
32603
+ });
32604
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Box_default, { flexDirection: "column", height: rows, children: [
32605
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Banner, { info, scope, worker, status, activeProfile }),
32606
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Rule, { width }),
32607
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Box_default, { flexDirection: "column", height: bodyHeight, overflow: "hidden", children: [
32608
+ visible.map((line) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TranscriptLine, { line }, line.key)),
32609
+ clampedBack > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: ` \u2191 ${clampedBack} newer line(s) hidden \xB7 PageDown` }) : null
32610
+ ] }),
32611
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Box_default, { justifyContent: "flex-end", children: busy ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { color: "yellow", children: [
32612
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Spinner, {}),
32613
+ " processing \xB7 ",
32614
+ busy,
32615
+ "\u2026"
32616
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { children: " " }) }),
32617
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Rule, { width }),
32618
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CommandInput, { status, history, onSubmit: handleSubmit }),
32619
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Rule, { width }),
32620
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Hint, {})
32468
32621
  ] });
32469
32622
  }
32470
32623
 
@@ -32550,6 +32703,10 @@ async function runStart(profileDir2, profileName) {
32550
32703
  physicPath: config.physicPath ?? null,
32551
32704
  aiCli: config.aiCli ?? "claude"
32552
32705
  };
32706
+ const aiCli2 = config.aiCli ?? "claude";
32707
+ const workingDir = getWorkingProfile(profileDir2, aiCli2);
32708
+ const labels = profileLabels(config);
32709
+ bus.setActiveProfile(workingDir ? basename4(workingDir) : labels[0] ?? null);
32553
32710
  const tui = runTui(bus, info);
32554
32711
  bus.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
32555
32712
  void client.run();
@@ -32616,11 +32773,68 @@ async function runVersion(profileDir2, profileName, serverUrlFlag) {
32616
32773
  }
32617
32774
  }
32618
32775
 
32776
+ // src/ui/select-profile.tsx
32777
+ var import_react28 = __toESM(require_react(), 1);
32778
+ var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
32779
+ function Picker({
32780
+ items,
32781
+ title,
32782
+ onDone
32783
+ }) {
32784
+ const { exit } = use_app_default();
32785
+ const [index, setIndex] = (0, import_react28.useState)(0);
32786
+ use_input_default((input, key) => {
32787
+ if (key.upArrow) setIndex((i) => (i - 1 + items.length) % items.length);
32788
+ else if (key.downArrow) setIndex((i) => (i + 1) % items.length);
32789
+ else if (key.return) {
32790
+ onDone(items[index]?.value ?? null);
32791
+ exit();
32792
+ } else if (key.escape || key.ctrl && input === "c") {
32793
+ onDone(null);
32794
+ exit();
32795
+ }
32796
+ });
32797
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Box_default, { flexDirection: "column", paddingY: 1, children: [
32798
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Text, { bold: true, children: title }),
32799
+ items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(Text, { color: i === index ? "cyan" : void 0, children: [
32800
+ i === index ? "\u276F " : " ",
32801
+ item.label
32802
+ ] }, item.value)),
32803
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Text, { dimColor: true, children: "\u2191\u2193 move \xB7 Enter select \xB7 Esc cancel" })
32804
+ ] });
32805
+ }
32806
+ function selectFromList(items, title) {
32807
+ return new Promise((resolve5) => {
32808
+ let chosen = null;
32809
+ const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Picker, { items, title, onDone: (v) => chosen = v }));
32810
+ void app.waitUntilExit().then(() => resolve5(chosen));
32811
+ });
32812
+ }
32813
+
32619
32814
  // src/index.ts
32620
32815
  function flagValue(args, name) {
32621
32816
  const index = args.indexOf(`--${name}`);
32622
32817
  return index >= 0 ? args[index + 1] : void 0;
32623
32818
  }
32819
+ async function pickLinkedProfile(explicit, action) {
32820
+ if (explicit) return explicit;
32821
+ const linked = listProfiles().filter((p) => p.linked);
32822
+ if (linked.length === 0) {
32823
+ console.error("No linked profile \u2014 run `4pm link` first.");
32824
+ return null;
32825
+ }
32826
+ if (linked.length === 1) return linked[0].name;
32827
+ if (!process.stdout.isTTY) {
32828
+ console.error(
32829
+ `Multiple profiles \u2014 pass --profile <name> (or run \`4pm ${action}\` in a terminal to pick).`
32830
+ );
32831
+ return null;
32832
+ }
32833
+ return selectFromList(
32834
+ linked.map((p) => ({ label: p.name, value: p.name })),
32835
+ `Select a profile to ${action}:`
32836
+ );
32837
+ }
32624
32838
  async function main() {
32625
32839
  const [explicitProfile, args] = resolveProfileArg(process.argv.slice(2));
32626
32840
  const [command, ...rest] = args;
@@ -32631,12 +32845,20 @@ async function main() {
32631
32845
  break;
32632
32846
  }
32633
32847
  case "start": {
32634
- const name = explicitProfile ?? defaultProfileName();
32848
+ const name = await pickLinkedProfile(explicitProfile, "start");
32849
+ if (!name) {
32850
+ process.exitCode = 1;
32851
+ break;
32852
+ }
32635
32853
  await runStart(profileDir(name), name);
32636
32854
  break;
32637
32855
  }
32638
32856
  case "unlink": {
32639
- const name = explicitProfile ?? defaultProfileName();
32857
+ const name = await pickLinkedProfile(explicitProfile, "unlink");
32858
+ if (!name) {
32859
+ process.exitCode = 1;
32860
+ break;
32861
+ }
32640
32862
  runUnlink(profileDir(name), name);
32641
32863
  break;
32642
32864
  }
@@ -32659,13 +32881,15 @@ async function main() {
32659
32881
  "4pm \u2014 the 4PM worker-machine CLI",
32660
32882
  "",
32661
32883
  "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",
32884
+ " 4pm link Pair with the server (hashcode 1\u21922\u21923)",
32885
+ " 4pm start Connect WS, receive commands from the server",
32886
+ " 4pm unlink Delete a link (pick a profile if several)",
32887
+ " 4pm version Show the installed version (+ latest from the server)",
32888
+ " 4pm update Update to the latest version now",
32667
32889
  "",
32668
- "Without --profile \u21D2 the `default` profile (ADR-0014)."
32890
+ "Profiles: named after the paired account (e.g. mcacc1); when several exist,",
32891
+ "start/unlink show a picker (\u2191\u2193 + Enter) \u2014 no --profile needed.",
32892
+ "Server URL: from FOURPM_SERVER (default http://localhost:3001)."
32669
32893
  ].join("\n")
32670
32894
  );
32671
32895
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "type": "module",