@4pm/cli 0.2.0 → 0.2.1

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 (3) hide show
  1. package/README.md +11 -8
  2. package/dist/index.js +146 -65
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # @4pm/cli
2
2
 
3
- CLI chạy trên máy worker: nhận lệnh từ server và điều phối các CLI AI
4
- (claude / codex …). Node ESM + TypeScript.
3
+ **Agent chạy trên máy worker** — nhận lệnh từ `@4pm/server` qua WebSocket và điều phối
4
+ các CLI AI (`claude` / `codex`) cùng công cụ khác (`git`, `gh`, `glab`, `shell`).
5
+ Node ESM + TypeScript.
5
6
 
6
- Logic điều phối ở `src/core/`, subcommand ở `src/commands/`, giao tiếp server qua
7
- `src/services/` (dùng `@4pm/sdk`). Phần dùng chung tách vào `src/common/` (errors,
8
- logger, io); hàm thuần để ở `src/utils/`.
7
+ Vòng đời: **pairing** với server bằng hashcode (file `.cre`) → xin `ws_token` → mở
8
+ WebSocket TLS → nhận lệnh → **spawn tiến trình con**, gom output (backpressure) trả về
9
+ server. Nhiều instance trên một máy dùng profile riêng, server gom về một **worker**.
9
10
 
10
- > Cây thư mục: xem `README.md` (gốc) § Cấu trúc thư mục.
11
+ ```bash
12
+ pnpm --filter @4pm/cli dev
13
+ ```
11
14
 
12
- Còn thiếu (thêm khi bắt đầu phát triển): `tsconfig.json`, cấu hình ESLint,
13
- dependencies (vd `tsx`, thư viện parse CLI).
15
+ > Tổ chức mã, quy ước & phần còn thiếu: [`CLAUDE.md`](./CLAUDE.md).
16
+ > Đặc tả hành vi cli: [`11-docs/61-cli/`](../../11-docs/61-cli/README.md).
package/dist/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire as __createRequire } from 'node:module';
3
+ import { fileURLToPath as __fileURLToPath } from 'node:url';
4
+ import { dirname as __pathDirname } from 'node:path';
5
+ const require = __createRequire(import.meta.url);
6
+ const __filename = __fileURLToPath(import.meta.url);
7
+ const __dirname = __pathDirname(__filename);
2
8
  var __create = Object.create;
3
9
  var __defProp = Object.defineProperty;
4
10
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -3735,7 +3741,7 @@ function resolveProfile(args) {
3735
3741
  if (index >= 0) {
3736
3742
  const name = args[index + 1];
3737
3743
  if (!name || name.startsWith("--")) {
3738
- throw new Error("--profile c\u1EA7n k\xE8m t\xEAn profile.");
3744
+ throw new Error("--profile requires a profile name.");
3739
3745
  }
3740
3746
  const rest = [...args.slice(0, index), ...args.slice(index + 2)];
3741
3747
  return [name, rest];
@@ -3881,17 +3887,17 @@ async function fetchCliVersion(serverUrl) {
3881
3887
  async function runLink(serverUrl, profileDir2, profileName) {
3882
3888
  if (readCredential(profileDir2, profileName)) {
3883
3889
  console.log(
3884
- `Profile "${profileName}" \u0111\xE3 c\xF3 li\xEAn k\u1EBFt (.cre). Ch\u1EA1y \`4pm unlink --profile ${profileName}\` tr\u01B0\u1EDBc n\u1EBFu mu\u1ED1n pair l\u1EA1i.`
3890
+ `Profile "${profileName}" is already linked (.cre). Run \`4pm unlink --profile ${profileName}\` first if you want to pair again.`
3885
3891
  );
3886
3892
  return;
3887
3893
  }
3888
3894
  const hashcode1 = randomBytes(32).toString("hex");
3889
3895
  console.log(`\u2500\u2500 4PM cli pairing (profile: ${profileName}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
3890
- console.log("1. M\u1EDF web 4PM \u2192 /machines, nh\u1EADp hashcode (1):");
3896
+ console.log("1. Open the 4PM web \u2192 /machines, enter hashcode (1):");
3891
3897
  console.log(`
3892
3898
  ${hashcode1}
3893
3899
  `);
3894
- console.log("2. Web s\u1EBD hi\u1EC3n th\u1ECB hashcode (2) \u2014 d\xE1n v\xE0o \u0111\xE2y.");
3900
+ console.log("2. The web will show hashcode (2) \u2014 paste it here.");
3895
3901
  const rl = createInterface({ input: process.stdin, output: process.stdout });
3896
3902
  const hashcode2 = (await rl.question("hashcode (2): ")).trim();
3897
3903
  rl.close();
@@ -3900,10 +3906,10 @@ async function runLink(serverUrl, profileDir2, profileName) {
3900
3906
  serverUrl,
3901
3907
  hashcode3,
3902
3908
  scope: "unknown",
3903
- // scope do server quyết theo user pair (ADR-0010)
3909
+ // scope is decided by the server based on the pairing user (ADR-0010)
3904
3910
  pairedAt: (/* @__PURE__ */ new Date()).toISOString()
3905
3911
  });
3906
- console.log("\u2714 Li\xEAn k\u1EBFt th\xE0nh c\xF4ng \u2014 \u0111\xE3 l\u01B0u .cre. Ch\u1EA1y `4pm start` \u0111\u1EC3 k\u1EBFt n\u1ED1i.");
3912
+ console.log("\u2714 Linked successfully \u2014 saved .cre. Run `4pm start` to connect.");
3907
3913
  }
3908
3914
 
3909
3915
  // src/commands/start.ts
@@ -3924,7 +3930,7 @@ MCowBQYDK2VwAyEARlfzjaFhe5bhRrMp/ZuH6sVqg/S7f8B1dliuF21iDTw=
3924
3930
  `;
3925
3931
 
3926
3932
  // src/version.ts
3927
- var CLI_VERSION = "0.2.0";
3933
+ var CLI_VERSION = "0.2.1";
3928
3934
 
3929
3935
  // src/core/update.ts
3930
3936
  function compareSemver(a, b) {
@@ -3944,15 +3950,15 @@ function updateViaNpm(version) {
3944
3950
  }
3945
3951
  async function updateViaDownload(tarballUrl, checksum, signature) {
3946
3952
  const res = await fetch(tarballUrl);
3947
- if (!res.ok) throw new Error(`T\u1EA3i tarball l\u1ED7i: HTTP ${res.status}`);
3953
+ if (!res.ok) throw new Error(`Failed to download tarball: HTTP ${res.status}`);
3948
3954
  const buf = Buffer.from(await res.arrayBuffer());
3949
3955
  const actual = createHash2("sha256").update(buf).digest("hex");
3950
3956
  if (actual !== checksum) {
3951
- throw new Error("Checksum tarball kh\xF4ng kh\u1EDBp \u2014 h\u1EE7y update (ADR-0015).");
3957
+ throw new Error("Tarball checksum mismatch \u2014 aborting update (ADR-0015).");
3952
3958
  }
3953
3959
  if (CLI_SIGNING_PUBLIC_KEY) {
3954
3960
  if (!signature) {
3955
- throw new Error("Thi\u1EBFu ch\u1EEF k\xFD tarball \u2014 h\u1EE7y update (ADR-0015).");
3961
+ throw new Error("Missing tarball signature \u2014 aborting update (ADR-0015).");
3956
3962
  }
3957
3963
  const trimmed = CLI_SIGNING_PUBLIC_KEY.trim();
3958
3964
  const publicKey = trimmed.includes("BEGIN") ? createPublicKey(trimmed) : createPublicKey({
@@ -3962,7 +3968,7 @@ async function updateViaDownload(tarballUrl, checksum, signature) {
3962
3968
  });
3963
3969
  const ok = verify(null, buf, publicKey, Buffer.from(signature, "base64"));
3964
3970
  if (!ok) {
3965
- throw new Error("Ch\u1EEF k\xFD tarball kh\xF4ng h\u1EE3p l\u1EC7 \u2014 h\u1EE7y update (ADR-0015).");
3971
+ throw new Error("Invalid tarball signature \u2014 aborting update (ADR-0015).");
3966
3972
  }
3967
3973
  }
3968
3974
  const tmp = join3(mkdtempSync(join3(tmpdir(), "4pm-update-")), "cli.tgz");
@@ -3977,7 +3983,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
3977
3983
  try {
3978
3984
  meta = await fetchCliVersion(serverUrl);
3979
3985
  } catch (err) {
3980
- console.warn(`Kh\xF4ng check \u0111\u01B0\u1EE3c version cli: ${err}`);
3986
+ console.warn(`Could not check cli version: ${err}`);
3981
3987
  return { action: "none" };
3982
3988
  }
3983
3989
  const mandatory = compareSemver(CLI_VERSION, meta.minSupported) < 0;
@@ -3985,12 +3991,12 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
3985
3991
  if (!outdated) return { action: "none" };
3986
3992
  if (!mandatory && !autoUpdate) {
3987
3993
  console.warn(
3988
- `C\xF3 version m\u1EDBi ${meta.latest} (\u0111ang ch\u1EA1y ${CLI_VERSION}) \u2014 autoUpdate \u0111ang t\u1EAFt.`
3994
+ `A new version ${meta.latest} is available (running ${CLI_VERSION}) \u2014 autoUpdate is off.`
3989
3995
  );
3990
3996
  return { action: "none" };
3991
3997
  }
3992
3998
  console.log(
3993
- `${mandatory ? "B\u1EAFt bu\u1ED9c" : "T\u1EF1 \u0111\u1ED9ng"} update cli ${CLI_VERSION} \u2192 ${meta.latest}\u2026`
3999
+ `${mandatory ? "Mandatory" : "Automatic"} cli update ${CLI_VERSION} \u2192 ${meta.latest}\u2026`
3994
4000
  );
3995
4001
  try {
3996
4002
  if (installedViaNpm()) {
@@ -4005,10 +4011,10 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
4005
4011
  return { action: "updated", version: meta.latest };
4006
4012
  } catch (err) {
4007
4013
  if (mandatory) {
4008
- console.error(`Update th\u1EA5t b\u1EA1i: ${err}`);
4014
+ console.error(`Update failed: ${err}`);
4009
4015
  return { action: "blocked", minSupported: meta.minSupported };
4010
4016
  }
4011
- console.warn(`Update th\u1EA5t b\u1EA1i (b\u1ECF qua \u2014 kh\xF4ng b\u1EAFt bu\u1ED9c): ${err}`);
4017
+ console.warn(`Update failed (skipping \u2014 not mandatory): ${err}`);
4012
4018
  return { action: "none" };
4013
4019
  }
4014
4020
  }
@@ -4056,14 +4062,18 @@ var PermissionCode = {
4056
4062
  // org
4057
4063
  ORG_READ: "org.read",
4058
4064
  ORG_UPDATE: "org.update",
4065
+ // soft-delete the whole org + cancel its subscription (ADMIN only)
4066
+ ORG_DELETE: "org.delete",
4059
4067
  // user
4060
4068
  USER_READ_SELF: "user.read_self",
4061
4069
  USER_LIST: "user.list",
4062
4070
  USER_CREATE: "user.create",
4063
4071
  USER_READ: "user.read",
4064
4072
  USER_UPDATE: "user.update",
4073
+ // ADMIN resets a sub-account's password directly (user-0010)
4074
+ USER_SET_PASSWORD: "user.set_password",
4065
4075
  USER_DELETE: "user.delete",
4066
- // mời user qua email (ADR-0021)
4076
+ // invite users by email (ADR-0021)
4067
4077
  USER_INVITE: "user.invite",
4068
4078
  // team
4069
4079
  TEAM_LIST: "team.list",
@@ -4085,6 +4095,8 @@ var PermissionCode = {
4085
4095
  PROJECT_DETACH_TEAM: "project.detach_team",
4086
4096
  PROJECT_ATTACH_USER: "project.attach_user",
4087
4097
  PROJECT_DETACH_USER: "project.detach_user",
4098
+ PROJECT_ATTACH_MANAGER: "project.attach_manager",
4099
+ PROJECT_DETACH_MANAGER: "project.detach_manager",
4088
4100
  PROJECT_AI_ASSIST: "project.ai_assist",
4089
4101
  PROJECT_MEMO_READ: "project.memo_read",
4090
4102
  PROJECT_MEMO_WRITE: "project.memo_write",
@@ -4097,7 +4109,7 @@ var PermissionCode = {
4097
4109
  MACHINE_LIST: "machine.list",
4098
4110
  MACHINE_UPDATE: "machine.update",
4099
4111
  MACHINE_UNLINK: "machine.unlink",
4100
- // gán/gỡ physic project cho cli
4112
+ // assign/detach a cli's physic project
4101
4113
  MACHINE_PHYSIC_MANAGE: "machine.physic_manage",
4102
4114
  // command
4103
4115
  COMMAND_EXECUTE: "command.execute",
@@ -4112,16 +4124,21 @@ var PermissionCode = {
4112
4124
  BILLING_MANAGE: "billing.manage"
4113
4125
  };
4114
4126
  var ALL_PERMISSION_CODES = Object.values(PermissionCode);
4127
+ var GrantSubjectType = {
4128
+ USER: "user",
4129
+ TEAM: "team",
4130
+ PROJECT: "project"
4131
+ };
4115
4132
  var P = PermissionCode;
4116
4133
  var COMMON = [
4117
4134
  P.AUTH_LOGOUT,
4118
4135
  P.AUTH_CHANGE_PASSWORD,
4119
4136
  P.USER_READ_SELF,
4120
- // notification của chính mình
4137
+ // one's own notifications
4121
4138
  P.NOTIFICATION_READ
4122
4139
  ];
4123
4140
  var ROLE_DEFAULT_PERMISSIONS = {
4124
- // ADMIN: full quyền
4141
+ // ADMIN: full permissions
4125
4142
  [Role.ADMIN]: ALL_PERMISSION_CODES,
4126
4143
  [Role.PM]: [
4127
4144
  ...COMMON,
@@ -4150,6 +4167,8 @@ var ROLE_DEFAULT_PERMISSIONS = {
4150
4167
  P.PROJECT_DETACH_TEAM,
4151
4168
  P.PROJECT_ATTACH_USER,
4152
4169
  P.PROJECT_DETACH_USER,
4170
+ P.PROJECT_ATTACH_MANAGER,
4171
+ P.PROJECT_DETACH_MANAGER,
4153
4172
  P.PROJECT_AI_ASSIST,
4154
4173
  P.PROJECT_MEMO_READ,
4155
4174
  P.PROJECT_MEMO_WRITE,
@@ -4195,7 +4214,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
4195
4214
  P.PROJECT_READ,
4196
4215
  P.PROJECT_MEMO_READ,
4197
4216
  P.PROJECT_MEMO_WRITE,
4198
- // xem worker/cli của mình + setting physic project
4217
+ // view one's own worker/cli + configure the physic project
4199
4218
  P.MACHINE_LIST,
4200
4219
  P.MACHINE_PHYSIC_MANAGE,
4201
4220
  P.COMMAND_EXECUTE,
@@ -4210,8 +4229,70 @@ var PROJECT_SCOPE_CHECKED_CODES = [
4210
4229
  P.PROJECT_DETACH_TEAM,
4211
4230
  P.PROJECT_ATTACH_USER,
4212
4231
  P.PROJECT_DETACH_USER,
4232
+ P.PROJECT_ATTACH_MANAGER,
4233
+ P.PROJECT_DETACH_MANAGER,
4213
4234
  P.PROJECT_AI_ASSIST
4214
4235
  ];
4236
+ var SELF_SERVICE_CODES = [
4237
+ P.AUTH_LOGOUT,
4238
+ P.AUTH_CHANGE_PASSWORD,
4239
+ P.USER_READ_SELF,
4240
+ P.NOTIFICATION_READ
4241
+ ];
4242
+ var TEAM_GRANTABLE_CODES = [
4243
+ P.TEAM_READ,
4244
+ P.TEAM_UPDATE,
4245
+ P.TEAM_DELETE,
4246
+ P.TEAM_ATTACH_USER,
4247
+ P.TEAM_DETACH_USER,
4248
+ P.TEAM_ATTACH_PROJECT,
4249
+ P.TEAM_DETACH_PROJECT,
4250
+ P.PROJECT_READ,
4251
+ P.PROJECT_UPDATE,
4252
+ P.PROJECT_ATTACH_TEAM,
4253
+ P.PROJECT_DETACH_TEAM,
4254
+ P.PROJECT_ATTACH_USER,
4255
+ P.PROJECT_DETACH_USER,
4256
+ P.PROJECT_ATTACH_MANAGER,
4257
+ P.PROJECT_DETACH_MANAGER,
4258
+ P.PROJECT_AI_ASSIST,
4259
+ P.PROJECT_MEMO_READ,
4260
+ P.PROJECT_MEMO_WRITE,
4261
+ P.COMMAND_EXECUTE,
4262
+ P.COMMAND_READ,
4263
+ P.MACHINE_LIST,
4264
+ P.MACHINE_PHYSIC_MANAGE
4265
+ ];
4266
+ var PROJECT_GRANTABLE_CODES = [
4267
+ P.PROJECT_READ,
4268
+ P.PROJECT_UPDATE,
4269
+ P.PROJECT_DELETE,
4270
+ P.PROJECT_ATTACH_TEAM,
4271
+ P.PROJECT_DETACH_TEAM,
4272
+ P.PROJECT_ATTACH_USER,
4273
+ P.PROJECT_DETACH_USER,
4274
+ P.PROJECT_ATTACH_MANAGER,
4275
+ P.PROJECT_DETACH_MANAGER,
4276
+ P.PROJECT_AI_ASSIST,
4277
+ P.PROJECT_MEMO_READ,
4278
+ P.PROJECT_MEMO_WRITE,
4279
+ P.COMMAND_EXECUTE,
4280
+ P.COMMAND_READ,
4281
+ P.MACHINE_PHYSIC_MANAGE
4282
+ ];
4283
+ var SELF_SERVICE_SET = new Set(SELF_SERVICE_CODES);
4284
+ var TEAM_GRANTABLE_SET = new Set(TEAM_GRANTABLE_CODES);
4285
+ var PROJECT_GRANTABLE_SET = new Set(PROJECT_GRANTABLE_CODES);
4286
+ function grantSubjectsFor(code) {
4287
+ if (SELF_SERVICE_SET.has(code)) return [];
4288
+ const subjects = [GrantSubjectType.USER];
4289
+ if (TEAM_GRANTABLE_SET.has(code)) subjects.push(GrantSubjectType.TEAM);
4290
+ if (PROJECT_GRANTABLE_SET.has(code)) subjects.push(GrantSubjectType.PROJECT);
4291
+ return subjects;
4292
+ }
4293
+ var PERMISSION_GRANT_SUBJECTS = Object.fromEntries(
4294
+ ALL_PERMISSION_CODES.map((code) => [code, grantSubjectsFor(code)])
4295
+ );
4215
4296
 
4216
4297
  // ../../22-packages/11-shared/11-constants/src/auth.ts
4217
4298
  var ACCESS_TOKEN_TTL_SEC = 60 * 60;
@@ -4310,8 +4391,8 @@ var OutputBatcher = class {
4310
4391
  };
4311
4392
  }
4312
4393
  /**
4313
- * Nạp thêm output; flush khi đạt ngưỡng; áp backpressure/truncate khi
4314
- * vượt trần.
4394
+ * Push more output; flush at the threshold; apply backpressure/truncate when
4395
+ * over the cap.
4315
4396
  */
4316
4397
  push(chunk) {
4317
4398
  if (this.buffer.length >= this.opts.maxBufferKb * 1024) {
@@ -4335,7 +4416,7 @@ var OutputBatcher = class {
4335
4416
  }
4336
4417
  }
4337
4418
  /**
4338
- * Đẩy toàn bộ buffer hiện tại đi ngay (1 batch).
4419
+ * Flush the entire current buffer immediately (one batch).
4339
4420
  */
4340
4421
  flush() {
4341
4422
  if (this.timer) {
@@ -4399,7 +4480,7 @@ async function runCommand(dispatch, emit) {
4399
4480
  emit({
4400
4481
  commandId: dispatch.commandId,
4401
4482
  seq: seq++,
4402
- chunk: `spawn l\u1ED7i: ${err.message}
4483
+ chunk: `spawn error: ${err.message}
4403
4484
  `,
4404
4485
  done: true,
4405
4486
  exitCode: -1
@@ -4748,13 +4829,13 @@ var WsClient = class {
4748
4829
  heartbeat = null;
4749
4830
  stopped = false;
4750
4831
  credential;
4751
- /** Output đệm trong lúc mất kết nối (flush khi nối lại). */
4832
+ /** Output buffered while disconnected (flushed on reconnect). */
4752
4833
  offlineBuffer = [];
4753
4834
  offlineBufferBytes = 0;
4754
- /** Request cli → server đang chờ reply (quota.check — ADR-0020). */
4835
+ /** cli → server requests awaiting a reply (quota.check — ADR-0020). */
4755
4836
  pending = /* @__PURE__ */ new Map();
4756
4837
  /**
4757
- * Vòng đời chính: connect + reconnect backoff cho tới khi bị revoke.
4838
+ * Main lifecycle: connect + reconnect backoff until revoked.
4758
4839
  */
4759
4840
  async run() {
4760
4841
  for (let attempt = 0; !this.stopped; attempt++) {
@@ -4766,16 +4847,16 @@ var WsClient = class {
4766
4847
  this.logout(err.errorCode);
4767
4848
  return;
4768
4849
  }
4769
- console.error(`K\u1EBFt n\u1ED1i l\u1ED7i: ${err}`);
4850
+ console.error(`Connection error: ${err}`);
4770
4851
  }
4771
4852
  if (this.stopped) return;
4772
4853
  const wait = backoffJitterMs(Math.min(attempt, 6), 1e3, 6e4);
4773
- console.log(`K\u1EBFt n\u1ED1i l\u1EA1i sau ${Math.round(wait / 1e3)}s\u2026`);
4854
+ console.log(`Reconnecting in ${Math.round(wait / 1e3)}s\u2026`);
4774
4855
  await sleep(wait);
4775
4856
  }
4776
4857
  }
4777
4858
  /**
4778
- * 1 phiên WS: token → connect → handshake → xử lý message tới khi đóng.
4859
+ * One WS session: token → connect → handshake → handle messages until close.
4779
4860
  */
4780
4861
  connectOnce() {
4781
4862
  return new Promise((resolve5, reject) => {
@@ -4802,14 +4883,14 @@ var WsClient = class {
4802
4883
  try {
4803
4884
  this.handleMessage(JSON.parse(String(raw)));
4804
4885
  } catch (err) {
4805
- console.error(`Message l\u1ED7i: ${err}`);
4886
+ console.error(`Message error: ${err}`);
4806
4887
  }
4807
4888
  });
4808
4889
  socket.on("close", (code) => {
4809
4890
  this.clearHeartbeat();
4810
4891
  this.sessionKey = null;
4811
4892
  if (code === CLOSE_DRAINING) {
4812
- console.log("Server draining \u2014 s\u1EBD k\u1EBFt n\u1ED1i l\u1EA1i (backoff + jitter).");
4893
+ console.log("Server draining \u2014 will reconnect (backoff + jitter).");
4813
4894
  }
4814
4895
  resolve5();
4815
4896
  });
@@ -4821,13 +4902,13 @@ var WsClient = class {
4821
4902
  });
4822
4903
  }
4823
4904
  /**
4824
- * Xử lý control message (hello_ack/error) và envelope kênh.
4905
+ * Handle control messages (hello_ack/error) and channel envelopes.
4825
4906
  */
4826
4907
  handleMessage(message) {
4827
4908
  if ("type" in message) {
4828
4909
  if (message.type === "hello_ack" && this.ecdh) {
4829
4910
  this.sessionKey = deriveSessionKey(this.ecdh, message.pubkey);
4830
- console.log("\u2714 \u0110\xE3 k\u1EBFt n\u1ED1i server (m\xE3 h\xF3a phi\xEAn s\u1EB5n s\xE0ng).");
4911
+ console.log("\u2714 Connected to the server (session encryption ready).");
4831
4912
  this.sendMachineStatus();
4832
4913
  this.flushOfflineBuffer();
4833
4914
  this.startHeartbeat();
@@ -4840,11 +4921,11 @@ var WsClient = class {
4840
4921
  }
4841
4922
  if (message.errorCode === "CLI_VERSION_UNSUPPORTED") {
4842
4923
  this.stopped = true;
4843
- console.error(`Server t\u1EEB ch\u1ED1i version cli (${message.message}) \u2014 h\xE3y update.`);
4924
+ console.error(`Server rejected the cli version (${message.message}) \u2014 please update.`);
4844
4925
  this.socket?.close();
4845
4926
  return;
4846
4927
  }
4847
- console.warn(`Server b\xE1o l\u1ED7i: ${message.errorCode}`);
4928
+ console.warn(`Server reported an error: ${message.errorCode}`);
4848
4929
  this.socket?.close();
4849
4930
  return;
4850
4931
  }
@@ -4927,9 +5008,9 @@ var WsClient = class {
4927
5008
  }
4928
5009
  }
4929
5010
  /**
4930
- * Gửi 1 envelope (payload mã hóa + wsToken từng message — arch 0004).
4931
- * Mất kết nối: output lệnh đang chạy giữ trong buffer (trần 512KB,
4932
- * bỏ cũ nhất) rồi flush khi nối lại.
5011
+ * Send one envelope (encrypted payload + per-message wsToken — arch 0004).
5012
+ * When disconnected: the running command's output is held in a buffer (512KB cap,
5013
+ * dropping the oldest) then flushed on reconnect.
4933
5014
  */
4934
5015
  send(channel, data, replyTo = null) {
4935
5016
  if (!this.socket || !this.sessionKey) {
@@ -4948,13 +5029,13 @@ var WsClient = class {
4948
5029
  this.socket.send(JSON.stringify(envelope));
4949
5030
  }
4950
5031
  /**
4951
- * Request cli → server và chờ reply theo id envelope (timeout 15s).
4952
- * Dùng cho quota.check trước khi spawn AI cli (ADR-0020).
5032
+ * Send a cli → server request and await the reply by envelope id (15s timeout).
5033
+ * Used for quota.check before spawning an AI cli (ADR-0020).
4953
5034
  */
4954
5035
  request(channel, data) {
4955
5036
  return new Promise((resolve5, reject) => {
4956
5037
  if (!this.socket || !this.sessionKey) {
4957
- reject(new Error("Ch\u01B0a k\u1EBFt n\u1ED1i server."));
5038
+ reject(new Error("Not connected to the server."));
4958
5039
  return;
4959
5040
  }
4960
5041
  const { payload, nonce } = encryptPayload(this.sessionKey, data);
@@ -4968,7 +5049,7 @@ var WsClient = class {
4968
5049
  };
4969
5050
  const timer = setTimeout(() => {
4970
5051
  this.pending.delete(envelope.id);
4971
- reject(new Error("Server kh\xF4ng tr\u1EA3 l\u1EDDi (timeout)."));
5052
+ reject(new Error("Server did not respond (timeout)."));
4972
5053
  }, 15e3);
4973
5054
  timer.unref?.();
4974
5055
  this.pending.set(envelope.id, {
@@ -4978,16 +5059,16 @@ var WsClient = class {
4978
5059
  this.socket.send(JSON.stringify(envelope));
4979
5060
  });
4980
5061
  }
4981
- /** quota.check trước khi spawn AI cli (ADR-0020). */
5062
+ /** quota.check before spawning an AI cli (ADR-0020). */
4982
5063
  checkQuota(metric, amount = 1) {
4983
5064
  return this.request(WsChannels.QUOTA_CHECK, { metric, amount });
4984
5065
  }
4985
- /** usage.report batch về server (ADR-0020). */
5066
+ /** usage.report batched to the server (ADR-0020). */
4986
5067
  reportUsage(events) {
4987
5068
  this.send(WsChannels.USAGE_REPORT, { events });
4988
5069
  }
4989
5070
  /**
4990
- * Đệm output khi offline (trần bytes — bỏ message cũ nhất khi vượt).
5071
+ * Buffer output while offline (byte cap — drop the oldest message when exceeded).
4991
5072
  */
4992
5073
  bufferOffline(channel, data) {
4993
5074
  const bytes = JSON.stringify(data).length;
@@ -4999,7 +5080,7 @@ var WsClient = class {
4999
5080
  }
5000
5081
  }
5001
5082
  /**
5002
- * Flush buffer output sau khi handshake lại thành công.
5083
+ * Flush the output buffer after a successful re-handshake.
5003
5084
  */
5004
5085
  flushOfflineBuffer() {
5005
5086
  const queued = this.offlineBuffer.splice(0);
@@ -5007,8 +5088,8 @@ var WsClient = class {
5007
5088
  for (const item of queued) this.send(item.channel, item.data);
5008
5089
  }
5009
5090
  /**
5010
- * Báo machine.status: version + trạng thái physic project đang phục vụ
5011
- * (path, folder tồn tại?, autonomous).
5091
+ * Report machine.status: version + the state of the physic project being served
5092
+ * (path, does the folder exist?, autonomous).
5012
5093
  */
5013
5094
  sendMachineStatus() {
5014
5095
  const config = readProfileConfig(this.context.profileDir);
@@ -5022,7 +5103,7 @@ var WsClient = class {
5022
5103
  autonomousRunning: false
5023
5104
  });
5024
5105
  }
5025
- /** Heartbeat 30s: ping + báo lại machine.status định kỳ. */
5106
+ /** Heartbeat every 30s: ping + periodically re-report machine.status. */
5026
5107
  startHeartbeat() {
5027
5108
  this.clearHeartbeat();
5028
5109
  this.heartbeat = setInterval(() => {
@@ -5036,14 +5117,14 @@ var WsClient = class {
5036
5117
  this.heartbeat = null;
5037
5118
  }
5038
5119
  /**
5039
- * Logout: đóng WS, xóa `.cre`, hướng dẫn pair lại (state Unlinked).
5120
+ * Logout: close WS, delete `.cre`, and prompt to pair again (Unlinked state).
5040
5121
  */
5041
5122
  logout(reason) {
5042
5123
  this.stopped = true;
5043
5124
  this.clearHeartbeat();
5044
5125
  this.socket?.close();
5045
5126
  deleteCredential(this.context.profileDir);
5046
- console.error(`Li\xEAn k\u1EBFt kh\xF4ng c\xF2n hi\u1EC7u l\u1EF1c (${reason}) \u2014 \u0111\xE3 x\xF3a .cre. Ch\u1EA1y \`4pm link\` \u0111\u1EC3 pair l\u1EA1i.`);
5127
+ console.error(`Link is no longer valid (${reason}) \u2014 deleted .cre. Run \`4pm link\` to pair again.`);
5047
5128
  }
5048
5129
  };
5049
5130
 
@@ -5052,7 +5133,7 @@ async function runStart(profileDir2, profileName) {
5052
5133
  const credential = readCredential(profileDir2, profileName);
5053
5134
  if (!credential) {
5054
5135
  console.error(
5055
- `Profile "${profileName}" ch\u01B0a li\xEAn k\u1EBFt \u2014 ch\u1EA1y \`4pm link --server <URL>\` tr\u01B0\u1EDBc.`
5136
+ `Profile "${profileName}" is not linked \u2014 run \`4pm link --server <URL>\` first.`
5056
5137
  );
5057
5138
  process.exitCode = 1;
5058
5139
  return;
@@ -5062,13 +5143,13 @@ async function runStart(profileDir2, profileName) {
5062
5143
  const result = await checkAndUpdate(credential.serverUrl, autoUpdate);
5063
5144
  if (result.action === "blocked") {
5064
5145
  console.error(
5065
- `CLI c\u0169 h\u01A1n minSupported (${result.minSupported}) v\xE0 update th\u1EA5t b\u1EA1i \u2014 kh\xF4ng th\u1EC3 connect.`
5146
+ `CLI is older than minSupported (${result.minSupported}) and the update failed \u2014 cannot connect.`
5066
5147
  );
5067
5148
  process.exitCode = 1;
5068
5149
  return;
5069
5150
  }
5070
5151
  if (result.action === "updated") {
5071
- console.log("Kh\u1EDFi \u0111\u1ED9ng l\u1EA1i v\u1EDBi version m\u1EDBi\u2026");
5152
+ console.log("Restarting with the new version\u2026");
5072
5153
  const child = spawn2(process.execPath, process.argv.slice(1), {
5073
5154
  stdio: "inherit",
5074
5155
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
@@ -5077,7 +5158,7 @@ async function runStart(profileDir2, profileName) {
5077
5158
  return;
5078
5159
  }
5079
5160
  initCommandHistory(profileDir2, profileName, config.commandHistoryUploadMinutes ?? 10);
5080
- console.log(`K\u1EBFt n\u1ED1i t\u1EDBi ${credential.serverUrl} (profile: ${profileName})\u2026`);
5161
+ console.log(`Connecting to ${credential.serverUrl} (profile: ${profileName})\u2026`);
5081
5162
  await new WsClient({
5082
5163
  credential,
5083
5164
  machine: collectFingerprint(),
@@ -5088,11 +5169,11 @@ async function runStart(profileDir2, profileName) {
5088
5169
  // src/commands/unlink.ts
5089
5170
  function runUnlink(profileDir2, profileName) {
5090
5171
  if (!readCredential(profileDir2, profileName)) {
5091
- console.log(`Profile "${profileName}" ch\u01B0a c\xF3 li\xEAn k\u1EBFt n\xE0o.`);
5172
+ console.log(`Profile "${profileName}" has no link yet.`);
5092
5173
  return;
5093
5174
  }
5094
5175
  deleteCredential(profileDir2);
5095
- console.log("\u2714 \u0110\xE3 x\xF3a .cre \u2014 h\u1EE7y li\xEAn k\u1EBFt ph\xEDa server \u1EDF web /machines.");
5176
+ console.log("\u2714 Deleted .cre \u2014 revoke the server-side link at web /machines.");
5096
5177
  }
5097
5178
 
5098
5179
  // src/index.ts
@@ -5119,14 +5200,14 @@ async function main() {
5119
5200
  default:
5120
5201
  console.log(
5121
5202
  [
5122
- "4pm \u2014 CLI m\xE1y worker c\u1EE7a 4PM",
5203
+ "4pm \u2014 the 4PM worker-machine CLI",
5123
5204
  "",
5124
- "C\xE1ch d\xF9ng:",
5125
- " 4pm [--profile <t\xEAn>] link [--server URL] Pairing v\u1EDBi server (hashcode 1\u21922\u21923)",
5126
- " 4pm [--profile <t\xEAn>] start K\u1EBFt n\u1ED1i WS, nh\u1EADn l\u1EC7nh t\u1EEB server",
5127
- " 4pm [--profile <t\xEAn>] unlink X\xF3a credential c\u1EE5c b\u1ED9 (.cre)",
5205
+ "Usage:",
5206
+ " 4pm [--profile <name>] link [--server URL] Pair with the server (hashcode 1\u21922\u21923)",
5207
+ " 4pm [--profile <name>] start Connect WS, receive commands from the server",
5208
+ " 4pm [--profile <name>] unlink Delete the local credential (.cre)",
5128
5209
  "",
5129
- "Kh\xF4ng c\xF3 --profile \u21D2 profile `default` (ADR-0014)."
5210
+ "Without --profile \u21D2 the `default` profile (ADR-0014)."
5130
5211
  ].join("\n")
5131
5212
  );
5132
5213
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4pm/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "description": "4PM CLI — drives Claude/Codex CLIs on AI worker machines (Node + TypeScript)",
6
6
  "type": "module",