@mearl/cloud-server 2.7.0 → 2.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/__tests__/mode.test.d.ts +1 -0
- package/dist/__tests__/qoder.test.d.ts +1 -0
- package/dist/cli.js +118 -5
- package/dist/daemon.d.ts +2 -0
- package/dist/index.js +4 -3
- package/dist/mode.d.ts +10 -0
- package/dist/paths.d.ts +2 -0
- package/dist/qoder.d.ts +29 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
npx @mearl/setup --cloud
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
Qoder Cloud Agent 使用同一套包和 server 命令,只在 setup 时选择配对模式:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx @mearl/setup --qoder-cloud --yes
|
|
17
|
+
```
|
|
18
|
+
|
|
13
19
|
在 Docker 或自定义进程托管等只需要 Server 包的场景,可单独安装 `@mearl/cloud-server`。
|
|
14
20
|
|
|
15
21
|
## 使用
|
|
@@ -17,6 +23,8 @@ npx @mearl/setup --cloud
|
|
|
17
23
|
### 命令行方式
|
|
18
24
|
|
|
19
25
|
Server **默认以后台守护进程方式运行**,启动后立即返回,并打印本地机器需要执行的连接命令。
|
|
26
|
+
Qoder 模式保持相同的 `start/status/restart/stop/logs` 交互;`start` 和 `restart` 生成新的
|
|
27
|
+
pair/connector 信息后退出,不监听端口或创建守护进程。
|
|
20
28
|
|
|
21
29
|
```bash
|
|
22
30
|
mearl-cloud-server [command] [options]
|
|
@@ -66,12 +74,13 @@ mearl-cloud-server --foreground
|
|
|
66
74
|
[CloudServer] Logs: ~/.mearl/cloud-server.log
|
|
67
75
|
|
|
68
76
|
Connect from your local machine:
|
|
69
|
-
npx @mearl/cloud-connector "ws://10.0.1.100:8080/ws?token=xxx"
|
|
77
|
+
npx @mearl/cloud-connector start "ws://10.0.1.100:8080/ws?token=xxx"
|
|
70
78
|
```
|
|
71
79
|
|
|
72
80
|
运行时文件位于 `~/.mearl/`:
|
|
73
81
|
|
|
74
82
|
- `cloud-server.json` - 连接配置(含 server URL、pid、端口等),统一 `@mearl/client` 自动读取
|
|
83
|
+
- `cloud-server-mode.json` - setup 在 Qoder 模式下写入的标识;普通 `--cloud` 会删除它
|
|
75
84
|
- `cloud-server.log` - 后台进程日志(`logs` 子命令读取,或 `tail -f` 跟踪);超过 5MB 会在下次启动时滚动为 `cloud-server.log.1`
|
|
76
85
|
|
|
77
86
|
> 守护进程的启停、状态、日志滚动等生命周期逻辑由共享包 [`@mearl/daemon-core`](../../daemon-core) 提供(与 cloud-connector 复用同一套实现)。
|
|
@@ -111,6 +120,10 @@ console.log(stats);
|
|
|
111
120
|
await server.stop();
|
|
112
121
|
```
|
|
113
122
|
|
|
123
|
+
Qoder 模式只复用 cloud-server CLI 作为建联入口。配对完成后的 action 继续通过 Qoder
|
|
124
|
+
Client-Side `mearl` 工具、Session API 和本地 connector 转发,不会连接一个本地
|
|
125
|
+
cloud-server 端口。
|
|
126
|
+
|
|
114
127
|
## 架构
|
|
115
128
|
|
|
116
129
|
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { join } from "node:path";
|
|
|
16
16
|
import { DAEMON_HOME } from "@mearl/daemon-core";
|
|
17
17
|
var CONFIG_DIR = DAEMON_HOME;
|
|
18
18
|
var CONFIG_FILE = join(CONFIG_DIR, "cloud-server.json");
|
|
19
|
+
var MODE_FILE = join(CONFIG_DIR, "cloud-server-mode.json");
|
|
19
20
|
var LOG_FILE = join(CONFIG_DIR, "cloud-server.log");
|
|
20
21
|
|
|
21
22
|
// src/index.ts
|
|
@@ -109,7 +110,7 @@ var CloudServer = class {
|
|
|
109
110
|
"[CloudServer] Connector traffic is using unencrypted ws://. Use a TLS-terminating proxy and wss:// when the connection crosses a trusted network."
|
|
110
111
|
);
|
|
111
112
|
}
|
|
112
|
-
const connectorCommand = `npx @mearl/cloud-connector "${publicWsUrl}"`;
|
|
113
|
+
const connectorCommand = `npx @mearl/cloud-connector start "${publicWsUrl}"`;
|
|
113
114
|
console.log(
|
|
114
115
|
`[CloudServer] Connector listener on 0.0.0.0:${this.options.port} (network); agent listener on 127.0.0.1:${this.options.agentPort} (local only)`
|
|
115
116
|
);
|
|
@@ -433,12 +434,13 @@ var CloudServer = class {
|
|
|
433
434
|
resolveConnector(selector) {
|
|
434
435
|
const connectors = this.getConnectorConnections();
|
|
435
436
|
if (connectors.length === 0) return { error: "No local connector available" };
|
|
437
|
+
const available = connectors.map(({ info }) => `${info.name} [${info.connectorId}]`).join(", ");
|
|
436
438
|
if (!selector) {
|
|
437
439
|
if (connectors.length === 1) {
|
|
438
440
|
return { ws: connectors[0].ws, info: connectors[0].info };
|
|
439
441
|
}
|
|
440
442
|
return {
|
|
441
|
-
error: `Multiple cloud connectors are connected (${connectors.length}).
|
|
443
|
+
error: `Multiple cloud connectors are connected (${connectors.length}). Pass --connector <id|name>. Available: ${available}`
|
|
442
444
|
};
|
|
443
445
|
}
|
|
444
446
|
const selection = resolveCloudConnectorSelector(
|
|
@@ -449,7 +451,6 @@ var CloudServer = class {
|
|
|
449
451
|
const matched = connectors.find(({ info }) => info === selection.connector);
|
|
450
452
|
return { ws: matched.ws, info: matched.info };
|
|
451
453
|
}
|
|
452
|
-
const available = connectors.map(({ info }) => `${info.name} [${info.connectorId}]`).join(", ");
|
|
453
454
|
if (selection.status === "ambiguous") {
|
|
454
455
|
return {
|
|
455
456
|
error: `Cloud connector selector "${selector}" is ambiguous. Available: ${available}`
|
|
@@ -563,7 +564,7 @@ function formatConnectInfo(record) {
|
|
|
563
564
|
const config = record;
|
|
564
565
|
const lines = [];
|
|
565
566
|
if (config.port) lines.push(` port: ${config.port}`);
|
|
566
|
-
const command2 = config.
|
|
567
|
+
const command2 = config.publicUrl ? `npx @mearl/cloud-connector start "${config.publicUrl}"` : config.connectorCommand;
|
|
567
568
|
if (command2) {
|
|
568
569
|
lines.push("");
|
|
569
570
|
lines.push(" Connect from your local machine:");
|
|
@@ -581,6 +582,9 @@ function daemon(foregroundArgs) {
|
|
|
581
582
|
formatInfo: formatConnectInfo
|
|
582
583
|
});
|
|
583
584
|
}
|
|
585
|
+
function isDaemonRunning() {
|
|
586
|
+
return daemon([]).running() !== null;
|
|
587
|
+
}
|
|
584
588
|
function startDaemon(passthroughArgs) {
|
|
585
589
|
return daemon(passthroughArgs).start();
|
|
586
590
|
}
|
|
@@ -619,8 +623,107 @@ async function runForeground(options) {
|
|
|
619
623
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
620
624
|
}
|
|
621
625
|
|
|
626
|
+
// src/mode.ts
|
|
627
|
+
import { mkdirSync as mkdirSync2, readFileSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
628
|
+
import { dirname } from "node:path";
|
|
629
|
+
var CLOUD_SERVER_MODE_ENV = "MEARL_CLOUD_MODE";
|
|
630
|
+
function isCloudServerMode(value) {
|
|
631
|
+
return value === "websocket" || value === "qoder";
|
|
632
|
+
}
|
|
633
|
+
function readStoredMode(storage) {
|
|
634
|
+
const modeFile = storage.modeFile ?? MODE_FILE;
|
|
635
|
+
const readText = storage.readText ?? ((filePath) => readFileSync(filePath, "utf8"));
|
|
636
|
+
try {
|
|
637
|
+
const value = JSON.parse(readText(modeFile));
|
|
638
|
+
return isCloudServerMode(value.mode) ? value.mode : null;
|
|
639
|
+
} catch {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function writeStoredMode(mode, storage) {
|
|
644
|
+
const modeFile = storage.modeFile ?? MODE_FILE;
|
|
645
|
+
const writeText = storage.writeText ?? ((filePath, value) => {
|
|
646
|
+
mkdirSync2(dirname(filePath), { recursive: true });
|
|
647
|
+
writeFileSync2(filePath, value, { encoding: "utf8", mode: 384 });
|
|
648
|
+
});
|
|
649
|
+
writeText(modeFile, `${JSON.stringify({ mode })}
|
|
650
|
+
`);
|
|
651
|
+
}
|
|
652
|
+
function clearStoredMode(storage) {
|
|
653
|
+
const modeFile = storage.modeFile ?? MODE_FILE;
|
|
654
|
+
const removeFile = storage.removeFile ?? ((filePath) => rmSync(filePath, { force: true }));
|
|
655
|
+
removeFile(modeFile);
|
|
656
|
+
}
|
|
657
|
+
function resolveCloudServerMode(env = process.env, storage = {}) {
|
|
658
|
+
const requested = env[CLOUD_SERVER_MODE_ENV];
|
|
659
|
+
if (requested !== void 0) {
|
|
660
|
+
if (!isCloudServerMode(requested)) {
|
|
661
|
+
throw new Error(`${CLOUD_SERVER_MODE_ENV} must be "websocket" or "qoder"`);
|
|
662
|
+
}
|
|
663
|
+
if (requested === "qoder") writeStoredMode(requested, storage);
|
|
664
|
+
else clearStoredMode(storage);
|
|
665
|
+
return requested;
|
|
666
|
+
}
|
|
667
|
+
return readStoredMode(storage) ?? "websocket";
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/qoder.ts
|
|
671
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
672
|
+
function createQoderPairingInfo(options = {}) {
|
|
673
|
+
const pairingCode = (options.createCode ?? (() => randomBytes2(24).toString("base64url")))();
|
|
674
|
+
if (!/^[A-Za-z0-9_-]{16,128}$/.test(pairingCode)) {
|
|
675
|
+
throw new Error("Qoder pairing code must be 16-128 URL-safe characters");
|
|
676
|
+
}
|
|
677
|
+
const createdAt = (options.now ?? Date.now)();
|
|
678
|
+
if (!Number.isSafeInteger(createdAt) || createdAt < 1e12 || createdAt > 9999999999999) {
|
|
679
|
+
throw new Error("Qoder pairing timestamp must be a 13-digit epoch millisecond value");
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
pairingCode,
|
|
683
|
+
createdAt,
|
|
684
|
+
connectorUrl: `qoder://pair/${pairingCode}?created=${createdAt}`,
|
|
685
|
+
toolInput: { action: "pair", payload: { code: pairingCode } }
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function formatQoderPairingOutput(info) {
|
|
689
|
+
const connectorCommand = `npx @mearl/cloud-connector start "${info.connectorUrl}"`;
|
|
690
|
+
return `[CloudServer] Qoder pairing mode is ready.
|
|
691
|
+
|
|
692
|
+
Connect from your local machine:
|
|
693
|
+
${connectorCommand}
|
|
694
|
+
|
|
695
|
+
Then immediately invoke the Qoder client-side custom tool named "mearl" with this input:
|
|
696
|
+
${JSON.stringify(info.toolInput)}
|
|
697
|
+
|
|
698
|
+
The connector resolves QODER_PERSONAL_ACCESS_TOKEN from the local environment, ~/.mearl/.env, or supported local credential storage. Never add the PAT to the pairing URL.`;
|
|
699
|
+
}
|
|
700
|
+
async function runQoderCloudServerCommand(command2, dependencies) {
|
|
701
|
+
const writeLine = dependencies.writeLine ?? console.log;
|
|
702
|
+
switch (command2) {
|
|
703
|
+
case "start":
|
|
704
|
+
case "restart":
|
|
705
|
+
if (dependencies.isServerRunning()) await dependencies.stopServer();
|
|
706
|
+
writeLine(
|
|
707
|
+
formatQoderPairingOutput(
|
|
708
|
+
(dependencies.createPairing ?? (() => createQoderPairingInfo()))()
|
|
709
|
+
)
|
|
710
|
+
);
|
|
711
|
+
break;
|
|
712
|
+
case "status":
|
|
713
|
+
dependencies.printStatus();
|
|
714
|
+
writeLine("[CloudServer] Mode: qoder pairing (no listening service)");
|
|
715
|
+
break;
|
|
716
|
+
case "stop":
|
|
717
|
+
await dependencies.stopServer();
|
|
718
|
+
break;
|
|
719
|
+
case "logs":
|
|
720
|
+
dependencies.printLogs();
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
622
725
|
// src/cli.ts
|
|
623
|
-
var CLOUD_SERVER_VERSION = true ? "2.7.
|
|
726
|
+
var CLOUD_SERVER_VERSION = true ? "2.7.2" : "unknown";
|
|
624
727
|
var argv = process.argv.slice(2);
|
|
625
728
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
626
729
|
console.log(CLOUD_SERVER_VERSION);
|
|
@@ -712,6 +815,16 @@ function parseOptions(args) {
|
|
|
712
815
|
return options;
|
|
713
816
|
}
|
|
714
817
|
async function main() {
|
|
818
|
+
const mode = resolveCloudServerMode();
|
|
819
|
+
if (mode === "qoder") {
|
|
820
|
+
await runQoderCloudServerCommand(command, {
|
|
821
|
+
isServerRunning: isDaemonRunning,
|
|
822
|
+
stopServer: stopDaemon,
|
|
823
|
+
printStatus: statusDaemon,
|
|
824
|
+
printLogs
|
|
825
|
+
});
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
715
828
|
if (foreground) {
|
|
716
829
|
await runForeground(parseOptions(rest));
|
|
717
830
|
return;
|
package/dist/daemon.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { CloudServerOptions } from './index.js';
|
|
2
|
+
/** Whether a live WebSocket daemon exists, independent from the selected transport mode. */
|
|
3
|
+
export declare function isDaemonRunning(): boolean;
|
|
2
4
|
/** Spawn the server as a detached background process. */
|
|
3
5
|
export declare function startDaemon(passthroughArgs: string[]): Promise<void>;
|
|
4
6
|
/** Stop a running background server. */
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { DAEMON_HOME } from "@mearl/daemon-core";
|
|
11
11
|
var CONFIG_DIR = DAEMON_HOME;
|
|
12
12
|
var CONFIG_FILE = join(CONFIG_DIR, "cloud-server.json");
|
|
13
|
+
var MODE_FILE = join(CONFIG_DIR, "cloud-server-mode.json");
|
|
13
14
|
var LOG_FILE = join(CONFIG_DIR, "cloud-server.log");
|
|
14
15
|
|
|
15
16
|
// src/index.ts
|
|
@@ -103,7 +104,7 @@ var CloudServer = class {
|
|
|
103
104
|
"[CloudServer] Connector traffic is using unencrypted ws://. Use a TLS-terminating proxy and wss:// when the connection crosses a trusted network."
|
|
104
105
|
);
|
|
105
106
|
}
|
|
106
|
-
const connectorCommand = `npx @mearl/cloud-connector "${publicWsUrl}"`;
|
|
107
|
+
const connectorCommand = `npx @mearl/cloud-connector start "${publicWsUrl}"`;
|
|
107
108
|
console.log(
|
|
108
109
|
`[CloudServer] Connector listener on 0.0.0.0:${this.options.port} (network); agent listener on 127.0.0.1:${this.options.agentPort} (local only)`
|
|
109
110
|
);
|
|
@@ -427,12 +428,13 @@ var CloudServer = class {
|
|
|
427
428
|
resolveConnector(selector) {
|
|
428
429
|
const connectors = this.getConnectorConnections();
|
|
429
430
|
if (connectors.length === 0) return { error: "No local connector available" };
|
|
431
|
+
const available = connectors.map(({ info }) => `${info.name} [${info.connectorId}]`).join(", ");
|
|
430
432
|
if (!selector) {
|
|
431
433
|
if (connectors.length === 1) {
|
|
432
434
|
return { ws: connectors[0].ws, info: connectors[0].info };
|
|
433
435
|
}
|
|
434
436
|
return {
|
|
435
|
-
error: `Multiple cloud connectors are connected (${connectors.length}).
|
|
437
|
+
error: `Multiple cloud connectors are connected (${connectors.length}). Pass --connector <id|name>. Available: ${available}`
|
|
436
438
|
};
|
|
437
439
|
}
|
|
438
440
|
const selection = resolveCloudConnectorSelector(
|
|
@@ -443,7 +445,6 @@ var CloudServer = class {
|
|
|
443
445
|
const matched = connectors.find(({ info }) => info === selection.connector);
|
|
444
446
|
return { ws: matched.ws, info: matched.info };
|
|
445
447
|
}
|
|
446
|
-
const available = connectors.map(({ info }) => `${info.name} [${info.connectorId}]`).join(", ");
|
|
447
448
|
if (selection.status === "ambiguous") {
|
|
448
449
|
return {
|
|
449
450
|
error: `Cloud connector selector "${selector}" is ambiguous. Available: ${available}`
|
package/dist/mode.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type CloudServerMode = 'websocket' | 'qoder';
|
|
2
|
+
export declare const CLOUD_SERVER_MODE_ENV = "MEARL_CLOUD_MODE";
|
|
3
|
+
interface CloudServerModeStorage {
|
|
4
|
+
modeFile?: string;
|
|
5
|
+
readText?: (filePath: string) => string;
|
|
6
|
+
removeFile?: (filePath: string) => void;
|
|
7
|
+
writeText?: (filePath: string, value: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function resolveCloudServerMode(env?: NodeJS.ProcessEnv, storage?: CloudServerModeStorage): CloudServerMode;
|
|
10
|
+
export {};
|
package/dist/paths.d.ts
CHANGED
|
@@ -9,5 +9,7 @@ export declare const CONFIG_DIR: string;
|
|
|
9
9
|
* Daemon lifecycle commands additionally read `pid` / `port` / `startedAt`.
|
|
10
10
|
*/
|
|
11
11
|
export declare const CONFIG_FILE: string;
|
|
12
|
+
/** Persistent transport mode selected by setup; independent from daemon liveness. */
|
|
13
|
+
export declare const MODE_FILE: string;
|
|
12
14
|
/** Daemon stdout/stderr log file. */
|
|
13
15
|
export declare const LOG_FILE: string;
|
package/dist/qoder.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface QoderPairingInfo {
|
|
2
|
+
pairingCode: string;
|
|
3
|
+
createdAt: number;
|
|
4
|
+
connectorUrl: string;
|
|
5
|
+
toolInput: {
|
|
6
|
+
action: 'pair';
|
|
7
|
+
payload: {
|
|
8
|
+
code: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface QoderPairingOptions {
|
|
13
|
+
now?: () => number;
|
|
14
|
+
createCode?: () => string;
|
|
15
|
+
}
|
|
16
|
+
export type CloudServerCommand = 'start' | 'stop' | 'restart' | 'status' | 'logs';
|
|
17
|
+
interface QoderCloudServerDependencies {
|
|
18
|
+
createPairing?: () => QoderPairingInfo;
|
|
19
|
+
isServerRunning: () => boolean;
|
|
20
|
+
printLogs: () => void;
|
|
21
|
+
printStatus: () => void;
|
|
22
|
+
stopServer: () => Promise<void>;
|
|
23
|
+
writeLine?: (message: string) => void;
|
|
24
|
+
}
|
|
25
|
+
/** Create a short-lived correlation handle without embedding Qoder credentials in the URL. */
|
|
26
|
+
export declare function createQoderPairingInfo(options?: QoderPairingOptions): QoderPairingInfo;
|
|
27
|
+
export declare function formatQoderPairingOutput(info: QoderPairingInfo): string;
|
|
28
|
+
export declare function runQoderCloudServerCommand(command: CloudServerCommand, dependencies: QoderCloudServerDependencies): Promise<void>;
|
|
29
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/cloud-server",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.2",
|
|
4
4
|
"description": "Cloud WebSocket server for Mearl — bridges cloud agents to local connectors",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"node-fetch": "^3.3.2",
|
|
31
31
|
"ws": "^8.18.0",
|
|
32
|
-
"@mearl/
|
|
33
|
-
"@mearl/client": "2.7.
|
|
34
|
-
"@mearl/
|
|
32
|
+
"@mearl/daemon-core": "2.7.2",
|
|
33
|
+
"@mearl/client": "2.7.2",
|
|
34
|
+
"@mearl/cloud-types": "2.7.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^24.9.1",
|