frely-cli 0.6.10 → 0.7.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.
Files changed (44) hide show
  1. package/README.md +97 -283
  2. package/README.zh-CN.md +240 -0
  3. package/dist/agent-help.d.ts +38 -2
  4. package/dist/agent-help.js +52 -4
  5. package/dist/agent-help.js.map +1 -1
  6. package/dist/auth.d.ts +3 -0
  7. package/dist/auth.js +52 -14
  8. package/dist/auth.js.map +1 -1
  9. package/dist/device/protocol.d.ts +1 -1
  10. package/dist/device/protocol.js +1 -1
  11. package/dist/device/protocol.js.map +1 -1
  12. package/dist/device/relay-client.d.ts +10 -2
  13. package/dist/device/relay-client.js +29 -4
  14. package/dist/device/relay-client.js.map +1 -1
  15. package/dist/index.js +15 -39
  16. package/dist/index.js.map +1 -1
  17. package/dist/mcp-command.d.ts +2 -2
  18. package/dist/mcp-command.js +31 -8
  19. package/dist/mcp-command.js.map +1 -1
  20. package/dist/runtime/mcp.js +58 -29
  21. package/dist/runtime/mcp.js.map +1 -1
  22. package/dist/runtime/process-manager.d.ts +1 -1
  23. package/dist/runtime/process-manager.js +4 -2
  24. package/dist/runtime/process-manager.js.map +1 -1
  25. package/dist/runtime/relay-mcp.d.ts +2 -1
  26. package/dist/runtime/relay-mcp.js.map +1 -1
  27. package/dist/runtime/relay-session.d.ts +47 -0
  28. package/dist/runtime/relay-session.js +56 -0
  29. package/dist/runtime/relay-session.js.map +1 -0
  30. package/dist/runtime/sandbox.d.ts +14 -0
  31. package/dist/runtime/sandbox.js +136 -0
  32. package/dist/runtime/sandbox.js.map +1 -0
  33. package/dist/runtime/workspace-registry.d.ts +5 -0
  34. package/dist/runtime/workspace-registry.js +87 -0
  35. package/dist/runtime/workspace-registry.js.map +1 -0
  36. package/dist/runtime/workspace-router.d.ts +5 -0
  37. package/dist/runtime/workspace-router.js +32 -0
  38. package/dist/runtime/workspace-router.js.map +1 -0
  39. package/dist/runtime/workspace.js +3 -1
  40. package/dist/runtime/workspace.js.map +1 -1
  41. package/dist/workspace-command.d.ts +8 -0
  42. package/dist/workspace-command.js +36 -0
  43. package/dist/workspace-command.js.map +1 -0
  44. package/package.json +2 -1
@@ -0,0 +1,240 @@
1
+ # frely-cli
2
+
3
+ [English](README.md) · [简体中文](README.zh-CN.md)
4
+
5
+ `frely-cli` 把你指定的一台电脑变成 AI Agent 可以远程操作的安全设备。浏览器 Agent(ChatGPT、Codex)和命令行 Agent(Claude Code、pi)通过 Model Context Protocol(MCP)连接,访问你授权的工作区(workspace)内的文件、命令与进程——在任何地方。
6
+
7
+ 两个可选功能,各自有独立的配置与授权:
8
+
9
+ - **Remote Agent Skill** —— 把已发布的 Frely Agent 安装为本地触发 Skill,从自动化流程中调用。
10
+ - **本地模型共享** —— 把本机 Ollama / OpenAI 兼容运行时发布为你的 Frely 个人 Provider。
11
+
12
+ 本仓库包含开源的 Frely 客户端与本地 MCP 运行时;托管的 Frely Relay 控制面是独立的服务依赖。不存在独立的 `friday-local` 项目,本地 MCP 执行属于 `frely-cli`。
13
+
14
+ ```text
15
+ Remote Agent Skill: 安装 -> frely login -> frely skill install -> frely agent invoke
16
+ Device MCP: 在被控电脑安装 -> frely login -> frely mcp -> 客户端添加 MCP URL -> OAuth 授权
17
+ ```
18
+
19
+ ## 安装
20
+
21
+ 独立安装器(standalone installer)会选择平台可执行文件、校验 SHA-256 并安装到用户目录,不需要 Node.js、npm 或 keyring。
22
+
23
+ ```sh
24
+ # macOS / Linux
25
+ curl -fsSL https://frely.cloud/install.sh | sh
26
+ ```
27
+
28
+ ```powershell
29
+ # Windows
30
+ irm https://github.com/FrelyHQ/frely-cli/releases/latest/download/install.ps1 | iex
31
+ ```
32
+
33
+ 或使用 npm(需要 Node.js 22 或更新版本):
34
+
35
+ ```sh
36
+ npm install --global --ignore-scripts frely-cli@latest
37
+ ```
38
+
39
+ 要安装指定版本,把 `latest` 换成版本号。带 tag 的发布会在跨平台验证后同时发布 npm 包与 GitHub Release 独立安装产物。
40
+
41
+ ### 从源码安装
42
+
43
+ 用 Bun 安装当前源码:
44
+
45
+ ```sh
46
+ cd /path/to/frely-cli
47
+ bun install
48
+ bun run build
49
+ bun install --global "$PWD"
50
+ ```
51
+
52
+ 全局安装命令请使用 `$PWD` 绝对路径。如果找不到 `frely`,把 Bun 全局 bin 目录加入 `PATH`:
53
+
54
+ ```sh
55
+ export PATH="$(bun pm bin -g):$PATH"
56
+ ```
57
+
58
+ 当前源码没有 keytar 依赖和原生 npm 构建步骤。依赖安装支持 `npm ci --ignore-scripts` 与 `bun install --ignore-scripts`。仓库以 npm 作为 CI 与发布的规范包管理器并提交 `package-lock.json`;Bun 仅用于本地开发,变更依赖时请保持 lockfile 同步。
59
+
60
+ ## 首次使用
61
+
62
+ 用消费端使用的 Frely 账号登录一次:
63
+
64
+ ```sh
65
+ frely login
66
+ ```
67
+
68
+ 浏览器会自动打开。要换浏览器或换账号,运行 `frely login --no-browser`,并把打印的 URL 只在你想要的那个账号已登录的浏览器中打开。在已登录状态下访问设备授权 URL,可能在点击 Approve 之前就把该码绑定到那个账号——如果默认浏览器用别的账号打开了,按 Ctrl+C,重新运行 `frely login --no-browser`,使用**新的 URL**。重新登录或复用旧 URL 都不会切换账号。使用自建 Relay 时请保留 `--relay <url>`。`FRELY_NO_BROWSER=1` 仍然支持。
69
+
70
+ ### Device MCP:让远程客户端控制这台电脑
71
+
72
+ 在被控电脑上开启文件、shell 与进程访问:
73
+
74
+ ```sh
75
+ frely mcp --workspace /path/to/project
76
+ ```
77
+
78
+ `frely login` 通过浏览器设备授权获取受限账号会话。`frely mcp` 初始化独立的 MCP 安全密钥、请求浏览器批准该设备与工作区,并安装用户级 Device Relay 服务(macOS LaunchAgent、Linux systemd 用户单元或 Windows 任务计划程序)。默认授权 90 天,`--days 1..180` 可选时长。
79
+
80
+ 打印要添加到 MCP 客户端的 URL:
81
+
82
+ ```sh
83
+ frely mcp url
84
+ ```
85
+
86
+ 如果 MCP 尚未配置,`frely mcp url` 会自动执行等价的 `frely mcp setup --workspace ~`:请求浏览器批准家目录(默认 90 天),然后安装后台服务并打印 URL。请先运行 `frely login`。安装提示走 stderr,stdout 保持单一 URL(或 `--json` 时一个 JSON 对象);批准或安装失败则不打印 URL。
87
+
88
+ 把打印的完整 URL 添加到支持 OAuth 的远程 MCP 客户端,选择 OAuth 并完成授权。保持电脑在线。验证首次连接:让客户端只列出所选工作区的顶层名称,不写文件、不跑 shell 命令——返回与目录一致的结果即连通。
89
+
90
+ 调用端的 Claude Code:
91
+
92
+ ```sh
93
+ claude mcp add --transport http frely-computer "<MCP_URL>"
94
+ ```
95
+
96
+ 在 Claude Code 中打开 `/mcp` 完成 OAuth 授权。每台设备使用不同的服务器名。让 Agent 用 Frely 工具做远程工作;它自带的 shell 仍跑在调用端电脑。同一设备的客户端共享其工作区与托管进程。
97
+
98
+ 授权生命周期:`frely mcp renew --days 180` 需要新的批准并轮换 MCP 执行密钥。MCP URL 始终绑定该设备。登录刷新、OAuth 刷新、重启与重复配置都不延长授权。在 Frely → **Device MCP**(`/user/account/connections`)管理你的设备。不带参数的 `frely mcp` 使用当前目录。
99
+
100
+ ### 调用 Frely 托管的 Agent
101
+
102
+ 使用有账号或受限 API-key 访问权限的已发布 Agent。安装为本地触发 Skill:
103
+
104
+ ```sh
105
+ frely skill install https://frely.cloud/api/public/virtual-models/<distribution-id> \
106
+ --host pi \
107
+ --scope global \
108
+ --json
109
+ ```
110
+
111
+ Creator 也可以提供一个现成的模型级、限额 API key 用于赞助/演示调用。只通过 stdin 传入,避免出现在 argv 或生成的 Skill 中:
112
+
113
+ ```sh
114
+ printf '%s' "$FRELY_AGENT_KEY" | \
115
+ frely skill install https://frely.cloud/api/public/virtual-models/<distribution-id> \
116
+ --host chatgpt \
117
+ --scope global \
118
+ --api-key-stdin \
119
+ --json
120
+ ```
121
+
122
+ CLI 会用目标模型级 MCP `tools/list` 端点验证 key,存入安全凭证库,在托管 Skill 元数据中只记录 `authMode=api-key`。分享请使用短生命周期、单模型、限额的 key;不要把 Creator 主 key 贴进去。
123
+
124
+ 生成的 Skill 通过 Frely 的模型级 MCP 端点调用已发布 Agent。从自动化调用已安装 Agent,完整任务走 stdin:
125
+
126
+ ```sh
127
+ printf '%s' '你的完整任务' | \
128
+ frely agent invoke '<distribution-id>' --input-stdin --json
129
+ ```
130
+
131
+ ## 本地模型共享
132
+
133
+ 把回环(loopback)OpenAI 兼容运行时发布为 Frely 个人 Provider,Ollama 是默认驱动:
134
+
135
+ ```sh
136
+ frely provider share ollama
137
+ ```
138
+
139
+ 自定义端点与模型选择:
140
+
141
+ ```sh
142
+ frely provider share openai-compatible \
143
+ --url http://127.0.0.1:8080/v1 \
144
+ --models model-a,model-b \
145
+ --slot <personal-provider-slot-id> \
146
+ --name "Local GPU"
147
+ ```
148
+
149
+ 要求:已登录 Frely、一个空闲的活跃个人 Provider slot、回环 HTTP、OpenAI 兼容 `/v1`(Ollama 默认端点 `http://127.0.0.1:11434/v1`)。模型名不能包含空格或 `/`。
150
+
151
+ 该命令创建服务端管理的 `openai-compatible` 个人 Provider,把本地端点存入仅属主可读的 CLI 状态,启动 Device Relay 服务,用设备 Ed25519 key 签署 Provider 凭证,配置 CPA 并启用声明的模型。现有 Frely Access Point 与 API-key 流程即可消费该 Provider。
152
+
153
+ Provider 检查与恢复:
154
+
155
+ ```sh
156
+ frely provider list
157
+ frely provider finalize <provider-id>
158
+ ```
159
+
160
+ `finalize` 为停留在 prepared 状态的 Provider 续跑 CPA 配置。
161
+
162
+ ## 升级与诊断
163
+
164
+ ```sh
165
+ frely doctor # 安装路径、发行形态、最新稳定版、本地账号/MCP/服务状态
166
+ frely doctor -v # 另加配置路径、运行时细节、授权到期、最近心跳、脱敏错误
167
+ frely upgrade # 原地升级当前正在运行的安装
168
+ ```
169
+
170
+ `frely upgrade` 永不换安装器、不改 PATH、不降级更新版本。standalone 下载会先校验 SHA-256 并做启动测试再替换可执行文件;npm/Bun 安装保持原全局目录。匹配的、正在运行的 Device Relay 服务会被暂停维护、重启并在安装后检查;凭证、设备身份、MCP URL、工作区与授权到期均保留。Windows 上 `upgrade` 打印检测到安装方式对应的 PowerShell 命令,请在本地终端执行。
171
+
172
+ `frely doctor` 是唯一诊断入口,永不重启服务。“Connected” 表示匹配的账号/设备进程在 75 秒内收到 WebSocket 心跳,且 MCP 授权与工作区与运行中的 relay 匹配。两种模式都不代替客户端完成 OAuth 授权,也不代替执行工具调用。`frely doctor --mcp` 是检查受保护凭证和服务端授权状态的推荐方式。
173
+
174
+ 完整行为:[self-upgrade 契约](docs/self-upgrade.md) 与[服务维护与旧版本迁移](docs/service-maintenance.md)。若安装了多个 `frely`,升级前先查看 `doctor` 显示的路径。
175
+
176
+ ## 本地执行边界
177
+
178
+ 本地 MCP 服务暴露:工作区检查、文件搜索/读/写/补丁、目录创建/删除/移动、shell 命令、常驻进程管理。
179
+
180
+ 文件系统工具约束在所选工作区内:拒绝 symlink 逃逸、普通文件读写上限 1 MiB、no-follow 读、原子替换写。`run_command` 与常驻进程工具以当前 OS 用户权限执行;工作区只约束它们的工作目录,**不是** shell 沙箱。
181
+
182
+ 只读本地操作可以并行;写与 shell 操作走本地公平调度器——避免了设备级 `busy -> 429` 行为。
183
+
184
+ ## 认证与密钥
185
+
186
+ 基础账号会话与 Network 会话使用私有明文文件,不能批准 MCP 授权,也不能在显式范围外调用账号管理操作。Provider key 与 MCP 执行 key 相互独立。
187
+
188
+ MCP 密钥使用 AES-256-GCM 文件,主密钥存 macOS Keychain、Windows 凭据管理器或 Linux Secret Service。无头部署可通过 `FRELY_CREDENTIAL_KEY` 注入 32 字节 key 并设 `FRELY_CREDENTIAL_STORE=encrypted-file`。MCP 没有明文回退;安全存储失败不影响基础功能。
189
+
190
+ 稳定的 MCP URL 不含凭证。远程客户端持有 OAuth 凭证;CLI 持有 MCP 执行私钥。Relay 校验 OAuth 资源绑定与当前 MCP 执行租约。到期会阻塞请求与排队任务、取消托管执行,但不回滚写入,也不为任意 shell 程序提供沙箱。
191
+
192
+ 存储、迁移、服务注入、发布要求与威胁边界:[凭证与安装边界](docs/credential-storage.md)。
193
+
194
+ ## 架构
195
+
196
+ - 产品定义与通用客户端接入:[docs/device-mcp.md](docs/device-mcp.md)
197
+ - Device Relay 传输:子协议、连接 grant、重连、回退状态机:[docs/device-transport.md](docs/device-transport.md)
198
+ - Relay OAuth 2.1 Authorization Code + PKCE、发现与 token 端点:[docs/mcp-oauth-relay-contract.md](docs/mcp-oauth-relay-contract.md)
199
+ - Cloud 命令与授权:[docs/cloud.md](docs/cloud.md)
200
+ - Frely Network 命令(未发布):[docs/frely-network.md](docs/frely-network.md)
201
+
202
+ 公网 MCP URL 是 Relay 返回的规范资源,例如 `https://connect.frely.cloud/mcp/<device-id>`。请使用 `frely mcp url`,不要从控制面域名推导。URL 不含 bearer secret。
203
+
204
+ ## 命令
205
+
206
+ ```text
207
+ frely login [--relay <https-url>] [--no-browser]
208
+ frely logout
209
+ frely whoami
210
+ frely doctor [-v] [--json]
211
+ frely upgrade
212
+ frely skill install <manifest-url> [--host chatgpt|codex|claude-code|pi|generic] [--scope global|project] [--api-key-stdin] [--json]
213
+ frely skill status <distribution-id> [--json]
214
+ frely skill remove <distribution-id> [--json]
215
+ frely agent invoke <distribution-id> (--input <text>|--input-stdin) [--json]
216
+ frely provider share [ollama|openai-compatible] [--url <loopback-v1-url>] [--models <a,b>] [--slot <slot-id>] [--name <name>]
217
+ frely provider list [--json]
218
+ frely provider finalize <provider-id>
219
+ frely mcp [--workspace <path>] [--days 1..180]
220
+ frely mcp renew [--days 1..180]
221
+ frely mcp url [--json]
222
+ frely mcp serve [--workspace <path>]
223
+ frely mcp service start|stop|uninstall
224
+ frely mcp revoke
225
+ frely mcp stdio [--workspace <path>]
226
+ ```
227
+
228
+ `frely mcp serve` 是 Device Relay 客户端的前台形式。远程执行需要已批准的工作区与 MCP 租约;只有 Provider 的连接不开启 MCP。
229
+
230
+ `frely logout` 删除账号会话并尝试停止后台服务。`frely mcp revoke` 撤销 MCP 租约并删除其安全凭证;保留 Provider 设备与服务。
231
+
232
+ ## 落地页
233
+
234
+ 开源 CLI 落地页在 [`site/`](site/README.md),与 CLI 同处 Apache-2.0 许可与商标政策下,为 `cli.frely.cloud` 准备,作为静态站点独立部署。网站资源不打进 npm 包。
235
+
236
+ ## 许可与商标
237
+
238
+ `frely-cli` 采用 Apache License 2.0,见 [`LICENSE`](LICENSE)。Frely 名称、logo 与产品名不作为商标授权,见 [`TRADEMARKS.md`](TRADEMARKS.md)。
239
+
240
+ 开发指引见 [`CONTRIBUTING.md`](CONTRIBUTING.md),私有漏洞报告见 [`SECURITY.md`](SECURITY.md)。
@@ -3,7 +3,7 @@ export declare const COMMANDS: readonly [{
3
3
  readonly usage: "frely cloud list|describe|call|login|logout [--help]";
4
4
  readonly auth: "cloud-oauth";
5
5
  readonly effect: "subcommand-dependent";
6
- readonly purpose: "Discover and call Frely cloud business operations at app.frely.cloud/mcp. Use describe before call; parameters and results are JSON.";
6
+ readonly purpose: "Discover and call Frely cloud business operations at frely.cloud/mcp. Use describe before call; parameters and results are JSON.";
7
7
  }, {
8
8
  readonly id: "upgrade";
9
9
  readonly usage: "frely upgrade";
@@ -112,6 +112,24 @@ export declare const COMMANDS: readonly [{
112
112
  readonly auth: "account-and-mcp";
113
113
  readonly effect: "local-execution";
114
114
  readonly purpose: "Serve authorized local workspace tools.";
115
+ }, {
116
+ readonly id: "mcp.workspace.add";
117
+ readonly usage: "frely mcp workspace add <path>";
118
+ readonly auth: "mcp";
119
+ readonly effect: "local-write";
120
+ readonly purpose: "Register an additional workspace directory for this device.";
121
+ }, {
122
+ readonly id: "mcp.workspace.list";
123
+ readonly usage: "frely mcp workspace list [--json]";
124
+ readonly auth: "mcp";
125
+ readonly effect: "read";
126
+ readonly purpose: "List all registered workspace directories.";
127
+ }, {
128
+ readonly id: "mcp.workspace.remove";
129
+ readonly usage: "frely mcp workspace remove <path>";
130
+ readonly auth: "mcp";
131
+ readonly effect: "local-write";
132
+ readonly purpose: "Unregister a workspace directory (but not the primary).";
115
133
  }, {
116
134
  readonly id: "mcp.service";
117
135
  readonly usage: "frely mcp service start|stop|uninstall";
@@ -148,7 +166,7 @@ export declare function agentHelp(): {
148
166
  readonly usage: "frely cloud list|describe|call|login|logout [--help]";
149
167
  readonly auth: "cloud-oauth";
150
168
  readonly effect: "subcommand-dependent";
151
- readonly purpose: "Discover and call Frely cloud business operations at app.frely.cloud/mcp. Use describe before call; parameters and results are JSON.";
169
+ readonly purpose: "Discover and call Frely cloud business operations at frely.cloud/mcp. Use describe before call; parameters and results are JSON.";
152
170
  }, {
153
171
  readonly id: "upgrade";
154
172
  readonly usage: "frely upgrade";
@@ -257,6 +275,24 @@ export declare function agentHelp(): {
257
275
  readonly auth: "account-and-mcp";
258
276
  readonly effect: "local-execution";
259
277
  readonly purpose: "Serve authorized local workspace tools.";
278
+ }, {
279
+ readonly id: "mcp.workspace.add";
280
+ readonly usage: "frely mcp workspace add <path>";
281
+ readonly auth: "mcp";
282
+ readonly effect: "local-write";
283
+ readonly purpose: "Register an additional workspace directory for this device.";
284
+ }, {
285
+ readonly id: "mcp.workspace.list";
286
+ readonly usage: "frely mcp workspace list [--json]";
287
+ readonly auth: "mcp";
288
+ readonly effect: "read";
289
+ readonly purpose: "List all registered workspace directories.";
290
+ }, {
291
+ readonly id: "mcp.workspace.remove";
292
+ readonly usage: "frely mcp workspace remove <path>";
293
+ readonly auth: "mcp";
294
+ readonly effect: "local-write";
295
+ readonly purpose: "Unregister a workspace directory (but not the primary).";
260
296
  }, {
261
297
  readonly id: "mcp.service";
262
298
  readonly usage: "frely mcp service start|stop|uninstall";
@@ -1,6 +1,6 @@
1
1
  import { VERSION } from "./version.js";
2
2
  export const COMMANDS = [
3
- { id: "cloud", usage: "frely cloud list|describe|call|login|logout [--help]", auth: "cloud-oauth", effect: "subcommand-dependent", purpose: "Discover and call Frely cloud business operations at app.frely.cloud/mcp. Use describe before call; parameters and results are JSON." },
3
+ { id: "cloud", usage: "frely cloud list|describe|call|login|logout [--help]", auth: "cloud-oauth", effect: "subcommand-dependent", purpose: "Discover and call Frely cloud business operations at frely.cloud/mcp. Use describe before call; parameters and results are JSON." },
4
4
  { id: "upgrade", usage: "frely upgrade", auth: "none", effect: "local-write", purpose: "Upgrade the current installation to the latest stable release. Windows prints a manual command; doctor checks versions." },
5
5
  { id: "help", usage: "frely help --agent --json", auth: "none", effect: "read", purpose: "Read this installed CLI's current Agent instructions." },
6
6
  { id: "key.budget", usage: "frely key budget (--api-key-stdin [--relay <url>]|--distribution <distribution-id>) [--json]", auth: "api-key", effect: "read", purpose: "Read the Key's self usage and each funding source's limits without account login." },
@@ -19,6 +19,9 @@ export const COMMANDS = [
19
19
  { id: "mcp.renew", usage: "frely mcp renew [--days 1..180]", auth: "account-and-browser", effect: "authorization", purpose: "Renew this device's execution authorization without changing its MCP URL." },
20
20
  { id: "mcp.url", usage: "frely mcp url [--json]", auth: "account-and-mcp-or-browser", effect: "authorization-if-unconfigured", purpose: "Read this device's stable MCP URL. If unconfigured, set up the user home directory with browser approval and install the background service. --json includes HTTP transport and OAuth connection details; setup prompts go to stderr." },
21
21
  { id: "mcp.serve", usage: "frely mcp serve [--workspace <path>]", auth: "account-and-mcp", effect: "local-execution", purpose: "Serve authorized local workspace tools." },
22
+ { id: "mcp.workspace.add", usage: "frely mcp workspace add <path>", auth: "mcp", effect: "local-write", purpose: "Register an additional workspace directory for this device." },
23
+ { id: "mcp.workspace.list", usage: "frely mcp workspace list [--json]", auth: "mcp", effect: "read", purpose: "List all registered workspace directories." },
24
+ { id: "mcp.workspace.remove", usage: "frely mcp workspace remove <path>", auth: "mcp", effect: "local-write", purpose: "Unregister a workspace directory (but not the primary)." },
22
25
  { id: "mcp.service", usage: "frely mcp service start|stop|uninstall", auth: "none", effect: "local-service", purpose: "Pause, resume or remove the local MCP background service for maintenance. Supported upgrades restore running services without these commands." },
23
26
  { id: "mcp.revoke", usage: "frely mcp revoke", auth: "account", effect: "remote-write", purpose: "Revoke device MCP execution authorization for every connected client." },
24
27
  { id: "mcp.stdio", usage: "frely mcp stdio [--workspace <path>]", auth: "mcp", effect: "local-execution", purpose: "Serve authorized local tools over stdio." },
@@ -47,8 +50,54 @@ export function agentHelp() {
47
50
  commands: COMMANDS,
48
51
  };
49
52
  }
53
+ const GROUPS = [
54
+ { title: "Account", ids: ["login", "logout", "whoami"] },
55
+ { title: "Status", ids: ["doctor", "upgrade", "help"] },
56
+ { title: "Device MCP", ids: ["mcp.setup", "mcp.renew", "mcp.url", "mcp.serve", "mcp.workspace.add", "mcp.workspace.list", "mcp.workspace.remove", "mcp.revoke", "mcp.stdio"] },
57
+ { title: "Providers", ids: ["provider.share", "provider.list", "provider.finalize"] },
58
+ { title: "Skills & Agents", ids: ["skill.install", "skill.status", "skill.remove", "agent.invoke"] },
59
+ { title: "Cloud, Key & Network", ids: ["cloud", "key.budget", "network"] },
60
+ ];
61
+ // Short, flag-light forms for the human `--help` overview. The full usage
62
+ // strings (with every flag) live on COMMANDS and stay in `frely help --agent
63
+ // --json`, which this overview points to for the complete contract.
64
+ const SHORT_USAGE = {
65
+ login: "frely login [--relay <url>] [--no-browser]",
66
+ logout: "frely logout",
67
+ whoami: "frely whoami",
68
+ doctor: "frely doctor [-v] [--json]",
69
+ upgrade: "frely upgrade",
70
+ help: "frely help --agent --json",
71
+ "mcp.setup": "frely mcp [--workspace <path>] [--days 1..180]",
72
+ "mcp.renew": "frely mcp renew [--days 1..180]",
73
+ "mcp.url": "frely mcp url [--json]",
74
+ "mcp.serve": "frely mcp serve [--workspace <path>]",
75
+ "mcp.workspace.add": "frely mcp workspace add <path>",
76
+ "mcp.workspace.list": "frely mcp workspace list [--json]",
77
+ "mcp.workspace.remove": "frely mcp workspace remove <path>",
78
+ "mcp.revoke": "frely mcp revoke",
79
+ "mcp.stdio": "frely mcp stdio [--workspace <path>]",
80
+ "provider.share": "frely provider share [ollama|openai-compatible] [--models <a,b>]",
81
+ "provider.list": "frely provider list [--json]",
82
+ "provider.finalize": "frely provider finalize <provider-id>",
83
+ "skill.install": "frely skill install <manifest-url> [--host <host>] [--scope <scope>]",
84
+ "skill.status": "frely skill status <distribution-id>",
85
+ "skill.remove": "frely skill remove <distribution-id>",
86
+ "agent.invoke": "frely agent invoke <distribution-id> (--input <text>|--input-stdin)",
87
+ cloud: "frely cloud list|describe|call|login|logout",
88
+ "key.budget": "frely key budget (--api-key-stdin|--distribution <id>)",
89
+ network: "frely network setup|status|find|use|logout",
90
+ };
50
91
  export function cliUsage() {
51
- return "Usage:\n" + COMMANDS.filter((command) => command.id !== "mcp.service").map((command) => " " + command.usage + "\n").join("") + "\nMaintenance (not required for supported upgrades):\n frely mcp service start|stop|uninstall\n";
92
+ const lines = ["Usage: frely <command>\n"];
93
+ for (const group of GROUPS) {
94
+ lines.push(`\n${group.title}:\n`);
95
+ for (const id of group.ids)
96
+ lines.push(` ${SHORT_USAGE[id] ?? id}\n`);
97
+ }
98
+ lines.push("\nMaintenance (not required for supported upgrades):\n frely mcp service start|stop|uninstall\n");
99
+ lines.push("\nFull flags and the machine-readable contract: frely help --agent --json\n");
100
+ return lines.join("");
52
101
  }
53
102
  export function mcpUsage() {
54
103
  return "Device MCP — use this computer from a remote MCP client.\n\nUsage:\n"
@@ -58,7 +107,6 @@ export function mcpUsage() {
58
107
  + "Run setup on the computer to control. Add its MCP URL to ChatGPT, Claude Code on another computer, or another HTTP MCP client, then authorize with OAuth.\n"
59
108
  + "If MCP is unconfigured, frely mcp url runs setup for your home directory (~), waits for browser approval, then installs the background service. Setup prompts go to stderr; stdout contains only the URL or JSON.\n"
60
109
  + "Existing workspaces are preserved. Expired authorization requires frely mcp renew.\n"
61
- + "Clients share the device workspace and managed processes. Shell runs under the device OS account.\n"
62
- + "Compatibility: frely mcp setup and frely mcp chatgpt remain available.\n";
110
+ + "Clients share the device workspace and managed processes. Shell runs under the device OS account.\n";
63
111
  }
64
112
  //# sourceMappingURL=agent-help.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-help.js","sourceRoot":"","sources":["../src/agent-help.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-help.js","sourceRoot":"","sources":["../src/agent-help.ts"],"names":[],"mappings":"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"}
package/dist/auth.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface AuthCredential {
8
8
  value: string;
9
9
  refreshToken?: string;
10
10
  expiresAt?: number;
11
+ /** Bound Frely CLI session id returned by the Relay when per-device sessions are supported. */
12
+ sessionBindingId?: string;
11
13
  }
12
14
  export interface AuthSnapshot {
13
15
  configured: boolean;
@@ -33,6 +35,7 @@ export declare function loginDevice(relayInput?: string, notify?: (details: {
33
35
  user: PublicUser;
34
36
  verificationUri: string;
35
37
  userCode: string;
38
+ sessionBound: boolean;
36
39
  }>;
37
40
  export declare function whoami(): Promise<PublicUser>;
38
41
  export declare function requireLogin(): Promise<{
package/dist/auth.js CHANGED
@@ -10,12 +10,17 @@ const CONFIG_VERSION = 3;
10
10
  const LEGACY_CONFIG_VERSION = 1;
11
11
  const OAUTH_CLIENT_ID = "frely-cli-basic";
12
12
  const OAUTH_SCOPE = "openid profile profile:read email offline_access device-relay:provider";
13
- const DEFAULT_RELAY = "https://app.frely.cloud";
13
+ const DEFAULT_RELAY = "https://frely.cloud";
14
+ // app.frely.cloud now belongs to the Frely App; logins saved against it must be redone on frely.cloud.
15
+ const RETIRED_RELAY_HOSTNAME = "app.frely.cloud";
14
16
  const REQUEST_TIMEOUT_MS = 15_000;
15
17
  function debugAuth(message) {
16
18
  if (process.env.FRELY_DEBUG === "1")
17
19
  process.stderr.write(`[debug] auth ${message}\n`);
18
20
  }
21
+ const SESSION_BINDING_DEVICE_ID_PARAM = "session_binding_device_id";
22
+ const SESSION_BINDING_ID_FIELD = "frely_cli_session_id";
23
+ const LOCAL_DEVICE_ID_PREFIX = "frd_local_";
19
24
  export function authConfigPath() {
20
25
  const root = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
21
26
  return join(root, "frely", "config.json");
@@ -42,13 +47,14 @@ export async function login(relayOrEmail, legacyPassword, legacyRelayInput) {
42
47
  export async function loginDevice(relayInput, notify, options = {}) {
43
48
  const relayUrl = normalizeRelayUrl(relayInput);
44
49
  await probeCredentialStore();
50
+ const deviceId = localDeviceId((await readConfig().catch(() => null))?.deviceId);
45
51
  const device = await requestDeviceCode(relayUrl);
46
52
  const verificationUri = validateVerificationUrl(device.verification_uri_complete || `${device.verification_uri}?user_code=${encodeURIComponent(device.user_code)}`, relayUrl);
47
53
  notify?.({ verificationUri, userCode: device.user_code });
48
54
  if (options.openBrowser !== false)
49
55
  openVerificationUrl(verificationUri);
50
- const token = await pollDeviceToken(relayUrl, device);
51
- const user = await fetchUser(relayUrl, { scheme: "bearer", value: token.accessToken, ...(token.refreshToken ? { refreshToken: token.refreshToken } : {}), expiresAt: token.expiresAt });
56
+ const token = await pollDeviceToken(relayUrl, device, deviceId);
57
+ const user = await fetchUser(relayUrl, { scheme: "bearer", value: token.accessToken, ...(token.refreshToken ? { refreshToken: token.refreshToken } : {}), expiresAt: token.expiresAt, ...(token.sessionBindingId ? { sessionBindingId: token.sessionBindingId } : {}) });
52
58
  const key = accountKey(relayUrl);
53
59
  await credentialStore.setPassword(SERVICE, key, JSON.stringify({
54
60
  version: 1,
@@ -56,18 +62,35 @@ export async function loginDevice(relayInput, notify, options = {}) {
56
62
  accessToken: token.accessToken,
57
63
  ...(token.refreshToken ? { refreshToken: token.refreshToken } : {}),
58
64
  expiresAt: token.expiresAt,
65
+ ...(token.sessionBindingId ? { sessionBindingId: token.sessionBindingId } : {}),
59
66
  }));
60
67
  try {
61
- await writeConfig({ version: CONFIG_VERSION, relayUrl, user });
68
+ await writeConfig({ version: CONFIG_VERSION, relayUrl, user, deviceId });
62
69
  }
63
70
  catch (error) {
64
71
  await credentialStore.deletePassword(SERVICE, key).catch(() => false);
65
72
  throw error;
66
73
  }
67
- return { user, verificationUri, userCode: device.user_code };
74
+ return { user, verificationUri, userCode: device.user_code, sessionBound: Boolean(token.sessionBindingId) };
75
+ }
76
+ /**
77
+ * A stable local device identifier used to bind OAuth sessions to this machine.
78
+ * It is generated once and persisted in the CLI config so re-login reuses the
79
+ * same id, enabling per-device session management on Relay versions that support it.
80
+ */
81
+ function localDeviceId(existing) {
82
+ if (typeof existing === "string" && existing.length > 0)
83
+ return existing;
84
+ return LOCAL_DEVICE_ID_PREFIX + randomUUID().replace(/-/g, "");
85
+ }
86
+ function appendSessionBinding(body, deviceId) {
87
+ if (typeof deviceId === "string" && deviceId.length > 0) {
88
+ body.set(SESSION_BINDING_DEVICE_ID_PARAM, deviceId);
89
+ }
68
90
  }
69
91
  export async function whoami() {
70
92
  const config = await readConfig();
93
+ assertCurrentRelay(config);
71
94
  const credential = await loadCredential(config, true);
72
95
  if (!credential)
73
96
  throw new Error("No Frely login is stored. Run `frely login`.");
@@ -79,6 +102,7 @@ export async function whoami() {
79
102
  }
80
103
  export async function requireLogin() {
81
104
  const config = await readConfig();
105
+ assertCurrentRelay(config);
82
106
  const credential = await loadCredential(config, true);
83
107
  if (!credential)
84
108
  throw new Error("No Frely login is stored. Run `frely login`.");
@@ -177,6 +201,7 @@ export async function readConfiguredLocalCredential(relayInput) {
177
201
  value: stored.accessToken,
178
202
  ...(isString(stored.refreshToken) ? { refreshToken: stored.refreshToken } : {}),
179
203
  ...(typeof stored.expiresAt === "number" ? { expiresAt: stored.expiresAt } : {}),
204
+ ...(isString(stored.sessionBindingId) ? { sessionBindingId: stored.sessionBindingId } : {}),
180
205
  };
181
206
  }
182
207
  catch {
@@ -198,6 +223,11 @@ export async function probeCredentialStore() {
198
223
  throw new Error("Credential store deletion failed.");
199
224
  }
200
225
  }
226
+ function assertCurrentRelay(config) {
227
+ if (new URL(config.relayUrl).hostname === RETIRED_RELAY_HOSTNAME) {
228
+ throw new Error("Frely moved to https://frely.cloud. Run `frely login` again.");
229
+ }
230
+ }
201
231
  async function readConfig() {
202
232
  const raw = await readFile(authConfigPath(), "utf8").catch(() => null);
203
233
  if (!raw)
@@ -234,14 +264,16 @@ async function requestDeviceCode(relayUrl) {
234
264
  const interval = numberPayload(record.interval, 1, 300);
235
265
  return { device_code: record.device_code, user_code: record.user_code, verification_uri: record.verification_uri, verification_uri_complete: record.verification_uri_complete, expires_in: expiresIn, interval };
236
266
  }
237
- async function pollDeviceToken(relayUrl, device) {
267
+ async function pollDeviceToken(relayUrl, device, deviceId) {
238
268
  const deadline = Date.now() + device.expires_in * 1000;
239
269
  let intervalMs = device.interval * 1000;
240
270
  while (Date.now() < deadline) {
271
+ const body = new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: device.device_code, client_id: OAUTH_CLIENT_ID, resource: `${relayUrl}/api` });
272
+ appendSessionBinding(body, deviceId);
241
273
  const response = await fetchWithTimeout(`${relayUrl}/api/auth/oauth2/token`, {
242
274
  method: "POST",
243
275
  headers: { "content-type": "application/x-www-form-urlencoded", origin: relayUrl, accept: "application/json" },
244
- body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: device.device_code, client_id: OAUTH_CLIENT_ID, resource: `${relayUrl}/api` }).toString(),
276
+ body: body.toString(),
245
277
  redirect: "error",
246
278
  });
247
279
  const payload = await safeJson(response);
@@ -249,7 +281,7 @@ async function pollDeviceToken(relayUrl, device) {
249
281
  const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
250
282
  if (response.ok && isString(record.access_token) && (!record.token_type || String(record.token_type).toLowerCase() === "bearer")) {
251
283
  const expiresIn = numberPayload(record.expires_in, 1, 86_400);
252
- return { accessToken: record.access_token, ...(isString(record.refresh_token) ? { refreshToken: record.refresh_token } : {}), expiresAt: Date.now() + expiresIn * 1000 };
284
+ return { accessToken: record.access_token, ...(isString(record.refresh_token) ? { refreshToken: record.refresh_token } : {}), expiresAt: Date.now() + expiresIn * 1000, ...(isString(record[SESSION_BINDING_ID_FIELD]) ? { sessionBindingId: record[SESSION_BINDING_ID_FIELD] } : {}) };
253
285
  }
254
286
  const errorCode = typeof record.error === "string" ? record.error : "";
255
287
  if (errorCode === "authorization_pending") {
@@ -296,29 +328,35 @@ async function loadCredential(config, refresh, input) {
296
328
  if (stored.version !== 1 || stored.type !== "basic-oauth" || !isString(stored.accessToken) || typeof stored.expiresAt !== "number")
297
329
  return null;
298
330
  if (refresh && stored.refreshToken && stored.expiresAt <= Date.now() + 30_000) {
299
- const next = await refreshOAuthCredential(config.relayUrl, stored.refreshToken);
331
+ const next = await refreshOAuthCredential(config.relayUrl, stored.refreshToken, config.deviceId);
300
332
  if (next.expiresAt === undefined)
301
333
  return null;
302
- await credentialStore.setPassword(SERVICE, accountKey(config.relayUrl), JSON.stringify({ version: 1, type: "basic-oauth", accessToken: next.value, ...(next.refreshToken ? { refreshToken: next.refreshToken } : {}), expiresAt: next.expiresAt }));
334
+ const refreshed = { version: 1, type: "basic-oauth", accessToken: next.value, ...(next.refreshToken ? { refreshToken: next.refreshToken } : {}), expiresAt: next.expiresAt };
335
+ const sessionBindingId = next.sessionBindingId ?? stored.sessionBindingId;
336
+ if (sessionBindingId)
337
+ refreshed.sessionBindingId = sessionBindingId;
338
+ await credentialStore.setPassword(SERVICE, accountKey(config.relayUrl), JSON.stringify(refreshed));
303
339
  return next;
304
340
  }
305
- return { scheme: "bearer", value: stored.accessToken, ...(stored.refreshToken ? { refreshToken: stored.refreshToken } : {}), expiresAt: stored.expiresAt };
341
+ return { scheme: "bearer", value: stored.accessToken, ...(stored.refreshToken ? { refreshToken: stored.refreshToken } : {}), expiresAt: stored.expiresAt, ...(stored.sessionBindingId ? { sessionBindingId: stored.sessionBindingId } : {}) };
306
342
  }
307
343
  catch {
308
344
  return null;
309
345
  }
310
346
  }
311
- async function refreshOAuthCredential(relayUrl, refreshToken) {
347
+ async function refreshOAuthCredential(relayUrl, refreshToken, deviceId) {
348
+ const body = new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: OAUTH_CLIENT_ID, resource: `${relayUrl}/api` });
349
+ appendSessionBinding(body, deviceId);
312
350
  const response = await fetchWithTimeout(`${relayUrl}/api/auth/oauth2/token`, {
313
351
  method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", origin: relayUrl, accept: "application/json" },
314
- body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: OAUTH_CLIENT_ID, resource: `${relayUrl}/api` }).toString(), redirect: "error",
352
+ body: body.toString(), redirect: "error",
315
353
  });
316
354
  const payload = await safeJson(response);
317
355
  const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
318
356
  if (!response.ok || !isString(record.access_token))
319
357
  throw new Error("Frely login expired. Run `frely login`.");
320
358
  const expiresIn = numberPayload(record.expires_in, 1, 86_400);
321
- return { scheme: "bearer", value: record.access_token, ...(isString(record.refresh_token) ? { refreshToken: record.refresh_token } : { refreshToken }), expiresAt: Date.now() + expiresIn * 1000 };
359
+ return { scheme: "bearer", value: record.access_token, ...(isString(record.refresh_token) ? { refreshToken: record.refresh_token } : { refreshToken }), expiresAt: Date.now() + expiresIn * 1000, ...(isString(record[SESSION_BINDING_ID_FIELD]) ? { sessionBindingId: record[SESSION_BINDING_ID_FIELD] } : {}) };
322
360
  }
323
361
  export function openVerificationUrl(url) {
324
362
  if (process.env.FRELY_NO_BROWSER === "1")