@foxden-app/foxclaw 0.5.8 → 0.5.10

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/dist/types.d.ts CHANGED
@@ -25,6 +25,58 @@ export interface ChatSessionSettings {
25
25
  activeTurnMessageMode: ActiveTurnMessageMode | null;
26
26
  updatedAt: number;
27
27
  }
28
+ export type QueuedTurnInputStatus = 'queued' | 'processing' | 'completed' | 'cancelled' | 'failed';
29
+ export interface QueuedTurnInputRecord {
30
+ queueId: string;
31
+ scopeId: string;
32
+ chatId: string;
33
+ chatType: string;
34
+ topicId: number | null;
35
+ threadId: string;
36
+ inputJson: string;
37
+ sourceSummary: string;
38
+ messageId: number | null;
39
+ status: QueuedTurnInputStatus;
40
+ error: string | null;
41
+ createdAt: number;
42
+ updatedAt: number;
43
+ resolvedAt: number | null;
44
+ }
45
+ export type PendingAttachmentBatchStatus = 'pending' | 'consumed' | 'cleared';
46
+ export interface PendingAttachmentBatchRecord {
47
+ batchId: string;
48
+ scopeId: string;
49
+ chatId: string;
50
+ chatType: string;
51
+ topicId: number | null;
52
+ threadId: string;
53
+ cwd: string | null;
54
+ mediaGroupId: string | null;
55
+ attachmentsJson: string;
56
+ caption: string;
57
+ messageId: number | null;
58
+ status: PendingAttachmentBatchStatus;
59
+ createdAt: number;
60
+ updatedAt: number;
61
+ resolvedAt: number | null;
62
+ }
63
+ export type GuidedPlanSessionState = 'awaiting_confirmation' | 'executing' | 'cancelled' | 'completed';
64
+ export interface GuidedPlanSessionRecord {
65
+ sessionId: string;
66
+ scopeId: string;
67
+ chatId: string;
68
+ chatType: string;
69
+ topicId: number | null;
70
+ threadId: string;
71
+ turnId: string;
72
+ cwd: string | null;
73
+ planMarkdown: string;
74
+ messageId: number | null;
75
+ state: GuidedPlanSessionState;
76
+ createdAt: number;
77
+ updatedAt: number;
78
+ resolvedAt: number | null;
79
+ }
28
80
  export interface CachedThread {
29
81
  index: number;
30
82
  threadId: string;
@@ -332,6 +384,7 @@ export interface RuntimeStatus {
332
384
  currentBindings: number;
333
385
  pendingApprovals: number;
334
386
  pendingUserInputs: number;
387
+ queuedTurns: number;
335
388
  activeTurns: number;
336
389
  lastError: string | null;
337
390
  updatedAt: string;
package/dist/update.d.ts CHANGED
@@ -7,6 +7,8 @@ export interface SelfUpdateStatus {
7
7
  locale: AppLocale;
8
8
  fromVersion: string;
9
9
  toVersion: string | null;
10
+ releaseNotes?: string[] | null;
11
+ releaseNotesVersion?: string | null;
10
12
  codexUpdate?: string | null;
11
13
  codexFromVersion?: string | null;
12
14
  codexToVersion?: string | null;
@@ -74,4 +76,5 @@ export declare function buildSelfUpdateLaunchCommand(options: {
74
76
  unitName?: string;
75
77
  }): SelfUpdateLaunchCommand;
76
78
  export declare function performSelfUpdate(options: PerformSelfUpdateOptions): SelfUpdateOutcome;
79
+ export declare function extractReleaseNotes(changelog: string, version: string, locale: AppLocale): string[] | null;
77
80
  export {};
package/dist/update.js CHANGED
@@ -132,6 +132,10 @@ export function readSelfUpdateStatus(statusFile) {
132
132
  locale: parsed.locale,
133
133
  fromVersion: parsed.fromVersion,
134
134
  toVersion: typeof parsed.toVersion === 'string' ? parsed.toVersion : null,
135
+ ...(Array.isArray(parsed.releaseNotes) ? {
136
+ releaseNotes: parsed.releaseNotes.filter((entry) => typeof entry === 'string'),
137
+ } : {}),
138
+ ...(typeof parsed.releaseNotesVersion === 'string' ? { releaseNotesVersion: parsed.releaseNotesVersion } : {}),
135
139
  ...(typeof parsed.codexUpdate === 'string' ? { codexUpdate: parsed.codexUpdate } : {}),
136
140
  ...(typeof parsed.codexFromVersion === 'string' ? { codexFromVersion: parsed.codexFromVersion } : {}),
137
141
  ...(typeof parsed.codexToVersion === 'string' ? { codexToVersion: parsed.codexToVersion } : {}),
@@ -185,6 +189,8 @@ export function createSelfUpdateRuntime(options) {
185
189
  locale,
186
190
  fromVersion: options.version,
187
191
  toVersion: null,
192
+ releaseNotes: null,
193
+ releaseNotesVersion: null,
188
194
  codexUpdate: null,
189
195
  codexFromVersion: null,
190
196
  codexToVersion: null,
@@ -229,6 +235,8 @@ export function createSelfUpdateRuntime(options) {
229
235
  locale,
230
236
  fromVersion: options.version,
231
237
  toVersion: null,
238
+ releaseNotes: null,
239
+ releaseNotesVersion: null,
232
240
  codexUpdate: null,
233
241
  codexFromVersion: null,
234
242
  codexToVersion: null,
@@ -296,9 +304,10 @@ export function performSelfUpdate(options) {
296
304
  runInherited(installer.command, installer.installArgs, installerEnv);
297
305
  const updatedEntryPoint = resolveUpdatedEntryPoint(installer, installerEnv);
298
306
  toVersion = readInstalledPackageVersion(updatedEntryPoint);
307
+ const releaseNotes = readInstalledReleaseNotes(updatedEntryPoint, toVersion, options.notificationFile);
299
308
  console.log('[UPDATE] Running checks and restarting the FoxClaw service...');
300
309
  runInherited(options.nodePath, [updatedEntryPoint, 'start'], installerEnv);
301
- completeNotification(options.notificationFile, 'succeeded', toVersion, codexUpdate, null);
310
+ completeNotification(options.notificationFile, 'succeeded', toVersion, codexUpdate, null, releaseNotes);
302
311
  console.log(`[OK] FoxClaw updated and restarted: ${options.version} -> ${toVersion}`);
303
312
  return {
304
313
  ok: true,
@@ -309,7 +318,7 @@ export function performSelfUpdate(options) {
309
318
  }
310
319
  catch (error) {
311
320
  const message = formatError(error);
312
- completeNotification(options.notificationFile, 'failed', toVersion, codexUpdate, message);
321
+ completeNotification(options.notificationFile, 'failed', toVersion, codexUpdate, message, null);
313
322
  console.error(`[FAIL] FoxClaw update failed: ${message}`);
314
323
  return {
315
324
  ok: false,
@@ -456,7 +465,65 @@ function readInstalledPackageVersion(updatedEntryPoint) {
456
465
  return 'unknown';
457
466
  }
458
467
  }
459
- function completeNotification(notificationFile, state, toVersion, codexUpdate, error) {
468
+ function readInstalledReleaseNotes(updatedEntryPoint, version, notificationFile) {
469
+ if (!version || version === 'unknown') {
470
+ return null;
471
+ }
472
+ const pending = notificationFile ? readSelfUpdateStatus(notificationFile) : null;
473
+ const locale = pending?.locale ?? 'zh';
474
+ const changelogPath = path.resolve(path.dirname(updatedEntryPoint), '..', 'CHANGELOG.md');
475
+ try {
476
+ return extractReleaseNotes(fs.readFileSync(changelogPath, 'utf8'), version, locale);
477
+ }
478
+ catch {
479
+ return null;
480
+ }
481
+ }
482
+ export function extractReleaseNotes(changelog, version, locale) {
483
+ const escapedVersion = escapeRegExp(version.replace(/^v/i, ''));
484
+ const versionHeadingPattern = new RegExp(`^##\\s+\\[?v?${escapedVersion}\\]?\\b.*$`, 'im');
485
+ const versionMatch = versionHeadingPattern.exec(changelog);
486
+ if (!versionMatch || versionMatch.index === undefined) {
487
+ return null;
488
+ }
489
+ const sectionStart = versionMatch.index + versionMatch[0].length;
490
+ const nextHeadingMatch = /^##\s+/m.exec(changelog.slice(sectionStart));
491
+ const versionSection = nextHeadingMatch
492
+ ? changelog.slice(sectionStart, sectionStart + nextHeadingMatch.index)
493
+ : changelog.slice(sectionStart);
494
+ const localizedSection = extractLocalizedReleaseNoteSection(versionSection, locale) ?? versionSection;
495
+ const bullets = localizedSection
496
+ .split(/\r?\n/)
497
+ .map(line => line.trim())
498
+ .filter(line => /^[-*]\s+/.test(line))
499
+ .map(line => line.replace(/^[-*]\s+/, '').trim())
500
+ .filter(Boolean)
501
+ .slice(0, 8);
502
+ return bullets.length > 0 ? bullets : null;
503
+ }
504
+ function extractLocalizedReleaseNoteSection(section, locale) {
505
+ const headingPattern = /^###\s+(.+)$/gm;
506
+ const headings = [...section.matchAll(headingPattern)];
507
+ if (headings.length === 0) {
508
+ return null;
509
+ }
510
+ const preferred = locale === 'zh' ? ['中文', 'Chinese'] : ['English', '英文'];
511
+ for (let index = 0; index < headings.length; index += 1) {
512
+ const heading = headings[index];
513
+ const headingText = heading[1].trim().toLowerCase();
514
+ if (!preferred.some(label => headingText === label.toLowerCase())) {
515
+ continue;
516
+ }
517
+ const start = heading.index + heading[0].length;
518
+ const next = headings[index + 1];
519
+ return next ? section.slice(start, next.index) : section.slice(start);
520
+ }
521
+ return null;
522
+ }
523
+ function escapeRegExp(value) {
524
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
525
+ }
526
+ function completeNotification(notificationFile, state, toVersion, codexUpdate, error, releaseNotes) {
460
527
  if (!notificationFile) {
461
528
  return;
462
529
  }
@@ -468,6 +535,8 @@ function completeNotification(notificationFile, state, toVersion, codexUpdate, e
468
535
  ...pending,
469
536
  state,
470
537
  toVersion,
538
+ releaseNotes,
539
+ releaseNotesVersion: releaseNotes && toVersion ? toVersion : null,
471
540
  codexUpdate: codexUpdate?.message ?? null,
472
541
  codexFromVersion: codexUpdate?.fromVersion ?? null,
473
542
  codexToVersion: codexUpdate?.toVersion ?? null,
@@ -0,0 +1,341 @@
1
+ # Codex 已经进手机了,但重度开发者真正缺的,是这套“运行层”
2
+
3
+ Codex 变强之后,开发者的兴奋点很容易落在模型上。
4
+
5
+ 能不能写更大的模块?能不能自己跑测试?能不能开多个 agent?能不能从手机上继续看进展?
6
+
7
+ 这些当然重要。
8
+
9
+ 但真正把 Codex 当生产力工具用起来的人,很快会遇到另一个更现实的问题:
10
+
11
+ 模型会写代码,不代表这条工作流能一直跑下去。
12
+
13
+ 它可能在你离开电脑后卡在审批上;可能跑到一半发现当前账号额度见底;可能某个 `auth.json` 已经 10 天没刷新;可能家里机器刚刷新出新 token,办公室机器还拿着旧副本;也可能 Telegram 显示“已发送”,但你根本不知道对端到底有没有导入成功。
14
+
15
+ 这才是 vibe coding 的最后一公里。
16
+
17
+ 不是“AI 会不会写代码”,而是“AI 写代码这件事,能不能稳定进入人的真实工作节奏”。
18
+
19
+ FoxClaw 做的,就是这一层。
20
+
21
+ 它不是一个简单的 Telegram 转发器,而是一套围绕本机 Codex 搭起来的运行层:手机控制、线程观察、审批接管、auth 生命周期、多 bot runtime、跨节点同步、同步可观测性,都在这套系统里打通。
22
+
23
+ ## 官方移动端很好,但边界不一样
24
+
25
+ OpenAI 正在把 Codex 带到更多入口。
26
+
27
+ 官方 Codex App 强调多 agent、worktree、云环境和跨界面的统一体验;ChatGPT 移动端也开始提供 Codex 远程访问能力,让用户可以在手机上连接正在运行 Codex 的主机,继续线程、回答问题、调整方向、审批动作、查看上下文。
28
+
29
+ 这条路线很清楚:官方要把 Codex 变成更原生、更统一的产品体验。
30
+
31
+ 但重度用户会走到另一层:
32
+
33
+ - 我不只一个 Codex 账号,额度窗口不同,有 Plus,也有 Free,还有备用账号。
34
+ - 我不只一个 bot,同一台机器上可能有默认 bot、隔离 bot、测试 bot、项目 bot。
35
+ - 我不只一台机器,WSL、Mac、家里电脑、办公室工作站都可能跑 Codex。
36
+ - 我需要观察电脑终端里已经启动的任务,而不是每次在手机上新开一个会话。
37
+ - 我需要知道 auth 到底是谁最新、谁失效、谁被跳过、谁导入成功。
38
+
39
+ 官方移动端解决的是通用入口和官方闭环。
40
+
41
+ FoxClaw 解决的是自管本机 Codex 的最后一公里。
42
+
43
+ 这两件事不是互相替代。一个像官方驾驶舱,一个像你自己机房里的运行保障系统。
44
+
45
+ 官方产品没有必要替你管理本地 `~/.codex/auth.json_*` 候选池,也不会替你在多台自管机器之间同步 refresh 后的 auth 文件,更不会替你设计一套 Telegram bot-to-bot 的 auth 运维协议。
46
+
47
+ FoxClaw 的看点就在这里:它把这些“真实使用中一定会冒出来,但官方入口不负责兜底”的问题,系统化了。
48
+
49
+ ## 真正的痛点:auth 不是一个文件,是一条生命周期
50
+
51
+ 在轻度使用时,Codex 的 auth 只是一个 `auth.json`。
52
+
53
+ 重度使用以后,它会变成一组状态:
54
+
55
+ 谁是当前账号?谁还有额度?谁是 Plus?谁是 Free?谁已经禁用?谁长时间没刷新?谁 refresh 失败?谁只是单候选失效,不应该污染全局同步状态?
56
+
57
+ FoxClaw 把 auth 当成生命周期对象来管理。
58
+
59
+ 在 `/auth` 面板里,用户看到的不再是一个孤零零的登录文件,而是一组候选账号:
60
+
61
+ - 额度窗口;
62
+ - 套餐类型;
63
+ - 当前使用项;
64
+ - 是否参与自动轮转;
65
+ - 最近刷新时间;
66
+ - 是否长期未刷新;
67
+ - 是否失效;
68
+ - 是否需要关注。
69
+
70
+ 候选多的时候,可以分页、搜索、筛选“全部 / 已启用 / 需关注”。一个账号触发用量限制或认证错误时,FoxClaw 会尝试切到下一个可用候选,重启对应 runtime 的 app-server,并用新账号重试刚才失败的请求。
71
+
72
+ 这不是为了“多账号炫技”。
73
+
74
+ 它解决的是一个非常朴素的问题:Codex 写到一半,不要因为一个窗口耗尽就停摆。
75
+
76
+ 更进一步,FoxClaw 还处理 refresh token 的维护。
77
+
78
+ OpenAI 没有公开 ChatGPT refresh token 的固定有效期,也没有公开旧 token 的重放宽限期。盲目定时刷新并不安全,因为 refresh 会轮换 token;如果新 token 已经被消费,但进程、网络或磁盘在保存前出错,候选可能就要重新登录。
79
+
80
+ 所以 FoxClaw 的策略更克制:
81
+
82
+ - `/auth refresh all` 是人工维护命令,需要显式确认风险;
83
+ - 已启用 ChatGPT 候选 `last_refresh` 超过 9 天时,后台才主动关注;
84
+ - 主动刷新必须等所有 runtime 空闲,没有审批、待输入、登录流程和 auth 镜像写入;
85
+ - 如果启用跨节点同步,刷新前还要先拿到跨节点刷新锁。
86
+
87
+ 它不是尽可能频繁刷新,而是在风险窗口来临前,用最小扰动把 auth 维护住。
88
+
89
+ ## 多 bot 是另一个分水岭
90
+
91
+ 很多工具做到“手机能发消息给 Codex”就停了。
92
+
93
+ FoxClaw 继续往下做了一层:同一台机器上可以跑多个 Telegram bot,而这些 bot 不必互相踩 auth。
94
+
95
+ 它仍然只运行一个 FoxClaw 服务,但默认会给每个 bot 启动独立的 `codex app-server`、独立 Codex home、独立当前 auth 选择。
96
+
97
+ 这意味着:
98
+
99
+ - A bot 正在跑长任务,B bot 仍然可以独立切换 auth;
100
+ - 一个隔离 bot 换号,不会直接影响默认终端 runtime;
101
+ - 如果你想让某个 bot 和终端共用 session,也可以显式把它设为默认/终端共享 bot;
102
+ - 已验证的登录或刷新凭据会镜像到同节点其他 bot home,但每个 bot 当前选择仍然独立。
103
+
104
+ 这是实际使用里的关键能力。
105
+
106
+ 因为 auth 切换不是一个 UI 动作。它牵涉 app-server、当前线程、审批状态、待输入状态、镜像写入和重启顺序。FoxClaw 会在 runtime 忙碌时拒绝切换,避免正在跑的请求被中途换号破坏。
107
+
108
+ 一个人用 Codex 可能只需要一个入口。
109
+
110
+ 一个真正把 Codex 当工作台的人,很快就需要多个隔离入口。
111
+
112
+ FoxClaw 正是在这个阶段开始体现差异。
113
+
114
+ ## 跨节点同步:哪台机器最新,不该靠人记
115
+
116
+ 节点内多 bot 解决的是一台机器的问题。
117
+
118
+ 跨节点 auth 同步解决的是另一件事:多台机器怎么共享同一组合法拥有的 ChatGPT auth 候选?
119
+
120
+ 场景很常见。
121
+
122
+ 家里机器上的 Codex 自动刷新了 token。办公室机器还在用旧副本。WSL 里跑了一个任务,Mac 上另一个 bot 也需要同一个账号。某台机器发现当前 auth 不可用了,它应该先问本机其他 runtime,还是去问另一个节点?
123
+
124
+ 靠人手工拷文件,迟早会乱。
125
+
126
+ FoxClaw 用 Telegram Bot-to-Bot 做了一套低频控制面。
127
+
128
+ 每台机器选择一个联系人 bot。联系人 bot 之间通过 Telegram 私聊传输加密同步包。同步包用 AES-256-GCM 加密,auth 内容、候选名、account id、`last_refresh` 都在密文里。接收方只接受 allowlist 里的 peer,密钥、cluster、nonce 或 payload 校验失败都不会写盘。
129
+
130
+ 这套机制只适用于你合法拥有并维护的 ChatGPT 账号、auth 文件和机器。它不是账号来源不明时的“搬运工具”,也不是绕过授权边界的方案。
131
+
132
+ 跨节点同步分三条路径:
133
+
134
+ 1. **Push**:本节点登录、Codex 自动刷新或手动刷新成功,并通过 usage 验证后,把较新的候选加密推给 peer。
135
+ 2. **Pull**:本节点发现当前候选不可用,先查同节点镜像;如果没有可用较新副本,再向 peer 请求同名同账号的较新候选。
136
+ 3. **Lease**:凡是会旋转 refresh token 的操作,例如 `/auth refresh all confirm` 或后台 9 天主动刷新,必须先申请跨节点刷新锁。任一 peer 忙碌、拒绝或超时,都阻止本轮刷新。
137
+
138
+ 这里最重要的是克制。
139
+
140
+ 跨节点恢复不会直接刷新 token,它只拉取 peer 已经持有的有效副本。找不到有效副本,就明确提示人工介入,用设备登录或 `/auth add <name>` 重新生成可用 auth。
141
+
142
+ 它不会把“恢复一个坏 auth”升级成“多台机器同时抢着轮换 refresh token”。
143
+
144
+ 真正会旋转 token 的动作,都要走 lease。
145
+
146
+ ## Bot-to-Bot 如果不可观察,就只是玄学
147
+
148
+ Telegram Bot-to-Bot 适合做这件事,是因为它天然跨 NAT,不需要公网 IP、FRP 或反向代理。auth 包、pull 请求、lease 请求、test ping/pong 都是低频小消息,很适合作为控制面。
149
+
150
+ 但它也有一个问题:发送成功不等于对端导入成功。
151
+
152
+ Telegram 收下了消息,只说明本节点发出去了。对端有没有收到?有没有解密?有没有通过 allowlist?是不是全局空闲?usage 验证有没有过?同名候选是不是同一个 account id?最后是导入、跳过还是失败?
153
+
154
+ 没有观测,这些都会变成玄学。
155
+
156
+ FoxClaw 0.5.x 把这部分补成了第一等能力。
157
+
158
+ `/auth sync status` 会显示 node id、联系人 bot、peer、最近发送、最近接收、最近导入、最近拉取、peer 活跃时间、同步系统级错误、单候选失败和最近事件。
159
+
160
+ `/auth sync events [过滤]` 可以按候选名、peer、requestId、事件类型、阶段、详情过滤。
161
+
162
+ `/auth sync trace <requestId>` 可以追一条请求的完整链路。
163
+
164
+ 一次正常的测试,现在会变成这样的流水:
165
+
166
+ ```text
167
+ out | test.ping | sent | peer=@peer_bot | requestId=...
168
+ in | test.pong | received | peer=@peer_bot | requestId=...
169
+ local | test.pong | matched | peer=@peer_bot | requestId=...
170
+ ```
171
+
172
+ 一次成功导入,会看到:
173
+
174
+ ```text
175
+ in | push.bundle | received | candidate=auth.json_xxx
176
+ local | push.bundle | processing | candidate=auth.json_xxx
177
+ local | push.bundle | imported | candidate=auth.json_xxx
178
+ ```
179
+
180
+ 单候选失败也不会再污染全局状态。
181
+
182
+ 比如某个远端候选返回 `token_invalidated`,它会进入“候选失败”,而不是把整个 auth sync 标成系统故障。用户看到的是“这个候选坏了”,不是“整套同步坏了”。
183
+
184
+ 这就是从脚本走向系统的差别。
185
+
186
+ ## 手机不是通知器,而是接管台
187
+
188
+ auth 管理解决的是 Codex 能不能继续跑。
189
+
190
+ 手机控制解决的是人能不能继续参与。
191
+
192
+ FoxClaw 的 `/watch` 很能说明它的定位。
193
+
194
+ 你可以在电脑上的 Codex CLI 里启动长任务,然后离开电脑,用手机观察同一个线程的进展。观察模式会同步 live turn 进度和审批请求;观察中的聊天默认只读,避免手机上的普通消息误插入正在观察的 turn。
195
+
196
+ 需要接管时,再明确使用:
197
+
198
+ - `/steer`:把新信息引导到当前 turn;
199
+ - `/queue`:排到下一轮;
200
+ - `/takeover`:中断当前 turn,用新消息接管;
201
+ - `/unwatch`:退出观察。
202
+
203
+ 这不是“手机也能聊天”。
204
+
205
+ 这是把手机变成 Codex 的接管台。
206
+
207
+ FoxClaw 还把 `/threads`、`/open`、`/where`、`/setup`、审批按钮、权限预设、Agent/Plan 模式、模型和 reasoning effort 都做成了手机端可操作的面板。
208
+
209
+ 人可以离开电脑,但不必离开工作流。
210
+
211
+ ## 这类客户最该试
212
+
213
+ FoxClaw 不适合所有人。
214
+
215
+ 如果你只是偶尔问 Codex 几个问题,官方入口已经很好。
216
+
217
+ 但如果你已经把 Codex 放进真实开发工作流,下面这些问题会越来越频繁:
218
+
219
+ - 它能不能连续跑几个小时?
220
+ - 我离开电脑后能不能继续看进度?
221
+ - 它要权限时我能不能及时批准?
222
+ - 当前账号额度耗尽后能不能自动换号?
223
+ - 多个 bot、多个任务、多个账号能不能互不干扰?
224
+ - 某台机器上的 auth 刷新后,其他机器能不能自动拿到?
225
+ - 同步失败时,我能不能知道到底卡在 peer、候选、usage 验证还是本机忙碌?
226
+
227
+ FoxClaw 最适合三类用户:
228
+
229
+ - **Codex 重度个人开发者**:已经把 Codex 用在真实项目里,希望吃饭、通勤、陪家人时仍能观察进度、审批动作、必要时接管。
230
+ - **AI 编程工作室或小团队**:有多台机器、多套账号、多条任务线,需要把额度、auth、bot 和节点协同起来,而不是靠人肉记忆维护。
231
+ - **强调本机控制的技术团队**:希望代码、shell、认证、审批和运行数据留在自己的机器上,只把受信任的聊天入口作为控制面。
232
+
233
+ 它带来的不是“功能更多”,而是中断更少。
234
+
235
+ ## 想试,怎么装
236
+
237
+ FoxClaw 是开源项目。
238
+
239
+ - GitHub:https://github.com/foxden-app/foxclaw
240
+ - npm:https://www.npmjs.com/package/@foxden-app/foxclaw
241
+
242
+ 最低准备:
243
+
244
+ - 一台已经能运行 Codex CLI 的 macOS 或 Linux 机器;
245
+ - Node.js 24+;
246
+ - 一个 Telegram bot token;
247
+ - 你的 Telegram 数字用户 ID;
248
+ - 一份已经登录过的 Codex。
249
+
250
+ 最短安装路径:
251
+
252
+ ```bash
253
+ npm install -g @foxden-app/foxclaw
254
+ foxclaw init
255
+ foxclaw doctor
256
+ foxclaw start
257
+ ```
258
+
259
+ pnpm 用户:
260
+
261
+ ```bash
262
+ pnpm add -g @foxden-app/foxclaw
263
+ foxclaw init
264
+ foxclaw doctor
265
+ foxclaw start
266
+ ```
267
+
268
+ `foxclaw init` 会生成 `~/.foxclaw/.env`,引导你填写 Telegram bot token、Telegram 用户 ID 和默认工作目录。`foxclaw doctor` 检查环境。`foxclaw start` 安装并启动后台服务。
269
+
270
+ 启动后,在 Telegram 私聊你的 bot,按这个顺序验证:
271
+
272
+ ```text
273
+ /help
274
+ /status
275
+ /setup
276
+ List files in DEFAULT_CWD.
277
+ ```
278
+
279
+ 能收到 Codex 返回,就说明手机到本机 Codex 的基础链路跑通了。
280
+
281
+ 然后再看:
282
+
283
+ - `/auth`:账号候选、额度和自动轮转;
284
+ - `/threads`:线程列表和绑定;
285
+ - `/watch`:观察电脑上已经跑起来的任务;
286
+ - `/setup`:模型、权限、Agent/Plan、Active turn 行为;
287
+ - `/auth sync status`:跨节点同步状态。
288
+
289
+ 如果你要在手机上发起设备码登录,需要先在 ChatGPT 网页左下角点用户名,进入“设置 > 安全”,启用“为 Codex 启用设备代码授权”。这是 ChatGPT/Codex 的前置开关,不是 FoxClaw 自己能绕过的设置。
290
+
291
+ 如果你手头已经有 Codex、OpenClaw、QwenPaw、OpenCode、Kimi CLI 这类能跑 shell 的 Agent,也可以让 Agent 帮你装。FoxClaw 仓库里有 Agent 辅助安装指南,会把写 `.env`、跑 doctor、装服务、验证 Telegram 回复这些步骤串起来。
292
+
293
+ ## 卡住了,先看这里
294
+
295
+ 安装 FoxClaw 最常见的卡点不是代码,而是环境拼图。
296
+
297
+ - **Telegram 没回复**:先确认 bot token、`TG_ALLOWED_USER_ID`、服务是否启动,再看 `foxclaw status` 和 `journalctl --user -u foxclaw.service -f`。
298
+ - **Codex 不能启动或不能登录**:先在电脑终端确认 `codex` CLI 本身可用,再回到 FoxClaw。
299
+ - **设备码登录失败**:确认 ChatGPT 设置里已经启用“为 Codex 启用设备代码授权”,并且 workspace 管理员没有禁用设备码登录。
300
+ - **auth 切换或刷新被拒绝**:通常是当前有 turn、审批、待输入、登录流程或 auth 镜像写入正在进行。等空闲后再试。
301
+ - **跨节点同步没导入**:先跑 `/auth sync test`,再看 `/auth sync status`、`/auth sync events <候选名>` 或 `/auth sync trace <requestId>`,区分是 peer 不通、候选不更新、usage 验证失败,还是本机忙碌。
302
+
303
+ 文档入口也都在仓库里:
304
+
305
+ - 新手安装指南:`docs/zh/install-for-beginners.md`
306
+ - 用户手册:`docs/zh/user-manual.md`
307
+ - 跨节点 auth 同步配置:`docs/zh/cross-node-auth-sync.md`
308
+ - 故障排查:`docs/zh/troubleshooting.md`
309
+
310
+ 这篇文章讲的是为什么值得装。真正动手时,按安装指南一步步来;遇到错误,不要靠猜,先跑 `doctor`,再看服务日志和对应文档。
311
+
312
+ FoxClaw 的目标不是让你成为运维专家,而是把常见坑都暴露成能检查、能定位、能恢复的状态。
313
+
314
+ ## 最后
315
+
316
+ Codex 的能力越强,用户越会把更长、更复杂、更重要的任务交给它。
317
+
318
+ 任务一旦变长,人就不可能一直坐在电脑前盯着。
319
+
320
+ 于是,真正的竞争点会从“谁能生成代码”,转向“谁能让 agent 稳定进入人的工作节奏”。
321
+
322
+ 官方移动端会解决通用入口和官方闭环。
323
+
324
+ FoxClaw 解决的是自管本机 Codex 的最后一公里:
325
+
326
+ - 代码、shell、认证、审批和运行数据留在本机;
327
+ - Telegram 或微信成为可信控制入口;
328
+ - 多 bot、多账号、多节点可以协同;
329
+ - auth 有生命周期,而不是一个容易过期的文件;
330
+ - 同步有协议、有锁、有验证、有事件流水;
331
+ - 用户可以观察、审批、引导、排队、接管。
332
+
333
+ 你可以离开电脑,但 Codex 不必停。
334
+
335
+ 你可以换到手机,但工作不必断。
336
+
337
+ 你可以有多个账号、多台机器、多条线程,但不必靠记忆和手工拷文件维护它们。
338
+
339
+ 这就是 FoxClaw 想解决的问题:
340
+
341
+ 让 vibe coding 从一次灵感驱动的对话,变成一套能持续运行、能被观察、能被接管、能被维护的真实开发工作流。
@@ -250,17 +250,17 @@ Create a short README-style summary of this folder.
250
250
 
251
251
  ## 10. Service Commands
252
252
 
253
- On Linux, `foxclaw start` manages a user-level systemd service. Check it with:
253
+ On Linux, `foxclaw start` manages a user-level systemd service and tries to enable systemd user linger so the service keeps running after you leave SSH or log out. Check it with:
254
254
 
255
255
  ```bash
256
256
  systemctl --user status foxclaw.service
257
257
  journalctl --user -u foxclaw.service -f
258
258
  ```
259
259
 
260
- The service starts again when your user session starts. If you need it to start after reboot before you log in, run:
260
+ If install reports that linger could not be enabled automatically, run:
261
261
 
262
262
  ```bash
263
- loginctl enable-linger "$USER"
263
+ sudo loginctl enable-linger "$USER"
264
264
  ```
265
265
 
266
266
  On macOS, `foxclaw start` manages launchd and starts FoxClaw when you log in.
@@ -261,10 +261,10 @@ Linux user systemd:
261
261
  systemctl --user is-enabled foxclaw.service
262
262
  ```
263
263
 
264
- `enabled` means it starts with your user session. To start after reboot before login:
264
+ `enabled` means it starts with your user session. `foxclaw start` tries to enable systemd user linger automatically so the service keeps running after SSH logout and before login. If automatic linger setup fails:
265
265
 
266
266
  ```bash
267
- loginctl enable-linger "$USER"
267
+ sudo loginctl enable-linger "$USER"
268
268
  ```
269
269
 
270
270
  macOS launchd starts FoxClaw when you log in after running:
@@ -248,17 +248,17 @@ Create a short README-style summary of this folder.
248
248
 
249
249
  ## 10. 服务命令
250
250
 
251
- Linux 上 `foxclaw start` 管理用户级 systemd 服务。查看状态:
251
+ Linux 上 `foxclaw start` 管理用户级 systemd 服务,并会尝试启用 systemd user linger,让服务在你退出 SSH 或桌面会话后继续运行。查看状态:
252
252
 
253
253
  ```bash
254
254
  systemctl --user status foxclaw.service
255
255
  journalctl --user -u foxclaw.service -f
256
256
  ```
257
257
 
258
- 如果希望重启后未登录也能启动用户服务:
258
+ 如果安装时提示 linger 启用失败,手动执行:
259
259
 
260
260
  ```bash
261
- loginctl enable-linger "$USER"
261
+ sudo loginctl enable-linger "$USER"
262
262
  ```
263
263
 
264
264
  macOS 上 `foxclaw start` 管理 launchd,并在你登录后启动 FoxClaw。
@@ -262,10 +262,10 @@ Linux 用户级 systemd:
262
262
  systemctl --user is-enabled foxclaw.service
263
263
  ```
264
264
 
265
- `enabled` 表示会随用户会话启动。如果希望机器重启后未登录也启动用户服务:
265
+ `enabled` 表示会随用户会话启动。`foxclaw start` 会尝试自动启用 systemd user linger,让服务在退出 SSH 或未登录时也继续运行。如果自动启用失败:
266
266
 
267
267
  ```bash
268
- loginctl enable-linger "$USER"
268
+ sudo loginctl enable-linger "$USER"
269
269
  ```
270
270
 
271
271
  macOS 上,运行过下面命令后,FoxClaw 会在你登录时由 launchd 启动:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",