agy-crew 0.0.0-stage → 0.9.2

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 (66) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +315 -2
  3. package/README.zh-CN.md +314 -0
  4. package/companion/accounts.mjs +300 -0
  5. package/companion/agy-companion.mjs +2136 -0
  6. package/companion/identity.mjs +25 -0
  7. package/companion/install.mjs +443 -0
  8. package/companion/observation.mjs +147 -0
  9. package/companion/pool/env.mjs +14 -0
  10. package/companion/pool/lease.mjs +89 -0
  11. package/companion/pool/loans.mjs +46 -0
  12. package/companion/pool/quota.mjs +142 -0
  13. package/companion/pool/results.mjs +41 -0
  14. package/companion/pool/select.mjs +115 -0
  15. package/companion/pool/state.mjs +104 -0
  16. package/companion/pool/token.mjs +25 -0
  17. package/companion/state-lock.mjs +103 -0
  18. package/companion/stream-worker.mjs +361 -0
  19. package/docs/INSTALL_FOR_AGENTS.md +213 -0
  20. package/docs/REFERENCE.md +509 -0
  21. package/docs/REFERENCE.zh-CN.md +619 -0
  22. package/opencode-skills/agy-crew-accounts/SKILL.md +85 -0
  23. package/opencode-skills/agy-crew-ask/SKILL.md +46 -0
  24. package/opencode-skills/agy-crew-implementer/SKILL.md +56 -0
  25. package/opencode-skills/agy-crew-jobs/SKILL.md +99 -0
  26. package/opencode-skills/agy-crew-jobs/references/setup.md +41 -0
  27. package/opencode-skills/agy-crew-jobs/references/troubleshooting.md +35 -0
  28. package/opencode-skills/agy-crew-lead/SKILL.md +39 -0
  29. package/opencode-skills/agy-crew-researcher/SKILL.md +52 -0
  30. package/opencode-skills/agy-crew-reviewer/SKILL.md +62 -0
  31. package/opencode-skills/agy-crew-reviewer/references/code-review.md +46 -0
  32. package/opencode-skills/agy-crew-reviewer/references/general-review.md +22 -0
  33. package/opencode-skills/agy-crew-staffer/SKILL.md +54 -0
  34. package/opencode.mjs +14 -0
  35. package/package.json +53 -4
  36. package/pi-skills/agy-crew-accounts/SKILL.md +85 -0
  37. package/pi-skills/agy-crew-ask/SKILL.md +46 -0
  38. package/pi-skills/agy-crew-implementer/SKILL.md +56 -0
  39. package/pi-skills/agy-crew-jobs/SKILL.md +99 -0
  40. package/pi-skills/agy-crew-jobs/references/setup.md +41 -0
  41. package/pi-skills/agy-crew-jobs/references/troubleshooting.md +35 -0
  42. package/pi-skills/agy-crew-lead/SKILL.md +39 -0
  43. package/pi-skills/agy-crew-researcher/SKILL.md +52 -0
  44. package/pi-skills/agy-crew-reviewer/SKILL.md +62 -0
  45. package/pi-skills/agy-crew-reviewer/references/code-review.md +46 -0
  46. package/pi-skills/agy-crew-reviewer/references/general-review.md +22 -0
  47. package/pi-skills/agy-crew-staffer/SKILL.md +54 -0
  48. package/scripts/generate-skills.mjs +138 -0
  49. package/skills/accounts/SKILL.md +78 -0
  50. package/skills/ask/SKILL.md +40 -0
  51. package/skills/implementer/SKILL.md +50 -0
  52. package/skills/jobs/SKILL.md +93 -0
  53. package/skills/jobs/references/setup.md +39 -0
  54. package/skills/jobs/references/troubleshooting.md +33 -0
  55. package/skills/lead/SKILL.md +32 -0
  56. package/skills/researcher/SKILL.md +46 -0
  57. package/skills/reviewer/SKILL.md +56 -0
  58. package/skills/reviewer/references/code-review.md +44 -0
  59. package/skills/reviewer/references/general-review.md +20 -0
  60. package/skills/staffer/SKILL.md +48 -0
  61. package/templates/ask.md +5 -0
  62. package/templates/harness-compatibility.md +5 -0
  63. package/templates/implement.md +39 -0
  64. package/templates/research.md +37 -0
  65. package/templates/review.md +27 -0
  66. package/templates/staffer.md +19 -0
@@ -0,0 +1,314 @@
1
+ <p align="center"><img src="assets/logo/gemini-agy.svg" width="440" alt="AGY-CREW"></p>
2
+
3
+ <p align="center"><a href="README.md">English</a> | <a href="README.zh-CN.md">简体中文</a></p>
4
+
5
+ <p align="center"><a href="https://antigravity.google/product/antigravity-cli"><img src="assets/badges/powered-by-antigravity.svg" height="20" alt="powered by: Antigravity"></a> <img src="assets/badges/model-gemini-3-8-flash.svg" height="20" alt="model: Gemini 3.8 Flash"></p>
6
+
7
+ <p align="center"><a href="https://claude.com/claude-code"><img src="assets/badges/claude-code-plugin.svg" height="20" alt="Claude Code plugin"></a> <a href="https://developers.openai.com/codex/"><img src="assets/badges/codex-plugin.svg" height="20" alt="Codex plugin"></a> <a href="LICENSE"><img src="assets/badges/license-mit.svg" height="20" alt="license: MIT"></a></p>
8
+
9
+ # agy-crew
10
+
11
+ 把 Google 的 Antigravity CLI(`agy`)雇来当 **Claude Code**、**OpenAI Codex**、**Pi**、**OpenCode** 等环境的「agy 员工」。agy-crew 是 Keli Wen 的 [agy-staff](https://github.com/keli-wen/agy-staff)(MIT)的 fork,详见[与 agy-staff 的关系](#与-agy-staff-的关系)。
12
+
13
+ ![设计图](assets/design.png)
14
+
15
+ **[安装](#安装) · [使用示例](#使用) · [账号池](#账号池) · [核心设计](#核心设计) · [为其他 CLI 做一个](#为其他-cli-做一个) · [升级与卸载](#升级与卸载) · [与 agy-staff 的关系](#与-agy-staff-的关系)**
16
+
17
+ ## 它适合做什么
18
+
19
+ agy-crew 提供五种角色(persona)。`staffer` 适合通用任务;`researcher` 负责调研;`reviewer` 审查代码、方案和决策;`implementer` 处理编码任务;`ask` 用于不需要工具的简短问答。前四种角色都使用相同的后台任务机制,由 `jobs` 技能负责等待、查看进度和收取结果。执行 `agy-crew install` 之后,所有 harness 都以 `/agy-crew-<persona>` 调用这些角色;通过 harness 的插件方式安装时,Claude Code 使用 `/agy-crew:<persona>`,Codex 使用 `$agy-crew:<persona>`。
20
+
21
+ 为什么需要它:GPT-5.6-Sol 开着 fast mode 也慢;Claude Code 快一些,但 Fable 额度有限,更适合用来编排 subagent,而不是亲自做每一次调研和审查。这些任务可以交给 agy:它几秒钟就能给出第二意见,调研和审查以 Flash 的速度完成,范围明确的实现任务放到后台执行,你继续做手头的事。另外,即使不追求速度,让另一个模型家族审同一份代码,也能发现主力 agent 自己发现不了的问题。
22
+
23
+ ![主 agent 将部分工作交给后台运行的 agy](assets/why.png)
24
+
25
+ ### 为什么要 fork
26
+
27
+ agy-crew 保留了 agy-staff 的角色、后台任务,以及“bash 加 skills”的可移植设计,并增加了:
28
+
29
+ 1. 终端命令 `agy-crew` 和 `install` 命令:从同一份共享 bundle 把技能装进检测到的每个 harness,取代各 harness 自己的插件缓存;
30
+ 2. 与上游 agy-staff 严格互斥(安装器发现它时拒绝运行);
31
+ 3. **账号池**:让并发任务跑在不同的 agy 账号上,并按账号、按模型家族感知配额(见[账号池](#账号池))。
32
+
33
+ `-crew` 后缀与作者的 `codex-staff` 对应:`-staff` 是单账号委派,`-crew` 是账号池。
34
+
35
+ ## 安装
36
+
37
+ 先安装 Antigravity CLI。下面的命令来自[官方安装文档](https://antigravity.google/docs/cli/install),安装后用 `agy --version` 确认命令可用。运行插件还需要 Node.js 20 或更高版本。
38
+
39
+ ```bash
40
+ curl -fsSL https://antigravity.google/cli/install.sh | bash
41
+ ```
42
+
43
+ 接着安装 agy-crew。**agy-crew 与 agy-staff 不能安装在同一台机器上**(见[与 agy-staff 的关系](#与-agy-staff-的关系)),安装器发现 agy-staff 时会拒绝运行。
44
+
45
+ 先选一条路线。
46
+
47
+ | 路线 | 需要 | 得到什么 | 适合 |
48
+ |---|---|---|---|
49
+ | A:npm(推荐) | Node.js 20+、npm | PATH 上的 `agy-crew` 命令 | 想要一条命令装好 |
50
+ | A′:git clone + node | Node.js 20+、git(不需要 npm) | 只有技能;命令写作 `node <clone>/companion/agy-companion.mjs …` | 没有 npm 或不想用 npm |
51
+ | B:插件管理器 | harness 自带的 CLI | harness 内的技能,没有终端命令 | 希望由 harness 管理插件 |
52
+
53
+ ### 路线 A(推荐):npm,一条命令
54
+
55
+ ```bash
56
+ npm install -g agy-crew
57
+ agy-crew install
58
+ ```
59
+
60
+ 想试用 `master` 上尚未发布的改动,可以改用 `npm install -g github:pStrikeZ/agy-crew`(需要 git)。开发时可以从检出目录安装:`git clone https://github.com/pStrikeZ/agy-crew && cd agy-crew && npm link`,再运行 `agy-crew install`。此时技能直接从检出目录运行,`git pull` 后立即生效;拉取之后请重新运行 `agy-crew install` 以刷新 bundle。
61
+
62
+ ### 路线 A′:不用 npm,git clone + node
63
+
64
+ ```bash
65
+ git clone https://github.com/pStrikeZ/agy-crew
66
+ node agy-crew/companion/agy-companion.mjs install
67
+ ```
68
+
69
+ 技能的行为完全一样:bundle 中记录的是 clone 内 companion 的绝对路径。PATH 上没有 `agy-crew` 命令,因此本文中所有以 `agy-crew` 开头的命令,都要改写成 `node <clone>/companion/agy-companion.mjs <命令>`,例如 `node <clone>/companion/agy-companion.mjs accounts login work1`。clone 必须留在原处(移动或删除会让技能失效),每次 `git pull` 之后都要重新运行安装命令。嫌麻烦的话可以设一个 shell 别名,例如 `alias agy-crew='node ~/agy-crew/companion/agy-companion.mjs'`。
70
+
71
+ ### `install` 做了什么(路线 A 和 A′)
72
+
73
+ `agy-crew install` 会根据配置目录自动检测已有的 harness。参数:`--hosts claude,codex,omp,opencode,dsh`(显式列表)、`--all`、`--dry-run`(只打印计划,不写入)、`--status`(显示各 host 的链接状态和 bundle)、`--json`。`agy-crew uninstall [--hosts …|--all]` 只移除安装器自己创建的链接。
74
+
75
+ | Host | 技能目录 |
76
+ |---|---|
77
+ | Claude Code(`claude`) | `~/.claude/skills` |
78
+ | Codex(`codex`) | `~/.agents/skills` |
79
+ | Oh My Pi(`omp`) | `~/.omp/agent/skills` |
80
+ | OpenCode(`opencode`) | `~/.config/opencode/skills` |
81
+ | DeepSeek Harness(`dsh`) | `~/.dsh/skills` |
82
+
83
+ 技能只渲染一次,写入 `~/.agy-crew/bundles/<version>-<digest>/`,其中 companion 路径为绝对路径;每个 harness 得到指向该 bundle 的目录链接(`agy-crew-<persona>`,Windows 上是 junction,不需要提权)。技能目录中任何不是安装器创建的内容都视为用户内容,绝不覆盖;出现任何冲突时,整次运行会在写入之前中止,并打印相关路径和处理办法。`install` 不覆盖 Pi,请使用下面的 Pi 软件包方式。
84
+
85
+ 安装后重启 harness 或开启新会话,然后按[首次使用](#首次使用)继续。
86
+
87
+ ### 路线 B:harness 的插件管理器
88
+
89
+ 插件管理器不需要终端命令,但账号池命令在这条路线下比较别扭:companion 位于插件缓存目录里,只能用缓存路径去调用(可以改在 agent 里用 `/agy-crew-accounts`,想用账号池也可以直接选路线 A 或 A′)。
90
+
91
+ <details>
92
+ <summary>Claude Code</summary>
93
+
94
+ ```bash
95
+ claude plugin marketplace add pStrikeZ/agy-crew
96
+ claude plugin install agy-crew@agy-crew
97
+ ```
98
+
99
+ 重启 Claude Code 后运行 `/agy-crew:ask reply with OK`。
100
+
101
+ </details>
102
+
103
+ <details>
104
+ <summary>Codex</summary>
105
+
106
+ ```bash
107
+ codex plugin marketplace add https://github.com/pStrikeZ/agy-crew
108
+ codex plugin add agy-crew@agy-crew
109
+ ```
110
+
111
+ 重启 Codex 后运行 `$agy-crew:ask reply with OK`。Codex 的命令沙箱无法运行 agy,companion 命令必须在沙箱之外运行(见 [INSTALL_FOR_AGENTS.md](docs/INSTALL_FOR_AGENTS.md))。
112
+
113
+ </details>
114
+
115
+ <details>
116
+ <summary>Pi</summary>
117
+
118
+ 运行 `pi install git:github.com/pStrikeZ/agy-crew` 安装插件。Pi 中的技能使用 `agy-crew-` 前缀,例如 `/skill:agy-crew-ask reply with OK`;任务管理技能是 `/skill:agy-crew-jobs`。
119
+
120
+ 更新时运行 `pi update --extension git:github.com/pStrikeZ/agy-crew`,然后在 Pi 中执行 `/reload`。
121
+
122
+ </details>
123
+
124
+ <details>
125
+ <summary>OpenCode</summary>
126
+
127
+ 使用 OpenCode V1(≥1.18.34 <2)的原生插件管理命令安装:
128
+
129
+ ```bash
130
+ opencode plugin 'agy-crew@git+https://github.com/pStrikeZ/agy-crew.git' --global
131
+ ```
132
+
133
+ 重启 OpenCode 后运行 `/agy-crew-ask reply with OK`。插件会自动注册八个 `agy-crew-*` 技能及其原生斜杠命令,包括 `/agy-crew-lead`、`/agy-crew-jobs` 和 `/agy-crew-accounts`,无需单独配置技能路径或命令包装。仍需安装 Node.js,并完成 `agy` 登录。
134
+
135
+ OpenCode 按完整包规格缓存;仅重启不会刷新相同的 Git 规格。升级时选择新的 Git 标签或提交,运行 `opencode plugin 'agy-crew@git+https://github.com/pStrikeZ/agy-crew.git#<tag-or-commit>' --global --force`,然后重启。配置与本地开发方式见[参考手册](docs/REFERENCE.zh-CN.md#opencode)。此适配器不覆盖 OpenCode V2。
136
+
137
+ </details>
138
+
139
+ ### 首次使用
140
+
141
+ 装好之后,按下面的顺序走一遍即可。
142
+
143
+ 1. **冒烟测试。** 重启 harness(或开启新会话),运行 `/agy-crew-ask reply with OK`(Codex 中是 `$agy-crew-ask reply with OK`;插件路线请用路线 B 中的写法)。应该会返回一句简短的回答。`ask` 不调用工具,也不需要额外的权限配置。如果失败,先运行 `agy --version`,并手动启动一次 `agy` 完成登录。
144
+ 2. **可选:账号池。** 不添加账号时,一切都跑在你自己的 agy 登录上;一个账号够用就可以跳过这一步。要添加账号,请在你自己的终端里运行(会打开浏览器登录,agent 无法代劳):
145
+
146
+ ```bash
147
+ agy-crew accounts login work1 # 在浏览器里登录;名字随意
148
+ agy-crew accounts login work2 # 每多一个账号重复一次(每次用不同的 Google 账号)
149
+ agy-crew accounts list # 每个账号都应显示 ready
150
+ agy-crew accounts quota --refresh # 查看各账号当前配额
151
+ ```
152
+
153
+ 路线 A′ 请把 `agy-crew` 换成 `node <clone>/companion/agy-companion.mjs`。详见[账号池](#账号池)。
154
+ 3. **日常使用。** 直接调用角色:`/agy-crew-researcher`、`/agy-crew-reviewer`、`/agy-crew-implementer`、`/agy-crew-staffer`、`/agy-crew-lead`(Codex 中是 `$agy-crew-…`)。后台任务的结果由 agent 通过 jobs 技能替你收取,不需要自己运行 `wait`。`/agy-crew-accounts` 用来查看账号池(账号、配额、冷却状态)。示例见[使用](#使用)。
155
+
156
+ > [!IMPORTANT]
157
+ > `staffer`、`researcher`、`reviewer` 和 `implementer` 默认使用 `unrestricted` 权限配置,可以读取仓库、运行命令和修改文件。插件会通过提示词说明任务边界和已有改动的归属;任务明确要求提交、推送或创建 PR 时,agy 才应执行相应操作,否则留下工作区改动供你审查。这些提示约定不能替代权限隔离。
158
+ >
159
+ > 日常使用不需要先运行 `setup`。如果希望保留 agy 自身的权限检查,可以选择 `--restricted`,并通过 `setup` 配置允许执行的命令。`setup` 默认只展示计划,经你确认后才写入配置(它会修改全局的 `~/.gemini/antigravity-cli/settings.json`)。按仓库保存的策略文件是 `.agy-crew/config.json`。使用前请阅读[权限说明](docs/REFERENCE.zh-CN.md#可选加固-setup),了解全局命令允许列表的作用范围,以及审查不可信内容时的限制。
160
+
161
+ ### 让 agent 帮你安装
162
+
163
+ 也可以把下面这段话交给你的 coding agent,让它按照仓库里的说明完成安装和验证:
164
+
165
+ ```
166
+ Read the raw text of https://raw.githubusercontent.com/pStrikeZ/agy-crew/master/docs/INSTALL_FOR_AGENTS.md
167
+ (curl it — do not work from a summary), or the same file in your local checkout of agy-crew, and follow it to
168
+ install and verify the agy-crew plugin for the harness you are running in. Respond in the user's language.
169
+ ```
170
+
171
+ ## 使用
172
+
173
+ 在 harness 中输入 `/agy-crew-`(Codex 中为 `$agy-crew-`),就能选择要使用的角色;通过 Claude Code 的插件方式安装时,写法是 `/agy-crew:staffer`。
174
+
175
+ 下面的示例使用用户级写法 `/agy-crew-…`(Codex 中显示为 `$agy-crew-…`)。通过插件方式安装时,Claude Code 使用 `/agy-crew:…`,Codex 使用 `$agy-crew:…`;Pi 使用 `/skill:agy-crew-…`。
176
+
177
+ | 想做的事 | 示例 |
178
+ | --- | --- |
179
+ | 编排持续推进的任务 | `/agy-crew-lead 调研可选方案,起草提案,再根据我的反馈修订` |
180
+ | 问一个简短的问题 | `/agy-crew-ask 你的后端模型是什么` |
181
+ | 交办一个通用任务 | `/agy-crew-staffer 汇总这个仓库里所有未完成的 TODO` |
182
+ | 生成图片 | `/agy-crew-staffer 生成一个像素风机器人吉祥物,存为 assets/mascot.png` |
183
+ | 审查当前改动 | `/agy-crew-reviewer 检查当前工作区的改动` |
184
+ | 审查某个 PR | `/agy-crew-reviewer 审查 PR #730` |
185
+ | 审查方案 | `/agy-crew-reviewer 检查 docs/plan.md 中的迁移方案,指出可能遗漏的问题` |
186
+ | 调研代码 | `/agy-crew-researcher 这个仓库的鉴权是怎么做的` |
187
+ | 实现修复 | `/agy-crew-implementer 修复那个不稳定的重试测试` |
188
+ | 查看或继续任务 | 直接说“agy 的任务进展如何”或“继续刚才的任务,再检查一下错误路径” |
189
+
190
+ 使用 `reviewer` 时,直接说明审查对象即可。agy 会自行调用 `gh pr view`、`git diff` 等命令收集材料,也可以读取指定文件。代码审查会按严重程度列出问题;方案和决策审查则会检查假设、取舍和可能遗漏的情况。
191
+
192
+ `staffer` 没有预设的专业分工,因此也适合调用其他角色没有专门介绍的 agy 原生工具,例如 `generate_image`。项目曾在 agy v1.1.15 上验证图像生成:一次调用约 30 秒生成了 1024×1024 的 PNG。这个记录可以作为使用示例,实际耗时取决于任务和运行环境。
193
+
194
+ ## 账号池
195
+
196
+ 一个 agy 登录只有一份配额:并发任务只能排队,常用模型也容易被用完。账号池让后台任务同时跑在多个 agy 账号上,并按账号、按模型家族记录配额。
197
+
198
+ 快速开始:
199
+
200
+ ```bash
201
+ agy-crew install # 如上
202
+ agy-crew accounts login work1 # 会打开浏览器登录;请在你自己的终端里运行
203
+ agy-crew accounts list # work1 显示为 ready
204
+ ```
205
+
206
+ 之后每次运行都会自动选择账号(默认就是 `--account auto`):优先选空闲账号,其次选已知剩余配额最多的。想要更多账号,换个名字再执行一次 `login` 即可。你自己的 agy 登录(**live**)永远不属于账号池:`auto` 不会用它,它也不会借出凭据。只有显式传入 `--account live`,或账号池里没有任何启用的账号时才会用到它,所以账号池为空时一切照旧。
207
+
208
+ 账号触及配额上限时,companion 会为该账号和模型家族记录冷却,直到 agy 报告的重置时间,没有报告则为 60 分钟。尚未开始会话的任务会转到另一个账号;已经在会话中的任务仍绑定原账号,因为会话历史保存在那里。如果该账号正在冷却,就由一个空闲账号在本次运行中出借凭据,结束后令牌文件会还原,崩溃后也会恢复。所有账号都无法继续时,任务以 `quota_exhausted` 结束并给出 `retry_at`,由你决定何时 `continue`。`agy-crew accounts quota --refresh` 会向 Google 的 Cloud Code 服务查询当前配额,这是唯一会联网的命令。
209
+
210
+ 已经有账号了?`agy-crew accounts import work2 --home <dir>` 会从现有的 HOME 形态目录复制令牌,例如 agy-cli-manager 的账号目录(`~/.agy-cli-manager/accounts/<name>`)。agy-crew 并不依赖 agy-cli-manager,导入只是复制文件,再做与登录相同的邮箱和重复检查。其他子命令:`enable`、`disable`、`cooldown clear`、`remove --yes`。完整说明见[参考手册](docs/REFERENCE.zh-CN.md#账号池)。
211
+
212
+ 现在,并行任务的数量大致以就绪账号数为宜。账号池为空时,所有任务都跑在你的 live 登录上。
213
+
214
+ ## 核心设计
215
+
216
+ `lead` 为当前主 agent 增加任务编排指导。在 lead 工作流中,主 agent 了解至足以明确任务后,默认用 `staffer` 承担实质性工作,等待结果返回后再验收、整合或追加任务;专门指导有帮助时再选择 specialist,`ask` 仅用于测试。主 agent 负责跨任务决策、验收、整合和交付,复用现有 jobs 工作流。使用 `/agy-crew-lead`(Claude Code 插件方式为 `/agy-crew:lead`,Codex 插件方式为 `$agy-crew:lead`,Pi 为 `/skill:agy-crew-lead`)。
217
+
218
+ `ask` 会在同一次调用中返回答案。其他角色启动后会先返回任务 ID,并给出收取结果的命令,例如 `wait <id> --timeout 10m`。主 agent 根据所在环境的能力等待任务;如果支持后台命令,就为每个任务保留一个独立的等待命令。
219
+
220
+ 主 agent 默认等待最终结果,不为例行汇报主动查询。你明确询问中间进展时,它才用 `observe` 查看当前快照,其中包含最近的工具活动和回答片段。任务完成后,`wait` 或 `result` 负责返回完整结果。
221
+
222
+ 下图按时间顺序展示一次后台任务:主 agent 默认等待最终结果(或推进已明确的独立工作),仅在用户询问时查看进度,最后收取报告。
223
+
224
+ [![后台任务从委派到完成的过程:主 agent 等待或查看进度时,worker 持续保存 AGY 的输出,最终交付完整报告](assets/integration.png)](assets/integration.svg)
225
+
226
+ 等待到期不会停止后台任务。任务本身有独立的执行时限,默认 60 分钟,可以在启动时用 `--timeout` 调整,最长 120 分钟。需要停止时使用 `cancel`;需要继续或重新开始时,由主 agent 根据你的要求调用 `continue` 或 `restart`。模型何时收到后台结果,仍由你使用的 agent 环境决定。
227
+
228
+ 关于参数、权限、进度快照和恢复方式,可以查阅[完整参考手册](docs/REFERENCE.zh-CN.md)。各版本的改动记录在[发布说明](docs/releases/)中。
229
+
230
+ ## 为其他 CLI 做一个
231
+
232
+ agy-crew 从 agy-staff 继承的设计是可移植(portable)的:只用 bash 和 skills,不强依赖特定的宿主、worker CLI 或运行时库。[docs/DESIGN.md](docs/DESIGN.md)(英文)把这套设计整理成了最佳实践。如果想为其他 agent CLI 做同类工具,可以把下面这段话交给你的 coding agent:
233
+
234
+ ```
235
+ Read the raw text of https://raw.githubusercontent.com/pStrikeZ/agy-crew/master/docs/DESIGN.md
236
+ (curl it — do not work from a summary). Then design an integration that lets this host agent delegate
237
+ work to <CLI> the same way. First read `<CLI> --help`, map what the CLI supports to the section
238
+ "What depends on the worker CLI", and list the principles it cannot support before writing any code.
239
+ Ask me before any call that spends model quota. Respond in the user's language.
240
+ ```
241
+
242
+ agent 动手写代码之前,会先列出你的 CLI 支持不了哪些原则,让你提前知道工具会在哪些地方降级。
243
+
244
+ ## 升级与卸载
245
+
246
+ 路线 A(npm):
247
+
248
+ ```bash
249
+ # 升级
250
+ npm install -g agy-crew@latest && agy-crew install # 之后重启 harness
251
+ # 卸载
252
+ agy-crew uninstall --all && npm uninstall -g agy-crew
253
+ ```
254
+
255
+ 如果是 `npm link` 的检出目录,运行 `git pull`,再运行 `agy-crew install` 刷新 bundle。
256
+
257
+ 路线 A′(git clone):
258
+
259
+ ```bash
260
+ # 升级
261
+ git -C <clone> pull && node <clone>/companion/agy-companion.mjs install # 之后重启 harness
262
+ # 卸载,然后删除 clone 目录
263
+ node <clone>/companion/agy-companion.mjs uninstall --all
264
+ ```
265
+
266
+ `~/.agy-crew` 里保存着账号池的账号和 bundle,两种卸载都不会删除它;只有想彻底清掉账号时才手动删除。
267
+
268
+ 路线 B:Claude Code 和 Codex 使用的是安装时复制到缓存中的插件。仓库发布新版本后,需要主动更新,并重启应用才能加载新的技能。
269
+
270
+ Claude Code 的更新命令是:
271
+
272
+ ```bash
273
+ claude plugin marketplace update agy-crew && claude plugin update agy-crew@agy-crew
274
+ ```
275
+
276
+ Codex 的更新命令是:
277
+
278
+ ```bash
279
+ codex plugin marketplace upgrade && codex plugin add agy-crew@agy-crew
280
+ ```
281
+
282
+ 这两个环境都按版本号管理插件缓存。如果更新后仍然看到旧行为,请先确认是否已重启应用,再参考[升级说明](docs/REFERENCE.zh-CN.md#升级)检查版本和实际安装的提交。Pi 和 OpenCode 的更新方式见上方安装说明。卸载路线 B 的安装,请使用 harness 自带的插件管理器(Claude Code:`claude plugin uninstall agy-crew@agy-crew`)。
283
+
284
+ ## 与 agy-staff 的关系
285
+
286
+ agy-crew 是 Keli Wen 的 [agy-staff](https://github.com/keli-wen/agy-staff) v0.7.4 的 fork,采用相同的 MIT 许可证。版本号延续 agy-staff 的历史(本 fork 从 0.8.0 开始)。
287
+
288
+ 改动内容:全面重命名(插件 id `agy-crew@agy-crew`,仓库内状态目录 `.agy-crew/`,运行信息前缀 `[agy-crew]`,错误前缀 `agy-crew error:`);新增终端命令和跨 harness 的 `install`(0.8.0),以及账号池(0.9.0)。上游的改动在适用时会继续合并进来。
289
+
290
+ agy-crew 与 agy-staff 不能共存:两者的技能都会出现在 agent 面前,并写入同一份仓库状态,因此发现 agy-staff 时,`agy-crew install` 会拒绝运行并说明如何移除。没有原地升级:先卸载 agy-staff,再安装 agy-crew;`.agy-staff/` 中的状态不会迁移。想换回上游,请先运行 `agy-crew uninstall --all`。详见 [LICENSE](LICENSE)、[NOTICE](NOTICE) 、[0.8.0](docs/releases/v0.8.0.md) 和 [0.9.0](docs/releases/v0.9.0.md) 发布说明。
291
+
292
+ ## 参与贡献
293
+
294
+ 欢迎通过 issue 或 PR 提出问题、分享使用经验或改进代码。新增模式或参数会影响对外接口,建议先开 issue 讨论预期行为。
295
+
296
+ 提交代码前请运行 `npm test`。标准测试使用临时仓库、临时 HOME 和假的 agy,不会调用真实模型或改动你的个人配置。新增回归测试也应保持这一点。需要验证真实 AGY 时,请使用[测试说明](tests/README.md)中单独启用的集成测试。
297
+
298
+ 运行逻辑位于 `companion/`:入口负责模式和任务命令,独立模块负责流式执行、进度快照、状态锁、产品标识(`identity.mjs`)、跨 harness 安装器(`install.mjs`)和账号池(`accounts.mjs`、`pool/`)。角色技能位于 `skills/`,共享的提示词模板位于 `templates/`。修改技能时请以 `skills/` 为准,再运行 `npm run generate:skills` 生成 `pi-skills/` 与 `opencode-skills/`,用 `npm run check:skills` 检查一致性。不要直接编辑生成文件;原有 `generate:pi` 和 `check:pi` 命令仍然可用。打包时使用 `npm run pack:checked`,先检查一致性再生成归档;发布前也会自动检查。
299
+
300
+ README 和参考手册都有中英文版本。修改使用方法或行为说明时,请同步更新对应版本,让两种语言的读者得到一致的信息。
301
+
302
+ ### 发版
303
+
304
+ 版本由 [Release 工作流](.github/workflows/release.yml) 通过 npm 的 Trusted Publishing 发布:由 GitHub 的 OIDC 身份证明构建来源,任何地方都不保存 npm token,每个版本都带来源证明(provenance)。
305
+
306
+ 1. 修改 `package.json`、`.claude-plugin/plugin.json` 和 `.codex-plugin/plugin.json` 里的版本号,并新增 `docs/releases/vX.Y.Z.md`。
307
+ 2. 提交后打 tag 并推送:`git tag vX.Y.Z && git push origin master vX.Y.Z`。
308
+ 3. 工作流会检查 tag 与各处版本号一致、发布说明存在,运行 `check:skills` 和测试,然后发布到 npm(带 `-` 后缀的预发布版本进入 `next` 标签),再用发布说明创建 GitHub Release。已在 npm 上的版本会跳过发布,因此重跑是安全的;也可以用 `workflow_dispatch` 对已有 tag 重跑。
309
+
310
+ fork 或新维护者的一次性准备:在仓库里启用 Actions;手动发布第一个版本(`npm publish`,npm 只允许给已存在的包配置受信任发布者;npm 可能先把这次上传暂存起来,只显示一个 `0.0.0-stage` 占位版本,需要在 npmjs.com 上批准后才正式发布);然后在 npmjs.com 的包设置里添加 GitHub Actions 类型的受信任发布者,填写本仓库和工作流文件 `release.yml`,并在发布权限里选择要求双重验证、禁止使用 token。
311
+
312
+ ## 许可证
313
+
314
+ 本项目采用 MIT 许可证,详见 [LICENSE](LICENSE) 和 [NOTICE](NOTICE)。原始版权归 Keli (pkuwkl),fork 的改动由 pStrikeZ 完成。
@@ -0,0 +1,300 @@
1
+ /**
2
+ * `accounts` subcommands: manage the pool of private agy logins.
3
+ *
4
+ * Pool state lives in <globalHome()>/pool.json, never in the repository, so
5
+ * nothing here calls the companion's ensureStateDir. Failures are thrown as
6
+ * PoolError; main() prints them and exits 1.
7
+ */
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { randomUUID } from 'node:crypto';
13
+ import { PRODUCT } from './identity.mjs';
14
+ import { accountEnv } from './pool/env.mjs';
15
+ import { accountDir, accountHome, accountSettingsPath, loadPool, poolExists, poolPaths, tokenPath,
16
+ updatePool, validateAccountName, PoolError } from './pool/state.mjs';
17
+ import { accountInLoan } from './pool/loans.mjs';
18
+ import { readToken } from './pool/token.mjs';
19
+ import { refreshQuotas, summarizeSnapshot } from './pool/quota.mjs';
20
+ import { replaceFile } from './state-lock.mjs';
21
+
22
+ const out = (text) => process.stdout.write(text + '\n');
23
+ const printJson = (value) => out(JSON.stringify(value, null, 2));
24
+ const FILE_TOKEN_MARKER = 'Using file-based token storage';
25
+
26
+ /** Current pool with dead leases and loans reaped (only when a pool exists). */
27
+ function currentPool() {
28
+ if (!poolExists()) return loadPool();
29
+ return updatePool((pool) => structuredClone(pool));
30
+ }
31
+
32
+ const busy = (pool, name) => pool.leases.some((l) => l.account === name) || accountInLoan(pool, name);
33
+
34
+ function requireAccount(pool, name) {
35
+ if (!name) throw new PoolError('this command needs an account name (see `accounts list`)');
36
+ const account = pool.accounts.find((a) => a.name === name);
37
+ if (!account) throw new PoolError(`unknown account "${name}" (see \`accounts list\`)`);
38
+ return account;
39
+ }
40
+
41
+ function accountState(pool, account, now) {
42
+ if (account.disabled) return 'disabled';
43
+ if (account.auth_error_at || !fs.existsSync(tokenPath(accountHome(account.name)))) return 'needs login';
44
+ const cooling = Object.entries(pool.cooldowns).filter(([key, c]) => key.startsWith(`${account.name}|`) && Date.parse(c.retry_at) > now);
45
+ if (cooling.length) return cooling.map(([key, c]) => `cooling ${key.split('|')[1]} until ${c.retry_at}`).join('; ');
46
+ const n = pool.leases.filter((l) => l.account === account.name).length;
47
+ return n ? `busy ${n}` : 'ready';
48
+ }
49
+
50
+ function cmdList(opts) {
51
+ const pool = currentPool();
52
+ const now = Date.now();
53
+ const rows = pool.accounts.map((a) => ({
54
+ name: a.name, email: a.email, state: accountState(pool, a, now), last_used_at: a.last_used_at || null,
55
+ quota: pool.quota[a.name] || null,
56
+ cooldowns: Object.entries(pool.cooldowns).filter(([k]) => k.startsWith(`${a.name}|`)).map(([k, c]) => ({ family: k.split('|')[1], ...c })),
57
+ }));
58
+ if (opts.json) return printJson({ accounts: rows, live: { description: 'your own agy login; never pooled' } });
59
+ if (!rows.length) out(`No pool accounts. Add one with \`accounts login <name>\`.`);
60
+ for (const r of rows) {
61
+ out(`${r.name} <${r.email || 'unknown'}> ${r.state} last used ${r.last_used_at || 'never'}`);
62
+ out(` ${summarizeSnapshot(r.quota)}`);
63
+ }
64
+ out('live (your own agy login; never pooled)');
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // login / import
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /** Copy only the permission rules from the live settings file, so a pooled run
72
+ * gets the same allow/deny lists without inheriting anything else. */
73
+ function seedSettings(home, liveSettings) {
74
+ const target = accountSettingsPath(home);
75
+ if (fs.existsSync(target)) return;
76
+ try {
77
+ const live = JSON.parse(fs.readFileSync(liveSettings, 'utf8'));
78
+ if (!live.permissions) return;
79
+ fs.mkdirSync(path.dirname(target), { recursive: true });
80
+ fs.writeFileSync(target, JSON.stringify({ permissions: live.permissions }, null, 2) + '\n');
81
+ } catch { /* no usable live settings: the account starts with agy defaults */ }
82
+ }
83
+
84
+ function assertUniqueEmail(pool, email, self = null) {
85
+ if (!email) throw new PoolError('could not read an email from the token; refusing to add an account that cannot be told apart');
86
+ const dup = pool.accounts.find((a) => a.email === email && a.name !== self);
87
+ if (dup) throw new PoolError(`${email} is already enrolled as account "${dup.name}"`);
88
+ }
89
+
90
+ function newAccountRecord(name, email) {
91
+ return { name, email, added_at: new Date().toISOString(), last_used_at: null, disabled: false, auth_error_at: null, auth_error: null };
92
+ }
93
+
94
+ function interactiveLogin(home, logDir, { agyCommand }) {
95
+ fs.mkdirSync(home, { recursive: true });
96
+ const log = path.join(logDir, `${PRODUCT}-login.log`);
97
+ out(`Signing in to a private agy home: ${home}\n` +
98
+ 'agy opens a browser sign-in. Complete it, then exit agy once its prompt appears (Ctrl+C or /exit).');
99
+ const agy = agyCommand(['--log-file', log]);
100
+ const r = spawnSync(agy.cmd, agy.args, { cwd: home, env: accountEnv(home), stdio: 'inherit', windowsHide: true });
101
+ if (r.error) throw new PoolError(`failed to launch agy: ${r.error.message}`);
102
+ let logText = '';
103
+ try { logText = fs.readFileSync(log, 'utf8'); } catch {}
104
+ if (!logText.includes(FILE_TOKEN_MARKER)) {
105
+ throw new PoolError(`agy did not report file-based token storage (see ${log}); the pool needs file-backed tokens`);
106
+ }
107
+ const token = readToken(home);
108
+ if (!token) throw new PoolError(`login did not produce a token file at ${tokenPath(home)}`);
109
+ return token;
110
+ }
111
+
112
+ function cmdLogin(opts, deps) {
113
+ const name = validateAccountName(opts._[1]);
114
+ const existing = currentPool().accounts.find((a) => a.name === name);
115
+ return existing ? reLogin(existing, deps) : firstLogin(name, deps);
116
+ }
117
+
118
+ function firstLogin(name, deps) {
119
+ const staging = path.join(poolPaths().enrollDir, randomUUID().slice(0, 8));
120
+ const home = path.join(staging, 'home');
121
+ try {
122
+ const token = interactiveLogin(home, staging, deps);
123
+ seedSettings(home, deps.liveSettings);
124
+ const dest = accountDir(name);
125
+ updatePool((pool) => {
126
+ if (pool.accounts.some((a) => a.name === name)) throw new PoolError(`account "${name}" already exists`);
127
+ assertUniqueEmail(pool, token.email);
128
+ fs.mkdirSync(dest, { recursive: true });
129
+ fs.renameSync(home, path.join(dest, 'home'));
130
+ pool.accounts.push(newAccountRecord(name, token.email));
131
+ });
132
+ out(`Added account ${name} <${token.email}>.`);
133
+ } finally {
134
+ fs.rmSync(staging, { recursive: true, force: true });
135
+ }
136
+ }
137
+
138
+ function reLogin(account, deps) {
139
+ const home = accountHome(account.name);
140
+ updatePool((pool) => { if (busy(pool, account.name)) throw new PoolError(`account ${account.name} is in use (lease or loan); try again when it is idle`); });
141
+ const token = tokenPath(home);
142
+ const backup = `${token}.agy-crew.login-backup`;
143
+ const hadToken = fs.existsSync(token);
144
+ if (hadToken) replaceFile(token, backup);
145
+ const restore = () => { fs.rmSync(token, { force: true }); if (hadToken) replaceFile(backup, token); };
146
+ try {
147
+ const fresh = interactiveLogin(home, accountDir(account.name), deps);
148
+ if (account.email && fresh.email !== account.email) {
149
+ throw new PoolError(`re-login produced ${fresh.email}, but account ${account.name} is ${account.email}; restored the previous token`);
150
+ }
151
+ updatePool((pool) => {
152
+ const a = requireAccount(pool, account.name);
153
+ assertUniqueEmail(pool, fresh.email, a.name);
154
+ a.email = fresh.email;
155
+ a.auth_error_at = null;
156
+ a.auth_error = null;
157
+ });
158
+ } catch (error) {
159
+ restore();
160
+ throw error;
161
+ }
162
+ fs.rmSync(backup, { force: true });
163
+ out(`Re-login complete for ${account.name}.`);
164
+ }
165
+
166
+ /** Token and settings files of a HOME-shaped directory (e.g. an agy-cli-manager
167
+ * account dir) or a bare `.gemini/antigravity-cli` style tree. */
168
+ function findTokenFile(dir) {
169
+ const candidates = [
170
+ path.join(dir, '.gemini', 'antigravity-cli'), path.join(dir, 'antigravity-cli'), dir,
171
+ ];
172
+ for (const base of candidates) {
173
+ const file = path.join(base, 'antigravity-oauth-token');
174
+ if (fs.existsSync(file)) return { token: file, settings: path.join(base, 'settings.json') };
175
+ }
176
+ return null;
177
+ }
178
+
179
+ function cmdImport(opts) {
180
+ const name = validateAccountName(opts._[1]);
181
+ if (!opts.home) throw new PoolError('accounts import needs --home <dir>');
182
+ const dir = path.resolve(opts.home);
183
+ if (!fs.existsSync(dir)) throw new PoolError(`--home directory does not exist: ${dir}`);
184
+ const real = fs.realpathSync.native(dir);
185
+ if (real === fs.realpathSync.native(os.homedir())) throw new PoolError('refusing to import the live home: your own agy login is never pooled');
186
+ const found = findTokenFile(dir);
187
+ if (!found) throw new PoolError(`no antigravity-oauth-token found under ${dir}`);
188
+ const home = accountHome(name);
189
+ const staged = readTokenFrom(found.token);
190
+ updatePool((pool) => {
191
+ if (pool.accounts.some((a) => a.name === name)) throw new PoolError(`account "${name}" already exists`);
192
+ assertUniqueEmail(pool, staged.email);
193
+ fs.mkdirSync(path.dirname(tokenPath(home)), { recursive: true });
194
+ fs.copyFileSync(found.token, tokenPath(home));
195
+ if (fs.existsSync(found.settings)) fs.copyFileSync(found.settings, accountSettingsPath(home));
196
+ pool.accounts.push(newAccountRecord(name, staged.email));
197
+ });
198
+ out(`Imported account ${name} <${staged.email}>.`);
199
+ }
200
+
201
+ function readTokenFrom(file) {
202
+ // readToken works on a HOME; mirror the layout in a scratch dir only when the source is not HOME-shaped
203
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), `${PRODUCT}-import-`));
204
+ try {
205
+ fs.mkdirSync(path.dirname(tokenPath(scratch)), { recursive: true });
206
+ fs.copyFileSync(file, tokenPath(scratch));
207
+ const token = readToken(scratch);
208
+ if (!token) throw new PoolError(`cannot read the token file ${file}`);
209
+ return token;
210
+ } finally { fs.rmSync(scratch, { recursive: true, force: true }); }
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // remove / enable / disable / cooldown
215
+ // ---------------------------------------------------------------------------
216
+
217
+ function cmdRemove(opts) {
218
+ const name = opts._[1];
219
+ if (!opts.yes) {
220
+ const pool = currentPool();
221
+ requireAccount(pool, name);
222
+ const bound = Object.values(pool.conversations).filter((c) => c.account === name).length;
223
+ out(`Would delete account ${name}: ${accountDir(name)}, its cooldowns, quota snapshot and ${bound} conversation binding(s).\nRerun with --yes to delete.`);
224
+ return;
225
+ }
226
+ const bound = updatePool((pool) => {
227
+ requireAccount(pool, name);
228
+ if (busy(pool, name)) throw new PoolError(`account ${name} is in use (lease or loan); try again when it is idle`);
229
+ pool.accounts = pool.accounts.filter((a) => a.name !== name);
230
+ for (const key of Object.keys(pool.cooldowns)) if (key.startsWith(`${name}|`)) delete pool.cooldowns[key];
231
+ delete pool.quota[name];
232
+ const gone = Object.entries(pool.conversations).filter(([, c]) => c.account === name).map(([id]) => id);
233
+ for (const id of gone) delete pool.conversations[id];
234
+ fs.rmSync(accountDir(name), { recursive: true, force: true });
235
+ return gone.length;
236
+ });
237
+ out(`Removed account ${name}. ${bound} conversation(s) bound to it can no longer be continued.`);
238
+ }
239
+
240
+ function cmdToggle(opts, disabled) {
241
+ const name = opts._[1];
242
+ updatePool((pool) => { requireAccount(pool, name).disabled = disabled; });
243
+ out(`Account ${name} ${disabled ? 'disabled' : 'enabled'}.`);
244
+ }
245
+
246
+ function cmdCooldown(opts) {
247
+ if (opts._[1] !== 'clear') throw new PoolError('usage: accounts cooldown clear <name> [--family <family>]');
248
+ const name = opts._[2];
249
+ const cleared = updatePool((pool) => {
250
+ requireAccount(pool, name);
251
+ const keys = Object.keys(pool.cooldowns).filter((k) => k.startsWith(`${name}|`) && (!opts.family || k === `${name}|${opts.family}`));
252
+ for (const key of keys) delete pool.cooldowns[key];
253
+ return keys.length;
254
+ });
255
+ out(`Cleared ${cleared} cooldown(s) for ${name}.`);
256
+ }
257
+
258
+ async function cmdQuota(opts, deps) {
259
+ const pool = currentPool();
260
+ const names = opts._.slice(1);
261
+ for (const name of names) requireAccount(pool, name);
262
+ const targets = names.length ? names : pool.accounts.map((a) => a.name);
263
+ if (opts.refresh) await refreshQuotas(targets, deps);
264
+ const latest = currentPool();
265
+ const now = Date.now();
266
+ const rows = targets.map((name) => ({ name, snapshot: latest.quota[name] || null,
267
+ cooldowns: Object.entries(latest.cooldowns).filter(([k, c]) => k.startsWith(`${name}|`) && Date.parse(c.retry_at) > now)
268
+ .map(([k, c]) => ({ family: k.split('|')[1], retry_at: c.retry_at })) }));
269
+ if (opts.json) return printJson({ accounts: rows });
270
+ if (!rows.length) out('No pool accounts. Add one with `accounts login <name>`.');
271
+ for (const r of rows) {
272
+ out(`${r.name}: ${summarizeSnapshot(r.snapshot)}${r.snapshot?.refreshed_at ? ` (as of ${r.snapshot.refreshed_at})` : ''}`);
273
+ for (const c of r.cooldowns) out(` cooling ${c.family} until ${c.retry_at}`);
274
+ }
275
+ }
276
+
277
+ /** settings.json of every pool account, for `setup --apply`. */
278
+ export function poolSettingsFiles() {
279
+ return loadPool().accounts.map((a) => ({ name: a.name, file: accountSettingsPath(accountHome(a.name)) }));
280
+ }
281
+
282
+ /** deps: { agyCommand, liveSettings } injected by the companion (avoids an import cycle). */
283
+ export async function cmdAccounts(opts, deps) {
284
+ const sub = opts._[0];
285
+ switch (sub) {
286
+ case 'list': return cmdList(opts);
287
+ case 'login': return cmdLogin(opts, deps);
288
+ case 'import': return cmdImport(opts);
289
+ case 'remove': return cmdRemove(opts);
290
+ case 'enable': return cmdToggle(opts, false);
291
+ case 'disable': return cmdToggle(opts, true);
292
+ case 'cooldown': return cmdCooldown(opts);
293
+ case 'quota': return cmdQuota(opts, deps);
294
+ default:
295
+ throw new PoolError('usage: accounts <list|login|import|remove|enable|disable|cooldown clear|quota> ...\n' +
296
+ ' accounts list [--json]\n accounts login <name>\n accounts import <name> --home <dir>\n' +
297
+ ' accounts remove <name> [--yes]\n accounts enable|disable <name>\n' +
298
+ ' accounts cooldown clear <name> [--family <f>]\n accounts quota [<name>...] [--refresh] [--json]');
299
+ }
300
+ }