@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 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}). Run \`mearl connector_list\` and pass --connector <id|name>.`
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.connectorCommand || (config.publicUrl ? `npx @mearl/cloud-connector "${config.publicUrl}"` : null);
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.0" : "unknown";
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}). Run \`mearl connector_list\` and pass --connector <id|name>.`
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;
@@ -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.0",
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/cloud-types": "2.7.0",
33
- "@mearl/client": "2.7.0",
34
- "@mearl/daemon-core": "2.7.0"
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",