@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.
- package/README.md +11 -8
- package/dist/index.js +146 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# @4pm/cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
(claude / codex
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
+
```bash
|
|
12
|
+
pnpm --filter @4pm/cli dev
|
|
13
|
+
```
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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}"
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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(`
|
|
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("
|
|
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("
|
|
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("
|
|
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(`
|
|
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
|
-
`
|
|
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 ? "
|
|
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
|
|
4014
|
+
console.error(`Update failed: ${err}`);
|
|
4009
4015
|
return { action: "blocked", minSupported: meta.minSupported };
|
|
4010
4016
|
}
|
|
4011
|
-
console.warn(`Update
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
4137
|
+
// one's own notifications
|
|
4121
4138
|
P.NOTIFICATION_READ
|
|
4122
4139
|
];
|
|
4123
4140
|
var ROLE_DEFAULT_PERMISSIONS = {
|
|
4124
|
-
// ADMIN: full
|
|
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
|
-
//
|
|
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
|
-
*
|
|
4314
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
4832
|
+
/** Output buffered while disconnected (flushed on reconnect). */
|
|
4752
4833
|
offlineBuffer = [];
|
|
4753
4834
|
offlineBufferBytes = 0;
|
|
4754
|
-
/**
|
|
4835
|
+
/** cli → server requests awaiting a reply (quota.check — ADR-0020). */
|
|
4755
4836
|
pending = /* @__PURE__ */ new Map();
|
|
4756
4837
|
/**
|
|
4757
|
-
*
|
|
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(`
|
|
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(`
|
|
4854
|
+
console.log(`Reconnecting in ${Math.round(wait / 1e3)}s\u2026`);
|
|
4774
4855
|
await sleep(wait);
|
|
4775
4856
|
}
|
|
4776
4857
|
}
|
|
4777
4858
|
/**
|
|
4778
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
4931
|
-
*
|
|
4932
|
-
*
|
|
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
|
-
*
|
|
4952
|
-
*
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
5011
|
-
* (path, folder
|
|
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 +
|
|
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:
|
|
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(`
|
|
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}"
|
|
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
|
|
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("
|
|
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(`
|
|
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}"
|
|
5172
|
+
console.log(`Profile "${profileName}" has no link yet.`);
|
|
5092
5173
|
return;
|
|
5093
5174
|
}
|
|
5094
5175
|
deleteCredential(profileDir2);
|
|
5095
|
-
console.log("\u2714
|
|
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
|
|
5203
|
+
"4pm \u2014 the 4PM worker-machine CLI",
|
|
5123
5204
|
"",
|
|
5124
|
-
"
|
|
5125
|
-
" 4pm [--profile <
|
|
5126
|
-
" 4pm [--profile <
|
|
5127
|
-
" 4pm [--profile <
|
|
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
|
-
"
|
|
5210
|
+
"Without --profile \u21D2 the `default` profile (ADR-0014)."
|
|
5130
5211
|
].join("\n")
|
|
5131
5212
|
);
|
|
5132
5213
|
}
|