@chenacc/agentbridge 0.2.1 → 0.2.3

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/.env.example CHANGED
@@ -63,3 +63,8 @@ AGENTBRIDGE_PROJECTS=agentbridge=D:\codex_project\手机远程操控agent
63
63
  # AGENTBRIDGE_NOTIFY_TOKEN=your-provider-token
64
64
  # AGENTBRIDGE_NOTIFY_CHANNEL=clawbot
65
65
  # AGENTBRIDGE_NOTIFY_LINK=1
66
+
67
+ # The bridge reads notify.json from its data directory. If that directory moved
68
+ # (the DSH plugin gives the bridge its own state folder), an older folder can be
69
+ # adopted once instead of re-entering the token by hand:
70
+ # AGENTBRIDGE_NOTIFY_IMPORT=D:\codex_project\agentbridge\.agentbridge
package/README.md CHANGED
@@ -8,23 +8,29 @@
8
8
 
9
9
  **一条命令,把手机变成这台电脑上 Agent 的遥控器。** 微信扫码即用,不用装 App,也不用和电脑连同一个 Wi-Fi。
10
10
 
11
- | 桌面端在干活 | 手机扫一扫就接管 | 手机端同一份对话 |
12
- |---|---|---|
13
- | <img src="./docs/assets/screen-desktop-console.jpg" alt="桌面端控制台" width="330" /> | <img src="./docs/assets/screen-mobile-1.png" alt="手机端控制台 1" width="180" /> | <img src="./docs/assets/screen-mobile-2.png" alt="手机端控制台 2" width="180" /> |
11
+ <img src="./docs/assets/hero-phone.jpg" alt="手机端控制台:语音/文字发指令,Agent 流式回复,与桌面同一份对话" width="240" align="right" />
12
+
13
+ 在电脑上跑着的 Claude Code / Codex / DeepSeek Harness / DSH Desktop 会话,执行 `/phone` 会出现一次性二维码:
14
+
15
+ - 手机扫码后**接管的就是屏幕上那一条对话**,不是新建一个"类似的"会话;
16
+ - 消息、工具调用、审批卡片、模型切换、停止任务,两端看到的是同一份事件流;
17
+ - 电脑始终是唯一执行端,凭据和文件访问不出本机。
14
18
 
15
19
  ```powershell
16
20
  npx @chenacc/agentbridge # 起桥并直接打印二维码
17
21
  ```
18
22
 
19
- 然后在 Claude Code / Codex / DSH Desktop 里输入 `/phone`,扫二维码即可。凭据和文件访问始终留在本机,手机只下发命令。
23
+ 然后在 Claude Code / Codex / DSH Desktop 里输入 `/phone`,扫二维码即可。
20
24
 
21
25
  > 觉得有用就给个 star:[github.com/chenacc1/agentbridge](https://github.com/chenacc1/agentbridge) —— 不点也不影响任何功能,只影响我有没有动力继续写。
22
26
 
27
+ ![桌面端控制台:左侧是 Agent / 项目 / 会话三级导航,右侧是同一段对话与工具活动](./docs/assets/hero-desktop.jpg)
28
+
23
29
  手机与桌面共享同一份可追加事件流,因此消息、工具活动、审批和任务状态始终同步。电脑是唯一的执行端:手机只运行界面,不运行 Agent,也拿不到任何供应商凭据。
24
30
 
25
31
  默认走 Cloudflare 隧道:手机用流量、在外地都能打开;隧道对外开放,因此自动带一道访问密钥门禁。
26
32
 
27
- **当前版本:** 桥 `@chenacc/agentbridge` **0.2.0** · DSH 插件 `@chenacc/dsh-agentbridge` **0.4.0**(两个包各自发布,见文末「版本与发布」)。
33
+ **当前版本:** 桥 `@chenacc/agentbridge` **0.2.1** · DSH 插件 `@chenacc/dsh-agentbridge` **0.4.1**(两个包各自发布,见文末「版本与发布」)。
28
34
 
29
35
  ## 它是怎么工作的
30
36
 
@@ -380,6 +386,9 @@ agentbridge phone --agent deepseek-harness
380
386
  | `AGENTBRIDGE_SESSION_TTL_MS` | `259200000` | 设备会话有效期(72 小时) |
381
387
  | `AGENTBRIDGE_CLAUDE_MODEL` | 供应商默认 | 可选的 Claude / Kimi 模型 ID |
382
388
  | `AGENTBRIDGE_NOTIFY_WEBHOOK` | 未设置 | 通知 Webhook;未设置时该功能静默 |
389
+ | `AGENTBRIDGE_NOTIFY_IMPORT` | 未设置 | 通知设置存在旧数据目录时,指向它一次即可自动迁过来 |
390
+
391
+ > 通知设置(webhook / token / 静默开关)保存在**桥的数据目录**下的 `notify.json`,默认是 `%USERPROFILE%\.agentbridge`。DSH 插件接管网关后数据目录换过位置,老配置如果留在原来的 `.agentbridge` 里,用 `AGENTBRIDGE_NOTIFY_IMPORT=<旧目录>` 启动一次就会自动迁过去;也可以直接在桌面控制台的「通知」面板重填一次。桥启动日志里有一行 `Notifications: pushplus/clawbot` 或 `Notifications: off — no webhook configured in …`,能直接看出它到底读到了哪个文件。
383
392
 
384
393
  **跨网络(CLI 与插件都认):**
385
394
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenacc/agentbridge",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Control local coding agents from a phone through a secure WeChat QR web console.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -65,8 +65,9 @@
65
65
  "verify:responsive": "node scripts/verify-responsive.mjs",
66
66
  "verify:guide": "node scripts/verify-product-guide.mjs",
67
67
  "verify:release": "node scripts/verify-plugin-release.mjs",
68
- "check": "node --check src/server.js && node --check src/cli.js && node --check src/config.js && node --check src/cc-switch-models.js && node --check src/workspaces/workspace-catalog.js && node --check src/conversations/conversation-catalog.js && node --check src/adapters/capabilities.js && node --check src/adapters/claude.js && node --check src/adapters/codex.js && node --check src/adapters/deepseek-harness.js && node --check src/adapters/dsh-desktop.js && node --check src/dsh-desktop-channels.js && node --check src/event-store.js && node --check src/notifier.js && node --check src/attachments.js && node --check src/session-manager.js && node --check integrations/dsh-phone/lib/index.js && node --check src/tunnel.js && node --check src/remote.js && node --check src/adapters/zcode-native.js && node --check public/adapter-capabilities.js && node --check public/workspace-navigation.js && node --check public/markdown.js && node --check public/highlight.js && node --check public/transcript.js && node --check public/app.js && node --check scripts/verify-workspace-ui.mjs && node --check scripts/verify-message-blocks.mjs && node --check scripts/verify-background-scrim.mjs && node --check scripts/verify-model-control.mjs && node --check scripts/verify-notifications.mjs && node --check scripts/verify-attachments.mjs && node --check scripts/verify-features.mjs && node --check scripts/verify-quick-nav.mjs && node --check scripts/verify-questions.mjs && node --check scripts/verify-responsive.mjs && node --check scripts/verify-product-guide.mjs && node --check scripts/verify-plugin-release.mjs",
69
- "verify:remote": "node scripts/verify-remote-tunnel.mjs"
68
+ "check": "node --check src/server.js && node --check src/cli.js && node --check src/config.js && node --check src/cc-switch-models.js && node --check src/workspaces/workspace-catalog.js && node --check src/conversations/conversation-catalog.js && node --check src/adapters/capabilities.js && node --check src/adapters/claude.js && node --check src/adapters/codex.js && node --check src/adapters/deepseek-harness.js && node --check src/adapters/dsh-desktop.js && node --check src/dsh-desktop-channels.js && node --check src/event-store.js && node --check src/notifier.js && node --check src/attachments.js && node --check src/session-manager.js && node --check integrations/dsh-phone/lib/index.js && node --check src/tunnel.js && node --check src/remote.js && node --check src/adapters/zcode-native.js && node --check public/adapter-capabilities.js && node --check public/workspace-navigation.js && node --check public/markdown.js && node --check public/highlight.js && node --check public/transcript.js && node --check public/app.js && node --check scripts/verify-workspace-ui.mjs && node --check scripts/verify-message-blocks.mjs && node --check scripts/verify-background-scrim.mjs && node --check scripts/verify-model-control.mjs && node --check scripts/verify-notifications.mjs && node --check scripts/verify-attachments.mjs && node --check scripts/verify-features.mjs && node --check scripts/verify-quick-nav.mjs && node --check scripts/verify-questions.mjs && node --check scripts/verify-responsive.mjs && node --check scripts/verify-product-guide.mjs && node --check scripts/verify-plugin-release.mjs && node --check scripts/capture-screenshots.mjs",
69
+ "verify:remote": "node scripts/verify-remote-tunnel.mjs",
70
+ "capture:shots": "node scripts/capture-screenshots.mjs"
70
71
  },
71
72
  "dependencies": {
72
73
  "katex": "0.19.1",
package/src/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
- import { mkdir, writeFile } from "node:fs/promises";
3
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
@@ -52,23 +52,37 @@ Tell the user to scan the QR with WeChat. While the phone controls this session,
52
52
  `;
53
53
  }
54
54
 
55
+ /**
56
+ * The Codex skill is named `phone-codex`, not `phone`.
57
+ *
58
+ * `~/.agents/skills` is shared: Codex reads it, and so does DSH Desktop. A skill
59
+ * called `phone` therefore shadows the DSH plugin's `/phone` command, and typing
60
+ * `/phone` in DSH Desktop runs `phone --agent codex` — pairing a Codex
61
+ * conversation instead of the one on screen. One name per client: the plugin owns
62
+ * `/phone`, this skill answers `/phone-codex`.
63
+ */
64
+ export const CODEX_SKILL_NAME = "phone-codex";
65
+ const CODEX_SKILL_MARKER = "# Send a Codex project to AgentBridge";
66
+
55
67
  export function renderCodexPhoneSkill(entryPath = cliEntry) {
56
68
  const portableEntry = entryPath.replaceAll("\\", "/").replaceAll('"', '\\"');
57
69
  return `---
58
- name: phone
59
- description: Generate a one-time WeChat QR code for a Codex project conversation. Use only when the user explicitly invokes /phone.
70
+ name: ${CODEX_SKILL_NAME}
71
+ description: Generate a one-time WeChat QR code for a Codex project conversation. Use only when the user explicitly invokes /${CODEX_SKILL_NAME}.
60
72
  disable-model-invocation: true
61
73
  ---
62
74
 
63
- # Send a Codex project to AgentBridge
75
+ ${CODEX_SKILL_MARKER}
64
76
 
65
- When the user invokes \`/phone\`, you must immediately execute the following command in the terminal from the current project directory. Do not merely describe the command or ask the user to run it:
77
+ When the user invokes \`/${CODEX_SKILL_NAME}\`, you must immediately execute the following command in the terminal from the current project directory. Do not merely describe the command or ask the user to run it:
66
78
 
67
79
  \`node "${portableEntry}" phone --agent codex --cwd .\`
68
80
 
69
81
  After a successful command, reply with the exact QR image Markdown emitted by the command (for example, \`![微信扫码二维码](<temporary QR image URL>)\`) so Codex Desktop renders a scannable image. The temporary QR image URL and pairing URL must be shown as clickable links; then tell the user to scan the 微信扫码二维码 with WeChat. Do not invent or alter either URL. If the command fails, show its exact safe error and do not claim that a QR code was generated.
70
82
 
71
83
  If the user supplies a known persisted Codex thread ID, append \`--session <thread-id>\` to resume that exact thread. Do not claim that the command attached the current desktop conversation when no thread ID was supplied: Codex does not expose a documented current-thread substitution to this local skill.
84
+
85
+ > Named \`${CODEX_SKILL_NAME}\` on purpose: DSH Desktop reads the same skills folder and owns \`/phone\` for the conversation on screen. Running \`/phone\` here would pair the wrong Agent.
72
86
  `;
73
87
  }
74
88
 
@@ -271,17 +285,51 @@ async function installClaudeSkill(options) {
271
285
  return target;
272
286
  }
273
287
 
288
+ /**
289
+ * Remove a `phone` skill this installer used to create.
290
+ *
291
+ * Earlier releases wrote the Codex skill as `phone`, which shadowed the DSH
292
+ * plugin's `/phone` in DSH Desktop because both clients read `~/.agents/skills`.
293
+ * Re-running the installer has to clean that up, or the wrong pairing keeps
294
+ * happening after an upgrade. Only a file carrying our own marker is touched —
295
+ * a hand-written `phone` skill belongs to whoever wrote it.
296
+ */
297
+ async function removeLegacyCodexSkill(root) {
298
+ const legacy = path.join(root, "phone", "SKILL.md");
299
+ let content;
300
+ try {
301
+ content = await readFile(legacy, "utf8");
302
+ } catch {
303
+ return null;
304
+ }
305
+ if (!content.includes(CODEX_SKILL_MARKER)) return null;
306
+ try {
307
+ await rm(path.join(root, "phone"), { recursive: true, force: true });
308
+ return legacy;
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+
274
314
  async function installCodexSkill(options) {
275
315
  if (options.user && options.project) throw new Error("Choose only one of --user or --project");
276
316
  const projectCwd = path.resolve(options.cwd ?? process.cwd());
277
317
  const root = options.project
278
318
  ? path.join(projectCwd, ".agents", "skills")
279
319
  : path.join(os.homedir(), ".agents", "skills");
280
- const target = path.join(root, "phone", "SKILL.md");
320
+
321
+ const removed = await removeLegacyCodexSkill(root);
322
+ if (removed) {
323
+ console.log(`已移除会抢占 /phone 的旧技能: ${removed}`);
324
+ console.log("(DSH Desktop 也读这个目录,旧的 phone 技能会让 /phone 绑到 Codex 会话。)");
325
+ }
326
+
327
+ const target = path.join(root, CODEX_SKILL_NAME, "SKILL.md");
281
328
  await mkdir(path.dirname(target), { recursive: true });
282
329
  await writeFile(target, renderCodexPhoneSkill(), { encoding: "utf8", flag: "w" });
283
- console.log(`Codex /phone 已安装到: ${target}`);
284
- console.log("重新打开 Codex 桌面项目后,在斜杠命令列表选择 /phone。未提供 thread ID 时,它会创建新的项目会话并清楚标注。");
330
+ console.log(`Codex /${CODEX_SKILL_NAME} 已安装到: ${target}`);
331
+ console.log(`重新打开 Codex 桌面项目后,在斜杠命令列表选择 /${CODEX_SKILL_NAME}。未提供 thread ID 时,它会创建新的项目会话并清楚标注。`);
332
+ console.log("DSH Desktop 里的 /phone 保持归插件所有,绑定的就是屏幕上那条会话。");
285
333
  return target;
286
334
  }
287
335
 
package/src/config.js CHANGED
@@ -105,16 +105,52 @@ function parseProjects(raw, workspace) {
105
105
  * end up in a tracked file. The bridge's own data directory is git-ignored, so a
106
106
  * small JSON file there is the honest place for it; environment variables still win
107
107
  * when both are present, which keeps `setx` and one-off experiments working.
108
+ *
109
+ * `legacyFiles` exists because the data directory can move under the user's feet:
110
+ * the DSH plugin started hosting the bridge and gave it its own state directory,
111
+ * which silently left notifications configured in the old one. When the current
112
+ * directory has no settings but an old one does, adopt them once and say so — the
113
+ * alternative is a feature that stops working with no explanation.
108
114
  */
109
- function readNotifyFile(dataDir) {
115
+ function readNotifyFile(dataDir, legacyFiles = []) {
116
+ const own = path.join(dataDir, "notify.json");
117
+ const parsed = readJsonObject(own);
118
+ if (parsed) return parsed;
119
+
120
+ for (const candidate of legacyFiles) {
121
+ const adopted = readJsonObject(candidate);
122
+ if (!adopted) continue;
123
+ try {
124
+ mkdirSync(dataDir, { recursive: true });
125
+ writeFileSync(own, JSON.stringify(adopted, null, 2), "utf8");
126
+ console.error(`Notifications: adopted settings from ${candidate} into ${own}`);
127
+ } catch {
128
+ // A read-only data directory still gets to use the settings for this run.
129
+ }
130
+ return adopted;
131
+ }
132
+ return {};
133
+ }
134
+
135
+ function readJsonObject(file) {
110
136
  try {
111
- const parsed = JSON.parse(readFileSync(path.join(dataDir, "notify.json"), "utf8"));
112
- return parsed && typeof parsed === "object" ? parsed : {};
137
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
138
+ return parsed && typeof parsed === "object" ? parsed : null;
113
139
  } catch {
114
- return {};
140
+ return null;
115
141
  }
116
142
  }
117
143
 
144
+ /** Candidate files a moved data directory may have left behind. */
145
+ export function legacyNotifyPaths(env = process.env, workspace = process.cwd()) {
146
+ const fromEnv = String(env.AGENTBRIDGE_NOTIFY_IMPORT ?? "")
147
+ .split(";")
148
+ .map((entry) => entry.trim())
149
+ .filter(Boolean)
150
+ .map((entry) => (/notify\.json$/i.test(entry) ? path.resolve(entry) : path.join(path.resolve(entry), "notify.json")));
151
+ return [...fromEnv, path.join(path.resolve(workspace), ".agentbridge", "notify.json")];
152
+ }
153
+
118
154
  function pick(env, key, fallback) {
119
155
  const value = String(env[key] ?? "").trim();
120
156
  return value || fallback || null;
@@ -153,7 +189,7 @@ export function loadConfig(env = process.env, workspace = process.cwd()) {
153
189
  const advertisedOrigin = publicOrigin || `http://${firstLanAddress()}:${port}`;
154
190
  const accessKey = (env.AGENTBRIDGE_ACCESS_KEY ?? "").trim() || null;
155
191
  const dataDir = path.resolve(env.AGENTBRIDGE_DATA_DIR ?? path.join(workspace, ".agentbridge"));
156
- const notifyFile = readNotifyFile(dataDir);
192
+ const notifyFile = readNotifyFile(dataDir, legacyNotifyPaths(env, workspace));
157
193
 
158
194
  return {
159
195
  host,
package/src/server.js CHANGED
@@ -994,6 +994,17 @@ export async function startBridge() {
994
994
  console.log(`Remote path: ${bridge.config.remoteProvider} → ${bridge.config.advertisedOrigin}`);
995
995
  }
996
996
  console.log(`WeChat scan target: ${bridge.getCurrentPairing().url}`);
997
+ // Notifications are silent by design, which once hid the fact that the config
998
+ // was being read from a directory that did not have it (the data directory moved
999
+ // when the DSH plugin started hosting the bridge). Say where it looked.
1000
+ if (bridge.notifier.enabled) {
1001
+ console.log(`Notifications: ${bridge.notifier.format}${bridge.notifier.channel ? `/${bridge.notifier.channel}` : ""}${bridge.notifier.muted ? " (muted)" : ""}`);
1002
+ } else {
1003
+ console.log(
1004
+ `Notifications: off — no webhook configured in ${path.join(bridge.config.dataDir, "notify.json")} `
1005
+ + "(set it in the desktop console, or point AGENTBRIDGE_NOTIFY_IMPORT at an older .agentbridge folder)",
1006
+ );
1007
+ }
997
1008
  // Silence here used to mean a QR encoding an address the phone could never reach.
998
1009
  if (new URL(bridge.config.advertisedOrigin).hostname === "127.0.0.1") {
999
1010
  console.warn(