glasslink 0.0.0-stage → 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AgentLink contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,207 @@
1
- # Temporary Holding Version
1
+ # glasslink
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 戴着 NIMO 眼镜,遥控你自己电脑上的 coding agent:看它干到哪了、批准或拒绝它要做的操作、随时打断、用语音给它下新指令。
4
+
5
+ `glasslink` 是装在**你自己电脑上**的那一半:它在本机启动你选的 agent(Claude Code、Codex、DeepSeek Harness、Kimi Code,或实验性的 Pi),把 agent 的进展发给中转服务,再把你在眼镜 / 手机上的操作转回给 agent。agent 用的是你自己的账号,代码一直在你自己的电脑上。
6
+
7
+ ## 三步接入
8
+
9
+ 1. **准备好你的 agent**:装好、登录好你要用的那一家(见下面「各家 agent 的准备步骤」)。电脑上还要有 Node.js 20 或更新版本(`node --version` 看一下)。
10
+ 2. **在终端运行**:进到你要让 agent 干活的项目目录,运行
11
+
12
+ ```sh
13
+ npx glasslink
14
+ ```
15
+
16
+ 第一次运行会列出这台电脑上检测到的 agent,用 ↑↓ 或数字选一个,回车确认。
17
+ 3. **在手机上输码**:终端里会用大字显示一个 6 位码。打开手机上的 AgentLink 页面,输入这个码。终端显示「已连接」就好了,眼镜上就能看到 agent 的进展。
18
+
19
+ 以后再在这台电脑上运行 `npx glasslink`,会直接连上(用上次的配对和 agent),不用再输码。
20
+
21
+ 终端里大概是这样:
22
+
23
+ ```
24
+ AgentLink 0.3.1 · 把电脑上的 coding agent 接到 NIMO 眼镜
25
+ 正在检查这台电脑上装了哪些 agent…
26
+ 选一个 agent(↑↓ 或数字选择,回车确认,Ctrl+C 退出):
27
+ 1. Claude Code 已安装 2.1.317 · 已登录
28
+ ❯ 2. Codex 已安装 0.158.0 · 已登录
29
+ 3. DeepSeek Harness 未安装
30
+ 4. Kimi Code 已安装 0.28.1
31
+ 5. Pi 实验性 已安装 0.84.2 · 已配置
32
+ 准备好了,回车选它
33
+
34
+ 在手机 AgentLink 页面输入这个码:
35
+
36
+ ██████ ██████ ██████ ██████ ██ ██████
37
+ ██ ██ ██ ██ ██ ██ ████ ██
38
+ ██ ██ ██ ██ ██████ ██ ██
39
+ ██ ██ ██ ██ ██ ██ ██ ██
40
+ ██ ██████ ██ ██████ ██████ ██
41
+
42
+ 配对码:707817
43
+ 10 分钟内有效,过期会自动换新码 · 按 Ctrl+C 退出
44
+ 等手机输码…(这个码还剩 9:41)
45
+ ```
46
+
47
+ ## 各家 agent 的准备步骤
48
+
49
+ ### Claude Code
50
+
51
+ ```sh
52
+ npm i -g @anthropic-ai/claude-code # 安装 Claude Code
53
+ claude # 运行一次,按提示登录(也可以用环境变量 ANTHROPIC_API_KEY)
54
+ npm i -g @anthropic-ai/claude-agent-sdk # AgentLink 用 Claude Agent SDK 驱动 Claude Code,只要装一次
55
+ npx glasslink --agent claude
56
+ ```
57
+
58
+ SDK 没有打进 glasslink 的包里(这样 `npx glasslink` 启动快,不用 Claude 的人也不用下载它)。glasslink 按这个顺序找 SDK:环境变量 `AGENTLINK_CLAUDE_SDK`(指向 SDK 目录或入口文件)→ 全局 node_modules → `~/.agentlink/node_modules`。不想全局安装,也可以:`npm i --prefix ~/.agentlink @anthropic-ai/claude-agent-sdk`。
59
+
60
+ 默认读你自己的 Claude Code 设置(`~/.claude/settings.json`、项目里的 `.claude/settings*.json` 和 `CLAUDE.md`):你在设置里允许过的操作照样直接跑,Claude Code 本来会问你的,改成在手机 / 眼镜上问。读文件、搜索、`ls` / `cat` / `pwd` / `git status|log|diff` 这类只读操作自动放行。
61
+
62
+ 加 `--strict`:除了上面这些只读操作,其余一律先问,设置里的「允许」规则也跳不过。
63
+
64
+ ### Codex
65
+
66
+ ```sh
67
+ npm i -g @openai/codex
68
+ codex login
69
+ npx glasslink --agent codex
70
+ ```
71
+
72
+ glasslink 通过 Codex 自带的 `codex app-server` 驱动它。默认用你自己的 Codex 配置(`~/.codex/config.toml` 里的 `approval_policy`、`sandbox_mode`),glasslink 不改。
73
+
74
+ 加 `--strict`:`approvalPolicy=untrusted`(只有 Codex 认定安全的只读命令直接跑,其余命令和所有改文件都先问你)+ `sandbox=workspace-write`(批准以后也只能改工作目录里的文件)。也可以用环境变量 `AGENTLINK_CODEX_APPROVAL`(`untrusted` / `on-request` / `never`)和 `AGENTLINK_CODEX_SANDBOX`(`read-only` / `workspace-write` / `danger-full-access`)自己指定。
75
+
76
+ ### DeepSeek Harness
77
+
78
+ ```sh
79
+ node --version # 需要 24.9 或更新
80
+ npm i -g @deepseek-ai/dsh
81
+ export DEEPSEEK_API_KEY=... # 或者运行 dsh web,在网页里填 key(存在 ~/.dsh 里,glasslink 认得)
82
+ npx glasslink --agent dsh
83
+ ```
84
+
85
+ 审批:默认是 dsh 自己的默认模式(`workspace-write`:工作目录里改文件、跑命令都不问,出了工作目录才问)。你自己设了环境变量 `DSH_PERMISSION_MODE` 的话照用。
86
+
87
+ 加 `--strict`:glasslink 用 `DSH_PERMISSION_MODE=read-only` 启动 dsh。dsh 先在只读沙箱里试,要写文件或执行会改东西的命令时被拦下,然后带着理由来问你能不能放宽,手机上会显示「写文件 c.txt(要放宽到「可写工作目录」:理由…)」。所以历史记录里会先看到一行「· 写文件 c.txt ✗」(第一次被沙箱拦下),批准后再出现一行 ✓,这是正常的。只读的命令(比如 `ls`)在只读沙箱里直接跑,不问。
88
+
89
+ ### Kimi Code
90
+
91
+ 按 Kimi Code 官方说明安装,装好后能运行 `kimi`(默认装在 `~/.kimi-code/bin/`)。然后:
92
+
93
+ ```sh
94
+ kimi # 运行一次,按提示登录
95
+ npx glasslink --agent kimi
96
+ ```
97
+
98
+ 审批由 Kimi Code 自己的设置决定。`kimi acp` 没有审批相关的启动参数,所以 `--strict` 对它不起作用(启动时会提示)。
99
+
100
+ ### Pi(实验性,待验证)
101
+
102
+ ```sh
103
+ npm i -g --ignore-scripts @earendil-works/pi-coding-agent
104
+ pi # 输入 /login 选一家登录,或者先设置好对应厂商的 API Key
105
+ npx glasslink --agent pi
106
+ ```
107
+
108
+ Pi 本身没有审批机制,默认什么都不问(跟你直接用 Pi 一样)。加 `--strict` 时,glasslink 启动它会挂一个很小的扩展(`pi --mode rpc -e <glasslink 自带的 pi-gate.mjs>`,不会写进你的 `~/.pi`):执行命令、写文件、改文件前先问你;读文件、搜索直接放行。这一家目前只按 Pi 0.84 的文档实现,**还没有跟真的 Pi 跑过完整对话**,可能有不稳定的地方。
109
+
110
+ ### 其他支持 ACP 协议的 agent
111
+
112
+ ```sh
113
+ npx glasslink --agent acp --acp-cmd "<启动 ACP 服务的命令>"
114
+ ```
115
+
116
+ 没有专门测试过。
117
+
118
+ ## 审批:默认跟随 agent,`--strict` 更严
119
+
120
+ glasslink **默认不改你的 agent 的权限配置**:agent 平时会问你的操作,改成在手机 / 眼镜上问;agent 平时不问就直接做的,手机上也不会弹批准。
121
+
122
+ 想让写文件、执行命令都先问,加 `--strict`(或者设环境变量 `AGENTLINK_STRICT=1`):
123
+
124
+ ```sh
125
+ npx glasslink --strict
126
+ ```
127
+
128
+ 启动时终端里会写明这次是哪种:「审批:跟随 DeepSeek Harness 自己的设置」或「审批:严格(写文件、执行命令都先问)」。
129
+
130
+ | agent | 默认(跟随它自己) | `--strict` |
131
+ |---|---|---|
132
+ | Claude Code | 读你的 Claude Code 设置;设置里允许过的直接跑 | 只读操作以外一律先问,设置里的允许规则也跳不过 |
133
+ | Codex | 用 `~/.codex/config.toml` 的设置 | `untrusted` + `workspace-write` 沙箱 |
134
+ | DeepSeek Harness | dsh 的默认 `workspace-write`:工作目录里改文件、跑命令不问 | `DSH_PERMISSION_MODE=read-only`:要写、要改都先来问 |
135
+ | Pi | Pi 没有审批,什么都不问 | 挂上 glasslink 的审批扩展:执行命令、写文件、改文件先问 |
136
+ | Kimi Code | Kimi Code 自己的设置 | 不起作用(`kimi acp` 没有对应参数) |
137
+ | 其他 ACP agent | 它自己的设置 | 不起作用 |
138
+
139
+ 不管哪种模式,手机 / 眼镜上的「允许」都只对这一次操作生效。
140
+
141
+ ## 常用命令
142
+
143
+ | 命令 | 做什么 |
144
+ |---|---|
145
+ | `npx glasslink` | 第一次:选 agent → 出码 → 手机输码;以后:直接启动 |
146
+ | `npx glasslink --agent codex` | 指定 agent(下次默认用这次的)。可选:`claude` `codex` `dsh` `kimi` `pi` |
147
+ | `npx glasslink --new` | 重新配对(换手机,或者想重新出码) |
148
+ | `npx glasslink --cwd ~/projects/app` | 指定 agent 干活的目录(默认是当前目录) |
149
+ | `npx glasslink --name 我的MacBook` | 手机上显示的电脑名(默认是主机名) |
150
+ | `npx glasslink --strict` | 严格审批:写文件、执行命令都先问(见上面「审批」) |
151
+ | `npx glasslink --verbose` | 多打印一些细节,比如和中转服务的每次短暂断线 |
152
+ | `npx glasslink logout` | 删掉这台电脑上的配对 |
153
+ | `npx glasslink status` | 看配对状态、中转服务能不能连上 |
154
+ | `npx glasslink pair <6 位码>` | 用**眼镜上**显示的配对码配对(眼镜出码的老流程) |
155
+ | `npx glasslink start` | 按已有配对启动,不交互(老流程) |
156
+
157
+ 运行期间终端要一直开着;按 Ctrl+C 退出。不想每次都用 npx,可以装成全局命令:`npm i -g glasslink`,以后直接运行 `glasslink`。
158
+
159
+ 中转服务地址默认是 `https://agent.quantkick.ai`,可以用 `--relay <URL>` 或环境变量 `AGENTLINK_RELAY` 改。
160
+
161
+ ## 安全说明
162
+
163
+ - **电脑上不开端口。** glasslink 只主动连出去到中转服务,别人没办法从外面连进你的电脑。
164
+ - **令牌只存你本机。** 配对后的令牌存在 `~/.agentlink/config.json`,文件权限 600(只有你自己能读),终端里不会打印。中转服务只存令牌的哈希。
165
+ - **对话不落盘。** 代码和对话内容只在中转服务的内存里经过(每条链路最近 200 条),不写硬盘,日志里也不记内容。
166
+ - **每次批准只管这一次。** 眼镜或手机上点「允许」只对当前这一个操作生效,glasslink 从不替你选「总是允许」;5 分钟没人答按拒绝。
167
+ - **配对码一次性。** 6 位码 10 分钟内有效、只能用一次。如果你没在手机上输码,终端却显示「已连接」,立刻按 Ctrl+C,再运行 `npx glasslink logout`。
168
+ - **agent 的权限照旧。** 默认 glasslink 不改 agent 的权限配置,只是把它的审批请求转到你的手机 / 眼镜上;想更严就加 `--strict`。
169
+ - **检测只看本地。** 列 agent 时 glasslink 只在本机找命令、跑 `--version`、看登录配置文件在不在,不会替你发起任何对话,也不会花你的额度。
170
+
171
+ ## 常见问题
172
+
173
+ **为什么有时手机上不弹批准,agent 就直接改了文件 / 跑了命令?** 因为默认审批跟随 agent 自己的设置,agent 平时不问的操作,手机上也不会问。比如 DeepSeek Harness 默认在工作目录里改文件、跑命令都不问;Claude Code 里你在设置中允许过的命令直接跑;Pi 本身就没有审批。想每次都先问,用 `npx glasslink --strict`(Kimi Code 和其他 ACP agent 除外,它们的审批只能在它们自己的设置里改)。
174
+
175
+ **配对码过期了怎么办?** 不用管,终端会自动换一个新码,在手机上输新的就行。
176
+
177
+ **手机上解除了配对 / 手机连了别的电脑,会怎样?** 这台电脑的配对作废,正在运行的 glasslink 会自动重新出一个码,不用重启。
178
+
179
+ **终端关了会怎样?** 眼镜和手机上显示「电脑未连接」。再运行一次 `npx glasslink` 就恢复了,不用重新输码。
180
+
181
+ **选了 Claude Code,提示没找到 Claude Agent SDK?** 运行 `npm i -g @anthropic-ai/claude-agent-sdk`,再重新启动。
182
+
183
+ **提示「现在不是交互式终端,没法让你选 agent」?** 在脚本或 CI 里运行时没法弹出选择列表,用 `--agent` 指定,比如 `npx glasslink --agent codex`。
184
+
185
+ **列表里显示「没看到登录信息」,但我明明登录过?** 有的 agent 把登录信息存在系统钥匙串里,glasslink 只看配置文件,可能看不到。直接选它试试;真没登录的话,agent 启动时会提示你。
186
+
187
+ **连不上中转服务?** 电脑要能用 HTTPS 访问中转服务。公司网络如果拦了,换个网络试试。glasslink 断线后会自己重连:30 秒内自己恢复的短暂断线不打印(加 `--verbose` 才显示);断开超过 30 秒会提示一次「连不上中转服务,正在重试…」,恢复时再提示一次。断线期间 agent 的进展先存在电脑上,连上后补发到手机。
188
+
189
+ **支持 Windows 吗?** 目前只在 macOS 上测过,Linux 应该也能用,Windows 还没验证。
190
+
191
+ ## 环境变量
192
+
193
+ | 变量 | 作用 |
194
+ |---|---|
195
+ | `AGENTLINK_RELAY` | 中转服务地址 |
196
+ | `AGENTLINK_STRICT` | 设成 `1` 等于加 `--strict` |
197
+ | `AGENTLINK_HOME` | 配置目录(默认 `~/.agentlink`) |
198
+ | `AGENTLINK_CLAUDE_SDK` | Claude Agent SDK 的位置(目录或入口文件) |
199
+ | `AGENTLINK_CODEX_CMD` / `AGENTLINK_CODEX_APPROVAL` / `AGENTLINK_CODEX_SANDBOX` | Codex 的启动命令(默认 `codex app-server`)、审批策略、沙箱 |
200
+ | `AGENTLINK_PI_CMD` | Pi 的启动命令(默认 `pi`,glasslink 会在后面加 `--mode rpc -e …`) |
201
+ | `AGENTLINK_DSH_CMD` / `AGENTLINK_KIMI_CMD` | DeepSeek Harness / Kimi Code 的 ACP 启动命令 |
202
+ | `DSH_PERMISSION_MODE` | dsh 自己的权限模式;你设了 glasslink 照用(`--strict` 时固定 `read-only`) |
203
+ | `NO_COLOR` | 不用颜色 |
204
+
205
+ ## 许可
206
+
207
+ MIT
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+ // agentlink:把电脑上的 coding agent 接到 NIMO 眼镜 / 手机。约定见 CONTRACT.md(§1–§3、§8)。
3
+ // glasslink 交互式:选 agent → 终端显示 6 位码 → 手机输码 → 进入运行状态(已配对就直接启动)
4
+ // glasslink pair <码> 老流程:用眼镜上显示的配对码配对
5
+ // glasslink start 老流程:按已有配对启动
6
+ // glasslink status / logout
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { loadConfig, saveConfig, clearConfig, resolveRelay, configPath } from '../src/config.js';
11
+ import { createClient } from '../src/relay-client.js';
12
+ import { Bridge } from '../src/runner.js';
13
+ import { createAdapter, AGENTS, CATALOG, detectAll, installHint, catalogEntry, approvalLine, strictFromEnv } from '../src/adapters/index.js';
14
+ import { pairByCode } from '../src/pairing.js';
15
+ import { select, makeColors, padEnd } from '../src/ui.js';
16
+ import { VERSION } from '../src/version.js';
17
+
18
+ const c = makeColors(process.stdout);
19
+ const ce = makeColors(process.stderr);
20
+ // 提示里怎么称呼自己:通过 npx 跑的就写 npx glasslink
21
+ const CMD = process.env.npm_command === 'exec' && !process.env.AGENTLINK_PLAIN_CMD ? 'npx glasslink' : 'glasslink';
22
+
23
+ const HELP = `AgentLink ${VERSION} · 戴着 NIMO 眼镜遥控你电脑上的 coding agent
24
+
25
+ 用法:
26
+ ${CMD} 第一次:选 agent → 终端显示 6 位码 → 在手机 AgentLink 页面输入
27
+ 以后:直接启动(用上次的配对和 agent)
28
+ --agent <名字> 直接指定 agent,不用选:${CATALOG.map((x) => x.id).join(' / ')}(还有 acp、fake)
29
+ --new 重新配对(换手机,或者想重新出码)
30
+ --cwd <目录> agent 在哪个目录干活(默认当前目录)
31
+ --name <电脑名> 手机上显示的电脑名(默认主机名)
32
+ --strict 严格审批:写文件、执行命令都先在手机 / 眼镜上问你(也可用环境变量 AGENTLINK_STRICT=1)
33
+ 不加时审批跟随 agent 自己的设置,agent 平时不问的操作手机上也不会弹批准
34
+ --verbose 打印更多细节(比如和中转服务的每一次短暂断线)
35
+ --acp-cmd "<命令>" 配合 --agent acp:任意 ACP 服务的启动命令
36
+ --relay <URL> 中转服务地址(默认 https://agent.quantkick.ai,也可用环境变量 AGENTLINK_RELAY)
37
+ ${CMD} logout 删掉这台电脑上的配对
38
+ ${CMD} status 看配对状态、中转服务能不能连上
39
+ ${CMD} pair <6 位码> 用眼镜上显示的配对码配对(眼镜出码的老流程)
40
+ ${CMD} start 按已有配对启动(老流程,和不带参数一样,但不会交互)
41
+ ${CMD} --version | --help
42
+
43
+ 审批:默认跟随 agent 自己的设置(glasslink 不改它的权限配置);加 --strict 切到各家最严的模式,
44
+ 写文件、执行命令都先问。Kimi Code 和 --agent acp 没法从外面切,--strict 对它们不起作用。
45
+
46
+ 按 Ctrl+C 退出。配置存在 ${configPath()}(权限 600)。
47
+
48
+ agent:
49
+ ${Object.entries(AGENTS).map(([k, v]) => ` ${k.padEnd(7)}${v}`).join('\n')}`;
50
+
51
+ const BOOL = new Set(['new', 'help', 'version', 'yes', 'strict', 'verbose']);
52
+ function parseArgs(argv) {
53
+ const o = { _: [] };
54
+ for (let i = 0; i < argv.length; i++) {
55
+ const a = argv[i];
56
+ if (a === '-h') o.help = true;
57
+ else if (a === '-v' || a === '-V') o.version = true;
58
+ else if (a.startsWith('--')) {
59
+ const [k, v] = a.slice(2).split(/=(.*)/s);
60
+ if (v !== undefined) o[k] = v;
61
+ else if (!BOOL.has(k) && i + 1 < argv.length && !argv[i + 1].startsWith('--')) o[k] = argv[++i];
62
+ else o[k] = true;
63
+ } else o._.push(a);
64
+ }
65
+ return o;
66
+ }
67
+
68
+ const die = (msg, code = 1) => { process.stderr.write(`${ce.red('✗')} ${msg}\n`); process.exit(code); };
69
+ const stamp = () => new Date().toTimeString().slice(0, 8);
70
+ const print = (l) => console.log(`${c.dim(stamp())} ${l}`);
71
+ const hostName = (args) => (typeof args.name === 'string' && args.name.trim() ? args.name.trim() : os.hostname().replace(/\.local$/, ''));
72
+
73
+ function resolveCwd(args) {
74
+ const cwd = path.resolve(typeof args.cwd === 'string' ? args.cwd.replace(/^~(?=$|\/)/, os.homedir()) : process.cwd());
75
+ if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) die(`工作目录不存在:${cwd}`);
76
+ return cwd;
77
+ }
78
+
79
+ function checkAgentId(id) {
80
+ if (!Object.hasOwn(AGENTS, id)) die(`不认识的 agent:「${id}」。可选:${Object.keys(AGENTS).join(' / ')}`);
81
+ return id;
82
+ }
83
+
84
+ const isStrict = (args) => args.strict === true || args.strict === 'true' || strictFromEnv();
85
+
86
+ function makeAdapter(agentId, args) {
87
+ try {
88
+ return createAdapter(agentId, { acpCmd: typeof args['acp-cmd'] === 'string' ? args['acp-cmd'] : undefined, print, stepMs: args['step-ms'] ? Number(args['step-ms']) : undefined, strict: isStrict(args) });
89
+ } catch (e) { return die(e.message); }
90
+ }
91
+
92
+ // 跑起来,直到退出。返回退出码:0 正常退出,2 配对失效
93
+ async function runBridge({ relay, token, adapter, cwd, args, unauthorizedHint }) {
94
+ const bridge = new Bridge({ client: createClient(relay, token), adapter, cwd, print, pollWait: args['poll-wait'] ? Number(args['poll-wait']) : 25, unauthorizedHint, verbose: !!args.verbose });
95
+ console.log(`AgentLink 启动中\n 中转服务:${relay}\n agent:${adapter.name}\n 工作目录:${cwd}\n ${approvalLine(adapter, isStrict(args))}`);
96
+ try { await bridge.start(); } catch (e) { die(`agent 启动失败:\n${e.message}`); }
97
+ let stopping = false;
98
+ const onSig = () => { if (stopping) process.exit(130); stopping = true; print('正在退出…(再按一次 Ctrl+C 强制退出)'); bridge.stop(0); };
99
+ process.on('SIGINT', onSig); process.on('SIGTERM', onSig);
100
+ const code = await bridge.done;
101
+ process.off('SIGINT', onSig); process.off('SIGTERM', onSig);
102
+ return code;
103
+ }
104
+
105
+ // ---- 交互式主命令 ----
106
+ function statusText(d) {
107
+ if (!d.installed) return c.dim('未安装');
108
+ const parts = [c.green('已安装') + (d.version ? ` ${d.version}` : '')];
109
+ if (d.id === 'claude' && !d.sdk) parts.push(c.yellow('缺 Claude Agent SDK'));
110
+ else if (d.loggedIn === true) parts.push(d.id === 'claude' || d.id === 'codex' ? '已登录' : '已配置');
111
+ else if (d.loggedIn === false) parts.push(c.yellow('没看到登录信息'));
112
+ return parts.join(' · ');
113
+ }
114
+
115
+ function detailText(d) {
116
+ const entry = catalogEntry(d.id);
117
+ if (!d.installed) return `还没装。${entry.install.length ? `安装:${entry.install.join(';')}` : entry.installText || ''}`;
118
+ if (d.notes.length) return d.notes.join(';');
119
+ return d.loggedIn === null ? `第一次用请先确认:${entry.login}` : '准备好了,回车选它';
120
+ }
121
+
122
+ async function chooseAgent(dets, preferred) {
123
+ const nameW = Math.max(...dets.map((d) => d.name.length + (d.experimental ? 6 : 0))) + 2;
124
+ const items = dets.map((d, i) => ({ label: `${i + 1}. ${padEnd(d.name + (d.experimental ? ' 实验性' : ''), nameW)}${statusText(d)}`, detail: detailText(d) }));
125
+ let initial = dets.findIndex((d) => d.id === preferred);
126
+ if (initial < 0) initial = Math.max(0, dets.findIndex((d) => d.ready));
127
+ const i = await select({ title: c.bold('选一个 agent') + c.dim('(↑↓ 或数字选择,回车确认,Ctrl+C 退出):'), items, initial,
128
+ footer: c.dim(` 其他 ACP agent:${CMD} --agent acp --acp-cmd "<命令>"`), doneText: (k) => `agent:${c.bold(dets[k].name)}${c.dim(`(下次可以直接 ${CMD} --agent ${dets[k].id})`)}` });
129
+ if (i === null) { console.log('已退出。'); process.exit(130); }
130
+ return dets[i].id;
131
+ }
132
+
133
+ // 选好的 agent 能不能用:没装 / Claude 缺 SDK → 给安装命令并退出;其余问题只提醒
134
+ async function checkReady(agentId) {
135
+ if (!catalogEntry(agentId)) return;
136
+ const [d] = await detectAll({ ids: [agentId] });
137
+ if (!d.installed) die(`${d.name} 还没装。\n${installHint(agentId)}\n装好后重新运行 ${CMD}。`);
138
+ if (agentId === 'claude' && !d.sdk) die(`还差一步:AgentLink 用 Claude Agent SDK 驱动 Claude Code,这台电脑上还没有。请运行:\n npm i -g @anthropic-ai/claude-agent-sdk\n装好后重新运行 ${CMD}。`);
139
+ for (const n of d.notes) console.log(`${c.yellow('!')} ${n}`);
140
+ if (d.experimental) console.log(`${c.yellow('!')} ${d.name} 是实验性支持,可能有不稳定的地方。`);
141
+ }
142
+
143
+ async function cmdMain(args) {
144
+ const tty = !!(process.stdin.isTTY && process.stdout.isTTY);
145
+ let cfg = loadConfig();
146
+ const fresh = !!args.new || !cfg.bridgeToken;
147
+ let relay;
148
+ try { relay = resolveRelay(args.relay, fresh ? {} : cfg); } catch (e) { die(e.message); }
149
+ const cwd = resolveCwd(args);
150
+ console.log(`${c.bold(`AgentLink ${VERSION}`)} · 把电脑上的 coding agent 接到 NIMO 眼镜`);
151
+
152
+ let agentId = typeof args.agent === 'string' ? checkAgentId(args.agent) : null;
153
+ if (!agentId && !fresh && cfg.agent && Object.hasOwn(AGENTS, cfg.agent)) agentId = cfg.agent;
154
+ if (!agentId && !fresh && !tty) agentId = 'claude'; // 老版本(0.1)配的,没记 agent:和以前的 start 一样默认 claude
155
+ if (!agentId) {
156
+ if (!tty) die(`现在不是交互式终端,没法让你选 agent。请用 --agent 指定,比如:${CMD} --agent claude\n可选:${CATALOG.map((x) => x.id).join(' / ')}`);
157
+ console.log(c.dim(`各家的准备步骤:${relay}/link`));
158
+ console.log('正在检查这台电脑上装了哪些 agent…');
159
+ const dets = await detectAll();
160
+ agentId = await chooseAgent(dets, cfg.agent);
161
+ }
162
+ await checkReady(agentId);
163
+
164
+ for (;;) {
165
+ if (!cfg.bridgeToken || args.new) {
166
+ if (args.new && cfg.bridgeToken) console.log(c.dim('重新配对:手机输入新码后,这台电脑原来的配对就作废了。'));
167
+ const ac = new AbortController();
168
+ const onSig = () => ac.abort();
169
+ process.on('SIGINT', onSig); process.on('SIGTERM', onSig);
170
+ let r;
171
+ try {
172
+ r = await pairByCode({ base: relay, name: hostName(args), agent: agentId, signal: ac.signal, cmd: CMD, pollWait: args['poll-wait'] ? Number(args['poll-wait']) : 25 });
173
+ } catch (e) {
174
+ if (e.code === 'aborted') { console.log('\n已退出,还没配对。'); process.exit(130); }
175
+ die(e.message);
176
+ } finally { process.off('SIGINT', onSig); process.off('SIGTERM', onSig); }
177
+ cfg = { relay, bridgeToken: r.bridgeToken, linkId: r.linkId, name: hostName(args), agent: agentId, pairedAt: new Date().toISOString() };
178
+ saveConfig(cfg);
179
+ args.new = false;
180
+ console.log(`\n${c.green('✓ 已连接')} 手机已经配对好了。以后在这台电脑上运行 ${CMD} 会直接连上,不用再输码。\n`);
181
+ } else if (cfg.agent !== agentId) {
182
+ cfg = { ...cfg, agent: agentId };
183
+ saveConfig(cfg);
184
+ }
185
+ const code = await runBridge({ relay, token: cfg.bridgeToken, adapter: makeAdapter(agentId, args), cwd, args, unauthorizedHint: null });
186
+ if (code !== 2) process.exit(code);
187
+ console.log(`\n${c.yellow('!')} 这台电脑的配对失效了(手机上解除了配对,或者手机连了别的电脑)。重新申请一个配对码…`);
188
+ cfg = { ...cfg, bridgeToken: undefined, linkId: undefined };
189
+ saveConfig(cfg);
190
+ }
191
+ }
192
+
193
+ // ---- 子命令 ----
194
+ async function cmdPair(args) {
195
+ const code = String(args._[1] || '').trim();
196
+ if (!/^\d{6}$/.test(code)) die(`配对码是眼镜上显示的 6 位数字,比如:${CMD} pair 123456\n(用手机配对的话不用这条,直接运行 ${CMD})`);
197
+ const cfg = loadConfig();
198
+ let relay;
199
+ try { relay = resolveRelay(args.relay, {}); } catch (e) { die(e.message); } // 配对时不沿用旧配置里的 relay,按参数 / 环境变量 / 默认
200
+ const name = hostName(args);
201
+ try {
202
+ const r = await createClient(relay).pair(code, name);
203
+ saveConfig({ ...cfg, relay, bridgeToken: r.bridgeToken, linkId: r.linkId, name, pairedAt: new Date().toISOString(), ...(typeof args.agent === 'string' ? { agent: checkAgentId(args.agent) } : {}) });
204
+ console.log(`配对成功(${name} ⇄ 眼镜,中转服务:${relay})。现在运行:${CMD} start(或直接 ${CMD})`);
205
+ } catch (e) {
206
+ if (e.status === 403) die('配对码不对或已经过期(10 分钟内有效、只能用一次)。请看眼镜上最新的配对码再试。');
207
+ if (e.status === 429) die('试太多次了,请过 1 分钟再试。');
208
+ if (e.status === 400) die('配对码格式不对,应该是 6 位数字。');
209
+ die(`配对失败:${e.message}(中转服务:${relay})`);
210
+ }
211
+ }
212
+
213
+ async function cmdStatus(args) {
214
+ const cfg = loadConfig();
215
+ const file = configPath();
216
+ if (!cfg.bridgeToken) { console.log(`还没配对。运行 ${CMD},按提示在手机 AgentLink 页面输入终端里显示的 6 位码。\n配置文件:${file}`); return; }
217
+ let relay;
218
+ try { relay = resolveRelay(args.relay, cfg); } catch (e) { die(e.message); }
219
+ let mode = '';
220
+ try { mode = (fs.statSync(file).mode & 0o777).toString(8); } catch { /* 忽略 */ }
221
+ console.log(`已配对:${cfg.name || '(没名字)'},链路 ${cfg.linkId},配对时间 ${cfg.pairedAt || '未知'}`);
222
+ console.log(`agent:${cfg.agent ? (catalogEntry(cfg.agent) || { name: cfg.agent }).name : '(没记,启动时默认 claude)'}`);
223
+ console.log(`中转服务:${relay}${cfg.relay && cfg.relay !== relay ? `(注意:配对时用的是 ${cfg.relay})` : ''}`);
224
+ console.log(`配置文件:${file}(权限 ${mode})`);
225
+ try { await createClient(relay).health(); console.log('中转服务能连上。'); } catch (e) { console.log(`中转服务连不上:${e.message}`); }
226
+ }
227
+
228
+ async function cmdStart(args) {
229
+ const cfg = loadConfig();
230
+ if (!cfg.bridgeToken) die(`还没配对:运行 ${CMD}(在手机上输码),或者用眼镜上的配对码运行 ${CMD} pair <配对码>`);
231
+ let relay;
232
+ try { relay = resolveRelay(args.relay, cfg); } catch (e) { die(e.message); }
233
+ if (cfg.relay && cfg.relay !== relay) console.log(`注意:配对时用的中转服务是 ${cfg.relay},现在连的是 ${relay},token 可能不认。`);
234
+ const cwd = resolveCwd(args);
235
+ const agentId = checkAgentId(typeof args.agent === 'string' ? args.agent : cfg.agent || 'claude');
236
+ const code = await runBridge({ relay, token: cfg.bridgeToken, adapter: makeAdapter(agentId, args), cwd, args,
237
+ unauthorizedHint: `配对已失效(可能在眼镜上点了「重新配对」,或手机连了别的电脑)。用眼镜上的新配对码运行:${CMD} pair <配对码>;用手机的话直接运行 ${CMD}` });
238
+ process.exit(code);
239
+ }
240
+
241
+ async function cmdLogout() {
242
+ const cfg = loadConfig();
243
+ const had = clearConfig();
244
+ if (!had) { console.log('这台电脑上本来就没有配对。'); return; }
245
+ console.log(`已删除这台电脑上的配对(${cfg.name || '这台电脑'},${configPath()})。`);
246
+ console.log(`手机 / 眼镜上会显示「电脑未连接」。下次运行 ${CMD} 会重新出码配对。`);
247
+ }
248
+
249
+ const args = parseArgs(process.argv.slice(2));
250
+ const cmd = args._[0];
251
+ if (args.version) { console.log(VERSION); process.exit(0); }
252
+ const run = { pair: cmdPair, start: cmdStart, status: cmdStatus, logout: cmdLogout }[cmd];
253
+ if (args.help || cmd === 'help') { console.log(HELP); process.exit(0); }
254
+ if (cmd && !run) { console.error(`不认识的命令:${cmd}\n\n${HELP}`); process.exit(1); }
255
+ (run || cmdMain)(args).catch((e) => die(e.message || String(e)));
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "glasslink",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.1",
4
+ "description": "戴着 NIMO AR 眼镜遥控你电脑上的 coding agent(Claude Code / Codex / DeepSeek Harness / Kimi Code / Pi):看进展、批准操作、打断、语音下指令",
5
+ "type": "module",
6
+ "bin": {
7
+ "glasslink": "bin/agentlink.js",
8
+ "agentlink": "bin/agentlink.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "test": "node --test --test-timeout=60000 test/*.test.mjs",
17
+ "demo": "node demo-local.mjs"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "keywords": [
23
+ "glasslink",
24
+ "agentlink",
25
+ "nimo",
26
+ "ar-glasses",
27
+ "smart-glasses",
28
+ "coding-agent",
29
+ "claude-code",
30
+ "codex",
31
+ "kimi",
32
+ "deepseek",
33
+ "acp",
34
+ "remote-control",
35
+ "cli"
36
+ ],
37
+ "license": "MIT",
38
+ "author": "AgentLink contributors"
39
+ }