@mearl/plugin-qoder 3.0.0-beta.8 → 3.0.0

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.en.md ADDED
@@ -0,0 +1,27 @@
1
+ # @mearl/plugin-qoder
2
+
3
+ [简体中文](README.md) | [English](README.en.md)
4
+
5
+ Optional Qoder integration: local CLI/ACP agents and a Cloud Session API/SSE connector. The core does not depend on this package, register Qoder by default, read PATs, or launch the CLI. This is distributed as an independent npm package.
6
+
7
+ To use the local Agent, select Qoder in Agent Chat and install the missing components there. A Qoder capability in the selected preset takes precedence. Use a preset to enable cloud pairing.
8
+
9
+ The built `dist/capability/` contains self-contained Node modules, a digest manifest, and `preset.json`. Use the existing installation flow:
10
+
11
+ ```bash
12
+ npx @mearl/setup preset apply ./node_modules/@mearl/plugin-qoder/dist/capability/preset.json
13
+ ```
14
+
15
+ If you already use other capabilities, merge the `qoder` package declaration into your existing preset: `preset apply` selects the entire preset. `npm install` alone does not grant permissions or activate capabilities. Package resources can also be distributed over trusted HTTPS URLs.
16
+
17
+ After installation, local agents are discovered through `agent.provider.qoder`. Uninstalling blocks new sessions while accepted sessions can continue and close. If sessions exist during an update, the new runtime activates only after they close.
18
+
19
+ Generate a one-time Cloud pairing URL on the agent machine:
20
+
21
+ ```bash
22
+ npx @mearl/plugin-qoder pair
23
+ ```
24
+
25
+ Follow the output to start `mearl-cloud-connector` on the browser machine, then immediately call Qoder's custom `mearl` tool to complete pairing. The browser machine must have this capability installed and supply `QODER_PERSONAL_ACCESS_TOKEN`. Credentials resolve in this order: process environment, `MEARL_ENV_FILE`, `~/.mearl/.env`, and macOS Keychain `ai.mearl.qoder.pat`. Credentials are never written to pairing URLs, registration records, or logs.
26
+
27
+ After installing the capability, you can also use `npx @mearl/setup --cloud-mode qoder --yes` with the cloud-server management commands. This package supplies both `cloud.connector.qoder` and `cloud.server.qoder`; missing installations report the missing capability explicitly. ACP permission events use the standard `acp/requestPermission` contract.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @mearl/plugin-qoder
2
2
 
3
+ [简体中文](README.md) | [English](README.en.md)
4
+
3
5
  可选的 Qoder 接入:本地 CLI/ACP Agent 与 Cloud Session API/SSE 连接器。核心不依赖本包,也不默认注册 Qoder、读取 PAT 或启动 CLI。本包作为独立 npm 包分发。
4
6
 
5
7
  构建后的 `dist/capability/` 包含自包含 Node 模块、摘要清单与 `preset.json`。使用现有安装流程:
@@ -10,6 +12,8 @@ npx @mearl/setup preset apply ./node_modules/@mearl/plugin-qoder/dist/capability
10
12
 
11
13
  若已有其他能力,请将这里的 `qoder` 包声明合并进现有 preset;`preset apply` 选择的是整套预设。仅 `npm install` 不会自动获得权限或激活能力。包资源同样可通过受信任 HTTPS 地址分发。
12
14
 
15
+ 使用本地 Agent 时,可在 Agent 对话中选择 Qoder 并安装缺失组件。当前预设提供的 Qoder 能力优先,云端配对通过预设启用。
16
+
13
17
  本地 Agent 在安装后通过 `agent.provider.qoder` 被发现。卸载后禁止新会话;已接受会话仍可继续和关闭。更新期间有会话时,关闭会话后才启用新运行时。
14
18
 
15
19
  Cloud 配对在 Agent 机器生成一次性地址:
package/dist/agent.d.ts CHANGED
@@ -1,19 +1,11 @@
1
1
  import type { AgentInstalledSkill } from '@mearl/browser-core/agentSkills';
2
- import fs from 'node:fs';
3
2
  import type * as acp from '@agentclientprotocol/sdk';
4
3
  import type { AgentChatImageInput, AgentChatModelInfo, AgentChatProviderDescriptor } from '@mearl/browser-core/agentChat';
5
4
  import type { AgentProvider, AgentProviderEvent, AgentProviderListSessionsOptions, AgentProviderListSessionsResult, AgentProviderResumeResult, AgentProviderSessionOptions } from '@mearl/agent-runtime';
5
+ export { qoderExecutableCandidatePaths } from '@mearl/agent-runtime/execution';
6
6
  export declare function qoderModelInfo(configOptions: acp.SessionConfigOption[] | null | undefined): AgentChatModelInfo | undefined;
7
7
  export declare function parseQoderSkillsList(output: string, cwd: string, home?: string): AgentInstalledSkill[];
8
8
  export declare function qoderInstallCommand(platform?: NodeJS.Platform): string;
9
- interface QoderExecutableCandidateOptions {
10
- env?: NodeJS.ProcessEnv;
11
- homedir?: () => string;
12
- platform?: NodeJS.Platform;
13
- processExecPath?: string;
14
- readdirSync?: typeof fs.readdirSync;
15
- }
16
- export declare function qoderExecutableCandidatePaths(options?: QoderExecutableCandidateOptions): string[];
17
9
  export declare class QoderAgentProvider implements AgentProvider {
18
10
  private readonly log;
19
11
  private readonly clientVersion;
@@ -65,4 +57,3 @@ export declare class QoderAgentProvider implements AgentProvider {
65
57
  private resolveExecutableCandidates;
66
58
  private emit;
67
59
  }
68
- export {};
package/dist/agent.js CHANGED
@@ -1,10 +1,10 @@
1
- import fs from 'node:fs';
2
1
  import os from 'node:os';
3
2
  import path from 'node:path';
4
3
  import { spawn } from 'node:child_process';
5
4
  import { Readable, Writable } from 'node:stream';
6
- import { agentExecutableCandidatePaths, AgentExecutableResolver, executableFileCandidates, probeAgentExecutable, } from '@mearl/agent-runtime/execution';
5
+ import { AgentExecutableResolver, executableFileCandidates, probeAgentExecutable, qoderExecutableCandidatePaths, } from '@mearl/agent-runtime/execution';
7
6
  import { agentSpawnCommand, createAgentChildEnv } from '@mearl/agent-runtime/execution';
7
+ export { qoderExecutableCandidatePaths } from '@mearl/agent-runtime/execution';
8
8
  const PROBE_TIMEOUT_MS = 5_000;
9
9
  const REQUEST_TIMEOUT_MS = 30_000;
10
10
  const MAX_MODEL_CHARS = 256;
@@ -78,18 +78,6 @@ export function parseQoderSkillsList(output, cwd, home = os.homedir()) {
78
78
  finish();
79
79
  return skills;
80
80
  }
81
- function qoderManagedExecutableCandidates(home, readdirSync = fs.readdirSync) {
82
- const directory = path.join(home, '.qoder', 'bin', 'qodercli');
83
- try {
84
- return readdirSync(directory, { withFileTypes: true })
85
- .filter(entry => entry.isFile() && entry.name.startsWith('qodercli-'))
86
- .sort((left, right) => right.name.localeCompare(left.name, undefined, { numeric: true }))
87
- .map(entry => path.join(directory, entry.name));
88
- }
89
- catch {
90
- return [];
91
- }
92
- }
93
81
  function isPermissionResponse(value) {
94
82
  if (!value || typeof value !== 'object')
95
83
  return false;
@@ -105,25 +93,6 @@ export function qoderInstallCommand(platform = process.platform) {
105
93
  ? 'npm install -g @qoder-ai/qodercli'
106
94
  : 'curl -fsSL https://qoder.com/install | bash';
107
95
  }
108
- export function qoderExecutableCandidatePaths(options = {}) {
109
- const env = options.env ?? process.env;
110
- const home = (options.homedir ?? os.homedir)();
111
- const platform = options.platform ?? process.platform;
112
- const executableNames = platform === 'win32' ? ['qodercli.exe', 'qodercli.cmd'] : ['qodercli'];
113
- return agentExecutableCandidatePaths({
114
- executableNames,
115
- configuredPath: env.MEARL_QODER_PATH,
116
- env,
117
- extraUserCandidates: [
118
- ...qoderManagedExecutableCandidates(home, options.readdirSync),
119
- ...executableNames.map(name => path.join(home, '.qoder', 'bin', name)),
120
- ],
121
- home,
122
- platform,
123
- processExecPath: options.processExecPath,
124
- readdirSync: options.readdirSync,
125
- });
126
- }
127
96
  function sessionOptions(options) {
128
97
  const cwd = path.resolve(options.cwd || os.homedir());
129
98
  return {
@@ -7,8 +7,8 @@
7
7
  },
8
8
  "resources": {
9
9
  "runtime.cjs": {
10
- "sha256": "eaad2563840828ecfc0e8a2cda9ead4b3c58cc480d8001074b0622b0b8d33b0d",
11
- "byteLength": 1103773
10
+ "sha256": "1a603acc3c2e3f00922cb20f2b29cfe0a05a9a2a5d7f9388809e7a6cb52f6a14",
11
+ "byteLength": 1103931
12
12
  }
13
13
  },
14
14
  "contributions": {
@@ -302,10 +302,10 @@ function mergeDefs(...defs) {
302
302
  function cloneDef(schema) {
303
303
  return mergeDefs(schema._zod.def);
304
304
  }
305
- function getElementAtPath(obj, path4) {
306
- if (!path4)
305
+ function getElementAtPath(obj, path5) {
306
+ if (!path5)
307
307
  return obj;
308
- return path4.reduce((acc, key) => acc?.[key], obj);
308
+ return path5.reduce((acc, key) => acc?.[key], obj);
309
309
  }
310
310
  function promiseAllObject(promisesObj) {
311
311
  const keys = Object.keys(promisesObj);
@@ -564,11 +564,11 @@ function explicitlyAborted(x, startIndex = 0) {
564
564
  }
565
565
  return false;
566
566
  }
567
- function prefixIssues(path4, issues) {
567
+ function prefixIssues(path5, issues) {
568
568
  return issues.map((iss) => {
569
569
  var _a3;
570
570
  (_a3 = iss).path ?? (_a3.path = []);
571
- iss.path.unshift(path4);
571
+ iss.path.unshift(path5);
572
572
  return iss;
573
573
  });
574
574
  }
@@ -1101,16 +1101,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
1101
1101
  }
1102
1102
  function formatError(error62, mapper = (issue2) => issue2.message) {
1103
1103
  const fieldErrors = { _errors: [] };
1104
- const processError = (error63, path4 = []) => {
1104
+ const processError = (error63, path5 = []) => {
1105
1105
  for (const issue2 of error63.issues) {
1106
1106
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1107
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
1107
+ issue2.errors.map((issues) => processError({ issues }, [...path5, ...issue2.path]));
1108
1108
  } else if (issue2.code === "invalid_key") {
1109
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
1109
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
1110
1110
  } else if (issue2.code === "invalid_element") {
1111
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
1111
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
1112
1112
  } else {
1113
- const fullpath = [...path4, ...issue2.path];
1113
+ const fullpath = [...path5, ...issue2.path];
1114
1114
  if (fullpath.length === 0) {
1115
1115
  fieldErrors._errors.push(mapper(issue2));
1116
1116
  } else {
@@ -1149,17 +1149,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
1149
1149
  }
1150
1150
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
1151
1151
  const result = { errors: [] };
1152
- const processError = (error63, path4 = []) => {
1152
+ const processError = (error63, path5 = []) => {
1153
1153
  var _a3;
1154
1154
  for (const issue2 of error63.issues) {
1155
1155
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1156
- issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
1156
+ issue2.errors.map((issues) => processError({ issues }, [...path5, ...issue2.path]));
1157
1157
  } else if (issue2.code === "invalid_key") {
1158
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
1158
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
1159
1159
  } else if (issue2.code === "invalid_element") {
1160
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
1160
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
1161
1161
  } else {
1162
- const fullpath = [...path4, ...issue2.path];
1162
+ const fullpath = [...path5, ...issue2.path];
1163
1163
  if (fullpath.length === 0) {
1164
1164
  result.errors.push(mapper(issue2));
1165
1165
  continue;
@@ -1198,8 +1198,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
1198
1198
  }
1199
1199
  function toDotPath(_path) {
1200
1200
  const segs = [];
1201
- const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1202
- for (const seg of path4) {
1201
+ const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1202
+ for (const seg of path5) {
1203
1203
  if (typeof seg === "number")
1204
1204
  segs.push(`[${seg}]`);
1205
1205
  else if (typeof seg === "symbol")
@@ -19231,13 +19231,13 @@ function resolveRef(ref, ctx) {
19231
19231
  if (!ref.startsWith("#")) {
19232
19232
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
19233
19233
  }
19234
- const path4 = ref.slice(1).split("/").filter(Boolean);
19235
- if (path4.length === 0) {
19234
+ const path5 = ref.slice(1).split("/").filter(Boolean);
19235
+ if (path5.length === 0) {
19236
19236
  return ctx.rootSchema;
19237
19237
  }
19238
19238
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
19239
- if (path4[0] === defsKey) {
19240
- const key = path4[1] === void 0 ? void 0 : decodeJSONPointerSegment(path4[1]);
19239
+ if (path5[0] === defsKey) {
19240
+ const key = path5[1] === void 0 ? void 0 : decodeJSONPointerSegment(path5[1]);
19241
19241
  if (!key || !ctx.defs[key]) {
19242
19242
  throw new Error(`Reference not found: ${ref}`);
19243
19243
  }
@@ -25031,9 +25031,8 @@ __export(runtime_exports, {
25031
25031
  module.exports = __toCommonJS(runtime_exports);
25032
25032
 
25033
25033
  // src/agent.ts
25034
- var import_node_fs3 = __toESM(require("node:fs"), 1);
25035
- var import_node_os2 = __toESM(require("node:os"), 1);
25036
- var import_node_path4 = __toESM(require("node:path"), 1);
25034
+ var import_node_os3 = __toESM(require("node:os"), 1);
25035
+ var import_node_path5 = __toESM(require("node:path"), 1);
25037
25036
  var import_node_child_process2 = require("node:child_process");
25038
25037
  var import_node_stream = require("node:stream");
25039
25038
 
@@ -25292,6 +25291,38 @@ function isExecutableFile(candidate) {
25292
25291
  }
25293
25292
  }
25294
25293
 
25294
+ // ../agent-runtime/dist/qoder-executable.js
25295
+ var import_node_fs3 = __toESM(require("node:fs"), 1);
25296
+ var import_node_os2 = __toESM(require("node:os"), 1);
25297
+ var import_node_path4 = __toESM(require("node:path"), 1);
25298
+ function qoderManagedExecutableCandidates(home, readdirSync = import_node_fs3.default.readdirSync) {
25299
+ const directory = import_node_path4.default.join(home, ".qoder", "bin", "qodercli");
25300
+ try {
25301
+ return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.startsWith("qodercli-")).sort((left, right) => right.name.localeCompare(left.name, void 0, { numeric: true })).map((entry) => import_node_path4.default.join(directory, entry.name));
25302
+ } catch {
25303
+ return [];
25304
+ }
25305
+ }
25306
+ function qoderExecutableCandidatePaths(options = {}) {
25307
+ const env = options.env ?? process.env;
25308
+ const home = (options.homedir ?? import_node_os2.default.homedir)();
25309
+ const platform = options.platform ?? process.platform;
25310
+ const executableNames = platform === "win32" ? ["qodercli.exe", "qodercli.cmd"] : ["qodercli"];
25311
+ return agentExecutableCandidatePaths({
25312
+ executableNames,
25313
+ configuredPath: env.MEARL_QODER_PATH,
25314
+ env,
25315
+ extraUserCandidates: [
25316
+ ...qoderManagedExecutableCandidates(home, options.readdirSync),
25317
+ ...executableNames.map((name) => import_node_path4.default.join(home, ".qoder", "bin", name))
25318
+ ],
25319
+ home,
25320
+ platform,
25321
+ processExecPath: options.processExecPath,
25322
+ readdirSync: options.readdirSync
25323
+ });
25324
+ }
25325
+
25295
25326
  // src/agent.ts
25296
25327
  var PROBE_TIMEOUT_MS = 5e3;
25297
25328
  var REQUEST_TIMEOUT_MS = 3e4;
@@ -25325,14 +25356,14 @@ function qoderModelInfo(configOptions) {
25325
25356
  ...displayName && displayName !== id ? { displayName } : {}
25326
25357
  };
25327
25358
  }
25328
- function parseQoderSkillsList(output2, cwd, home = import_node_os2.default.homedir()) {
25359
+ function parseQoderSkillsList(output2, cwd, home = import_node_os3.default.homedir()) {
25329
25360
  const skills = [];
25330
25361
  let current;
25331
25362
  const finish = () => {
25332
25363
  if (!current) return;
25333
25364
  const skillPath = current.path || (current.builtIn ? "bundled" : "");
25334
- const relativeToCwd = skillPath && import_node_path4.default.isAbsolute(skillPath) ? import_node_path4.default.relative(cwd, skillPath) : "";
25335
- const inProject = Boolean(relativeToCwd) && relativeToCwd !== ".." && !relativeToCwd.startsWith(`..${import_node_path4.default.sep}`) && !import_node_path4.default.isAbsolute(relativeToCwd);
25365
+ const relativeToCwd = skillPath && import_node_path5.default.isAbsolute(skillPath) ? import_node_path5.default.relative(cwd, skillPath) : "";
25366
+ const inProject = Boolean(relativeToCwd) && relativeToCwd !== ".." && !relativeToCwd.startsWith(`..${import_node_path5.default.sep}`) && !import_node_path5.default.isAbsolute(relativeToCwd);
25336
25367
  skills.push({
25337
25368
  name: current.name,
25338
25369
  path: skillPath,
@@ -25357,14 +25388,6 @@ function parseQoderSkillsList(output2, cwd, home = import_node_os2.default.homed
25357
25388
  finish();
25358
25389
  return skills;
25359
25390
  }
25360
- function qoderManagedExecutableCandidates(home, readdirSync = import_node_fs3.default.readdirSync) {
25361
- const directory = import_node_path4.default.join(home, ".qoder", "bin", "qodercli");
25362
- try {
25363
- return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.startsWith("qodercli-")).sort((left, right) => right.name.localeCompare(left.name, void 0, { numeric: true })).map((entry) => import_node_path4.default.join(directory, entry.name));
25364
- } catch {
25365
- return [];
25366
- }
25367
- }
25368
25391
  function isPermissionResponse(value) {
25369
25392
  if (!value || typeof value !== "object") return false;
25370
25393
  const outcome = value.outcome;
@@ -25375,27 +25398,8 @@ function isPermissionResponse(value) {
25375
25398
  function qoderInstallCommand(platform = process.platform) {
25376
25399
  return platform === "win32" ? "npm install -g @qoder-ai/qodercli" : "curl -fsSL https://qoder.com/install | bash";
25377
25400
  }
25378
- function qoderExecutableCandidatePaths(options = {}) {
25379
- const env = options.env ?? process.env;
25380
- const home = (options.homedir ?? import_node_os2.default.homedir)();
25381
- const platform = options.platform ?? process.platform;
25382
- const executableNames = platform === "win32" ? ["qodercli.exe", "qodercli.cmd"] : ["qodercli"];
25383
- return agentExecutableCandidatePaths({
25384
- executableNames,
25385
- configuredPath: env.MEARL_QODER_PATH,
25386
- env,
25387
- extraUserCandidates: [
25388
- ...qoderManagedExecutableCandidates(home, options.readdirSync),
25389
- ...executableNames.map((name) => import_node_path4.default.join(home, ".qoder", "bin", name))
25390
- ],
25391
- home,
25392
- platform,
25393
- processExecPath: options.processExecPath,
25394
- readdirSync: options.readdirSync
25395
- });
25396
- }
25397
25401
  function sessionOptions(options) {
25398
- const cwd = import_node_path4.default.resolve(options.cwd || import_node_os2.default.homedir());
25402
+ const cwd = import_node_path5.default.resolve(options.cwd || import_node_os3.default.homedir());
25399
25403
  return {
25400
25404
  ...options,
25401
25405
  cwd,
@@ -25620,7 +25624,7 @@ var QoderAgentProvider = class {
25620
25624
  throw new Error(resolution.error);
25621
25625
  }
25622
25626
  const { executable } = resolution;
25623
- const resolvedCwd = cwd || import_node_os2.default.homedir();
25627
+ const resolvedCwd = cwd || import_node_os3.default.homedir();
25624
25628
  const output2 = await this.runSkillsList(executable, resolvedCwd);
25625
25629
  return parseQoderSkillsList(output2, resolvedCwd);
25626
25630
  }
@@ -26360,27 +26364,27 @@ function scrollElementInPage(direction, distance, targetLabel, containerPolicy =
26360
26364
  } catch {
26361
26365
  }
26362
26366
  }
26363
- const path4 = [];
26367
+ const path5 = [];
26364
26368
  let cursor = candidate;
26365
- while (cursor && cursor.nodeType === 1 && path4.length < 8) {
26369
+ while (cursor && cursor.nodeType === 1 && path5.length < 8) {
26366
26370
  const tag = String(cursor.tagName || "").toLowerCase();
26367
26371
  if (!tag)
26368
26372
  break;
26369
26373
  const parent = cursor.parentElement;
26370
26374
  if (!parent) {
26371
- path4.unshift(tag);
26375
+ path5.unshift(tag);
26372
26376
  break;
26373
26377
  }
26374
26378
  const siblings = Array.from(parent.children).filter((sibling) => sibling.tagName === cursor.tagName);
26375
26379
  const index = siblings.indexOf(cursor) + 1;
26376
- path4.unshift(siblings.length > 1 ? tag + ":nth-of-type(" + index + ")" : tag);
26380
+ path5.unshift(siblings.length > 1 ? tag + ":nth-of-type(" + index + ")" : tag);
26377
26381
  cursor = parent;
26378
26382
  if (cursor === documentRef.documentElement) {
26379
- path4.unshift("html");
26383
+ path5.unshift("html");
26380
26384
  break;
26381
26385
  }
26382
26386
  }
26383
- return path4.join(" > ");
26387
+ return path5.join(" > ");
26384
26388
  }
26385
26389
  let metrics = scrollMetrics(element);
26386
26390
  if (!metrics)
@@ -26550,27 +26554,27 @@ function inspectElementsInPage(selector, filter, styles, maxResults, invalidSele
26550
26554
  } catch {
26551
26555
  }
26552
26556
  }
26553
- const path4 = [];
26557
+ const path5 = [];
26554
26558
  let cursor = element;
26555
- while (cursor && cursor.nodeType === 1 && path4.length < 8) {
26559
+ while (cursor && cursor.nodeType === 1 && path5.length < 8) {
26556
26560
  const tag = String(cursor.tagName || "").toLowerCase();
26557
26561
  if (!tag)
26558
26562
  break;
26559
26563
  const parent = cursor.parentElement;
26560
26564
  if (!parent) {
26561
- path4.unshift(tag);
26565
+ path5.unshift(tag);
26562
26566
  break;
26563
26567
  }
26564
26568
  const siblings = Array.from(parent.children).filter((sibling) => sibling.tagName === cursor.tagName);
26565
26569
  const index = siblings.indexOf(cursor) + 1;
26566
- path4.unshift(siblings.length > 1 ? tag + ":nth-of-type(" + index + ")" : tag);
26570
+ path5.unshift(siblings.length > 1 ? tag + ":nth-of-type(" + index + ")" : tag);
26567
26571
  cursor = parent;
26568
26572
  if (cursor === documentRef.documentElement) {
26569
- path4.unshift("html");
26573
+ path5.unshift("html");
26570
26574
  break;
26571
26575
  }
26572
26576
  }
26573
- return path4.join(" > ");
26577
+ return path5.join(" > ");
26574
26578
  }
26575
26579
  function readStyle(style, property) {
26576
26580
  const cssValue = style.getPropertyValue?.(property);
@@ -26999,8 +27003,8 @@ function resolveActionTimeoutSec(action, data, fallback = 15) {
26999
27003
 
27000
27004
  // src/auth.ts
27001
27005
  var import_node_fs4 = require("node:fs");
27002
- var import_node_os3 = require("node:os");
27003
- var import_node_path5 = require("node:path");
27006
+ var import_node_os4 = require("node:os");
27007
+ var import_node_path6 = require("node:path");
27004
27008
  var import_node_child_process3 = require("node:child_process");
27005
27009
  var QODER_TOKEN_KEY = "QODER_PERSONAL_ACCESS_TOKEN";
27006
27010
  var QODER_KEYCHAIN_SERVICE = "ai.mearl.qoder.pat";
@@ -27040,7 +27044,7 @@ function readMacKeychainToken() {
27040
27044
  const securityPath = "/usr/bin/security";
27041
27045
  if (!(0, import_node_fs4.existsSync)(securityPath)) return void 0;
27042
27046
  try {
27043
- const account = (0, import_node_os3.userInfo)().username;
27047
+ const account = (0, import_node_os4.userInfo)().username;
27044
27048
  const result = (0, import_node_child_process3.spawnSync)(
27045
27049
  securityPath,
27046
27050
  ["find-generic-password", "-a", account, "-s", QODER_KEYCHAIN_SERVICE, "-w"],
@@ -27062,10 +27066,10 @@ function resolveQoderPersonalAccessToken(options = {}) {
27062
27066
  const env = options.env ?? process.env;
27063
27067
  const inherited = normalizeToken(env[QODER_TOKEN_KEY]);
27064
27068
  if (inherited) return inherited;
27065
- const homeDirectory = options.homeDirectory ?? (0, import_node_os3.homedir)();
27069
+ const homeDirectory = options.homeDirectory ?? (0, import_node_os4.homedir)();
27066
27070
  const envFiles = [
27067
- ...env.MEARL_ENV_FILE?.trim() ? [(0, import_node_path5.resolve)(env.MEARL_ENV_FILE)] : [],
27068
- (0, import_node_path5.resolve)(homeDirectory, ".mearl", ".env")
27071
+ ...env.MEARL_ENV_FILE?.trim() ? [(0, import_node_path6.resolve)(env.MEARL_ENV_FILE)] : [],
27072
+ (0, import_node_path6.resolve)(homeDirectory, ".mearl", ".env")
27069
27073
  ];
27070
27074
  const readFile = options.readFile ?? ((file2) => (0, import_node_fs4.readFileSync)(file2, "utf8"));
27071
27075
  for (const file2 of new Set(envFiles)) {
@@ -27208,8 +27212,8 @@ var QoderApiClient = class {
27208
27212
  token;
27209
27213
  fetchImpl;
27210
27214
  baseUrl;
27211
- async request(path4, init = {}) {
27212
- const response = await this.fetchImpl(`${this.baseUrl}${path4}`, {
27215
+ async request(path5, init = {}) {
27216
+ const response = await this.fetchImpl(`${this.baseUrl}${path5}`, {
27213
27217
  ...init,
27214
27218
  headers: {
27215
27219
  Authorization: `Bearer ${this.token}`,
@@ -0,0 +1,52 @@
1
+ # Qoder Cloud Agent pairing
2
+
3
+ [简体中文](cloud-pairing.md) | [English](cloud-pairing.en.md)
4
+
5
+ Use this mode when running in Qoder Cloud Agent. Other remote agents use cloud-server / WebSocket. Install the Qoder capability on the browser machine as described in this package's README. The Agent machine can generate pairing information directly with this package. Both modes share Mearl action contracts.
6
+
7
+ Qoder mode uses official Client-Side custom tools and Session event APIs, without Cloudflare Tunnel or treating Qoder sandbox public endpoints as WebSockets. Follow this sequence:
8
+
9
+ 1. Check whether the Agent already exposes a Client-Side custom tool named `mearl`. If so, run:
10
+
11
+ ```bash
12
+ npx @mearl/plugin-qoder pair
13
+ ```
14
+
15
+ Read the local `npx @mearl/cloud-connector start "qoder://pair/..."` command and `pair` input from output. Show the local command to the user, then immediately submit the generated `pair` input to the `mearl` custom tool. It pauses until the user runs the connection command on the browser machine and pairing completes.
16
+
17
+ 2. After pairing, do not run `mearl ...` shell commands inside the Qoder sandbox for this Session's browser operations. Internally map the Skill's action, payload, browser, and timeout to custom-tool calls. Deliver ordinary Mearl results to users without exposing transport differences.
18
+
19
+ 3. Without a `mearl` tool, first determine whether available Qoder PAT and Agent ID/version permit reading the complete existing `tools` array, appending the configuration below, and updating the Agent. Do not overwrite other tools. If permissions or information are missing, guide the user to add:
20
+
21
+ ```json
22
+ {
23
+ "type": "custom",
24
+ "name": "mearl",
25
+ "description": "Invoke a Mearl browser action on the user's paired local browser. Use action=pair only with a code generated by @mearl/plugin-qoder pair.",
26
+ "input_schema": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "properties": {
30
+ "action": { "type": "string" },
31
+ "payload": { "type": "object" },
32
+ "browser": { "type": "string" },
33
+ "timeoutSec": { "type": "number", "minimum": 1 }
34
+ },
35
+ "required": ["action"]
36
+ }
37
+ }
38
+ ```
39
+
40
+ Tool configuration belongs to the Agent and affects new Sessions only. Tell users to create a new Session and restart the normal flow rather than repeatedly retrying the current Session.
41
+
42
+ 4. Only if the local connector reports missing authentication, 401, or an expired PAT, follow its error to configure a Qoder PAT on the browser machine and restart. Errors provide supported credential sources and configuration. Do not ask users to send PATs to the Agent or put them in command URLs, chat, or logs. If users proactively provide a PAT and explicitly request configuration, write a supported source without repeating it.
43
+
44
+ 5. `page_screenshot` prefers Qoder image-content blocks while preserving metadata. If the API version rejects image blocks, connector falls back to JSON text containing original base64 so image data is not lost.
45
+
46
+ ---
47
+
48
+ <a id="common"></a>
49
+
50
+ ## Common issues
51
+
52
+ <a id="timeout"></a>
@@ -1,5 +1,7 @@
1
1
  # Qoder Cloud Agent 配对模式
2
2
 
3
+ [简体中文](cloud-pairing.md) | [English](cloud-pairing.en.md)
4
+
3
5
  当前运行于 Qoder Cloud Agent 时使用本模式;其他远端 Agent 使用 cloud-server / WebSocket 模式。浏览器机器先按本包 README 安装 Qoder 能力,Agent 机器可直接使用本包生成配对信息;两种模式保持相同的 Mearl action 契约。
4
6
 
5
7
  Qoder 模式使用官方 Client-Side 自定义工具和 Session 事件接口,不依赖 Cloudflare Tunnel,也不把 Qoder 沙箱公网入口当作 WebSocket。Agent 按以下顺序处理:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/plugin-qoder",
3
- "version": "3.0.0-beta.8",
3
+ "version": "3.0.0",
4
4
  "description": "Optional Qoder CLI agent and cloud connector capability for Mearl",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -16,6 +16,7 @@
16
16
  }
17
17
  },
18
18
  "files": [
19
+ "README.en.md",
19
20
  "dist",
20
21
  "docs",
21
22
  "!dist/**/__tests__/**",
@@ -26,10 +27,10 @@
26
27
  },
27
28
  "dependencies": {
28
29
  "@agentclientprotocol/sdk": "^1.2.1",
29
- "@mearl/browser-core": "3.0.0-beta.8",
30
- "@mearl/client": "3.0.0-beta.8",
31
- "@mearl/agent-runtime": "3.0.0-beta.8",
32
- "@mearl/cloud-connector": "3.0.0-beta.8"
30
+ "@mearl/agent-runtime": "3.0.0",
31
+ "@mearl/cloud-connector": "3.0.0",
32
+ "@mearl/browser-core": "3.0.0",
33
+ "@mearl/client": "3.0.0"
33
34
  },
34
35
  "devDependencies": {
35
36
  "typescript": "^5.8.3",