ai_co_discuss 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/discuss-host/references/launch.md +1 -1
- package/README.md +15 -3
- package/docs/runner.md +15 -8
- package/package.json +1 -1
- package/runner/discussion_adapters.mjs +29 -12
- package/runner/discussion_launch.mjs +2 -0
- package/runner/discussion_runner.mjs +10 -3
- package/runner/discussion_setup.mjs +25 -45
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
1. 使用已安装的 `codiscuss`;未安装时用本仓库 `node runner/codiscuss.mjs`。`codiscuss launch settings` 只读本机软件默认配置,不启动服务或 AI。缺少配置时按仓库 `docs/runner.md` 的“两种讨论入口与启动配置”补齐;用户已提供的软件、目录和上限直接沿用,只询问无法确定的缺项。
|
|
8
8
|
2. 从当前软件的实际会话信息取得会话 ID,不自造编号。启动文件中 `entry` 使用 `conversation`,`current_session_id` 使用该实际 ID;主持人的 `participants` 覆盖配置必须对应当前实际 CLI / App 客户端,不能仅根据模型名称选适配器。当前会话继续主持,不另建主持聊天。
|
|
9
|
-
3. 站点始终明确指定。每场讨论选择主持人、专家、目标、用户原文、背景和上限;软件路径来自本机默认配置,本次可用 `participants` 覆盖。`cwd` / `discussion_project` 可省略,单个参与者可覆盖。已有会话仍在原目录,设置 cwd 不会迁移它。用户要一起查看某个项目时,把该项目目录设为 `cwd`;项目文件默认要求只读,仅当用户明确要求 AI 修改项目时才设 `"project_access": "write"`,也可在 `participants.NAME.project_access` 单独指定。只读不通过更换项目目录实现;agy
|
|
9
|
+
3. 站点始终明确指定。每场讨论选择主持人、专家、目标、用户原文、背景和上限;软件路径来自本机默认配置,本次可用 `participants` 覆盖。`cwd` / `discussion_project` 可省略,单个参与者可覆盖。已有会话仍在原目录,设置 cwd 不会迁移它。用户要一起查看某个项目时,把该项目目录设为 `cwd`;项目文件默认要求只读,仅当用户明确要求 AI 修改项目时才设 `"project_access": "write"`,也可在 `participants.NAME.project_access` 单独指定。只读不通过更换项目目录实现;agy 只在讨论服务新建的会话中运行并由原生项目权限限制,不能绑定已有会话或从当前 agy 对话主持全自动讨论。答案草稿只使用任务给出的 `draft_directory`,不写到项目或存放凭据的运行目录。
|
|
10
10
|
|
|
11
11
|
新讨论的 UTF-8 启动文件示例(替换示例内容;软件已在本机配置):
|
|
12
12
|
|
package/README.md
CHANGED
|
@@ -29,9 +29,21 @@ npx --yes --package ai_co_discuss codiscuss install
|
|
|
29
29
|
|
|
30
30
|
安装摘要为“配置待处理”时,处理列出的路径、旧配置或技能冲突后重跑 `codiscuss setup`;就绪后直接进入[发起第一场讨论](#发起第一场讨论)。已有配置和自定义技能会保留,数据存放在包外,升级或卸载包不会删除。安装就绪不代表真实 AI 已执行过任务。详细选项见 [npm 安装与本机配置](docs/runner.md#npm-安装与本机配置)。
|
|
31
31
|
|
|
32
|
+
#### 升级与卸载
|
|
33
|
+
|
|
34
|
+
已安装过时不要再用 `npx` 升级:npx 会直接运行已安装的旧版本。先停止本机网站,再强制从线上取最新版并重新配置:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
codiscuss local stop
|
|
38
|
+
npm install --global --prefer-online ai_co_discuss@latest
|
|
39
|
+
codiscuss setup
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`--prefer-online` 避免 npm 用本地缓存把 `latest` 解析成旧版本;完成后可用 `npm ls -g ai_co_discuss` 核对版本。卸载时先 `codiscuss local stop`,开启过登录自启的再运行 `codiscuss service autostart off`,最后 `npm uninstall --global ai_co_discuss`。讨论数据、软件配置和已复制的技能不会被删除,位置见 [卸载说明](docs/runner.md#npm-安装与本机配置)。
|
|
43
|
+
|
|
32
44
|
### 源码运行方式
|
|
33
45
|
|
|
34
|
-
|
|
46
|
+
供开发者使用;已完成包安装可直接跳到下一节。需要 Git、Python 3.10+,使用自动接入还需 Node.js 24+ 和 npm。仓库包含构建好的网页。
|
|
35
47
|
|
|
36
48
|
```sh
|
|
37
49
|
git clone https://github.com/huyan1458/ai_co_discuss.git
|
|
@@ -79,11 +91,11 @@ codiscuss local connect https://你的站点
|
|
|
79
91
|
|
|
80
92
|
> 请使用 discuss-host 和 discuss-participant,在 http://127.0.0.1:8765 新建一场全自动讨论,议题是“议题与目标”,我的问题和背景是“用户原文”。由当前 Codex 会话主持,参与者为 Codex 和 Claude,最多 3 轮、60 分钟,到限暂停;沿用本机软件配置。
|
|
81
93
|
|
|
82
|
-
将地址、议题和参与者替换为实际内容。AI 主持人使用已保存的软件配置完成建房与启动,**当前实际会话继续主持**;后续安排由本机服务交付,可在网页查看同一场讨论。无需用户手写启动 JSON。当前会话的 CLI / App
|
|
94
|
+
将地址、议题和参与者替换为实际内容。AI 主持人使用已保存的软件配置完成建房与启动,**当前实际会话继续主持**;后续安排由本机服务交付,可在网页查看同一场讨论。无需用户手写启动 JSON。当前会话的 CLI / App 类型必须与配置对应;agy 不能从自己的对话主持全自动讨论(只在讨论服务新建的会话中运行),请改用其他软件主持或从网页发起。其他接入方式与配置细节见 [两个入口的启动配置](docs/runner.md#两种讨论入口与启动配置)。
|
|
83
95
|
|
|
84
96
|
#### 项目与日常使用
|
|
85
97
|
|
|
86
|
-
两个入口都可按场指定项目目录、项目文件只读 / 可写及参与者覆盖配置;不需要项目资料时可省略。CLI 保持指定工作目录,回答草稿与运行数据分开。项目默认要求只读,但限制强度取决于软件:Codex 使用原生沙箱,agy
|
|
98
|
+
两个入口都可按场指定项目目录、项目文件只读 / 可写及参与者覆盖配置;不需要项目资料时可省略。CLI 保持指定工作目录,回答草稿与运行数据分开。项目默认要求只读,但限制强度取决于软件:Codex 使用原生沙箱,agy 只在讨论服务新建的会话中运行并由其独立项目权限限制、禁用 MCP(不绑定已有 agy 会话);Claude 的已有用户授权仍生效。权限冲突处理与实测边界见 [单次执行权限](docs/runner.md#单次执行权限)。
|
|
87
99
|
|
|
88
100
|
| 日常操作 | 命令 |
|
|
89
101
|
| --- | --- |
|
package/docs/runner.md
CHANGED
|
@@ -30,7 +30,7 @@ codiscuss local stop
|
|
|
30
30
|
|
|
31
31
|
`install/setup` 在 stderr 输出中文摘要,包括实际软件配置、Skill 文件就绪数、命令和网站地址;交互终端显示分步进度、多选列表与对齐的结果汇总(遵循 `NO_COLOR`),输出被重定向时保持逐行纯文本。结果 JSON 在 stdout 被重定向或加 `--json` 时写入 stdout,交互终端默认不打印。退出码与 JSON `status` 对应:`0` 为 `ready`(网站与讨论服务已就绪)或 `configured`(显式 `--no-start`);`2` 为 `needs_attention`(配置差异未确认、技能冲突或命令不可用);`1` 为失败。网站启动失败时返回 `start_failed` 与已保存的配置结果,修复后重跑 `setup` 即可。环境预检失败发生在写入前,仅输出错误。配置待处理不撤销已完成部分,也不妨碍启动可用的网站;`configured` 布尔值为 false,不能按成功处理。`ai_verified: false` 表示安装器没有执行真实模型,不证明账号、技能发现或多轮讨论已验收。
|
|
32
32
|
|
|
33
|
-
安装器按 [技能配置](skills.md#自动安装完整技能) 复制完整技能。`--skills-home DIR` 可覆盖技能用户根目录;默认当前用户 home。重复安装只更新由安装器管理且未被编辑的版本;冲突保留本地文件,输出实际路径并标记待处理。核对后可移走需替换的旧副本再重跑 `setup
|
|
33
|
+
安装器按 [技能配置](skills.md#自动安装完整技能) 复制完整技能。`--skills-home DIR` 可覆盖技能用户根目录;默认当前用户 home。重复安装只更新由安装器管理且未被编辑的版本;冲突保留本地文件,输出实际路径并标记待处理。核对后可移走需替换的旧副本再重跑 `setup`。安装器不调整登录状态,也不向 agy 全局设置写入权限规则;只清理早期版本写入的规则,见 [单次执行权限](#单次执行权限)。
|
|
34
34
|
|
|
35
35
|
安装实现保持固定软件表和本地文件复制。路径查找参考 [npm/node-which](https://github.com/npm/node-which/blob/main/lib/index.js) 的 PATH 顺序与执行权限检查,只覆盖所需命令和后缀;技能复制参考 [OpenAI 单文件安装脚本](https://github.com/openai/skills/blob/main/skills/.system/skill-installer/scripts/install-skill-from-github.py) 的整目录复制与同名保护思路,保留本项目的内容哈希更新判断。不引入这些项目作为依赖,也不增加在线技能下载、插件注册或后台更新。
|
|
36
36
|
|
|
@@ -38,6 +38,8 @@ codiscuss local stop
|
|
|
38
38
|
|
|
39
39
|
网站数据库为服务目录下 `local/discuss.sqlite3`,配置、登记表和讨论执行数据也位于包外;服务目录位置及 `DISCUSS_SERVICE_HOME` 见下文。源码运行的 `data/discuss.sqlite3` 不自动迁移。网站仍在而讨论服务已停止时,显式 `local open/start` 会要求网站管理进程恢复服务;不会自动恢复已暂停的房间。`local status` 的 `running` 要求网站和讨论服务同时运行,`website_running` 单独表示网站状态。`local stop` 关闭它创建的网站,并停止它启动或恢复且仍由相同 PID 持有的讨论服务;预先运行或后来独立启动的服务会保留。停止服务会暂停其托管中的活动讨论,数据库保留。日志在 `local/website.log`、`local/supervisor.log` 与原 `service.log`。
|
|
40
40
|
|
|
41
|
+
已安装后升级不要使用 `npx`(它会运行已安装的旧版本):先 `codiscuss local stop`,再 `npm install --global --prefer-online ai_co_discuss@latest`,然后重跑 `codiscuss setup`;`--prefer-online` 避免本地缓存把 `latest` 解析为旧版本。
|
|
42
|
+
|
|
41
43
|
卸载先 `codiscuss local stop`,按需要另用 `codiscuss service stop` 停止预先独立运行的服务,再执行 `npm uninstall --global ai_co_discuss`;自定义 prefix 时带回相同 `--prefix`。安装器不删除数据库、私有配置或已复制技能。
|
|
42
44
|
|
|
43
45
|
维护者从源码生成安装包时先完成相关测试与网页构建,再 `npm pack --pack-destination <已存在的输出目录>`。包采用运行文件白名单,不含数据库、凭据、`.dev`、前端源码或 node_modules;新增运行文件需同步清单。`npm run test:install` 在临时 prefix/home 中验证真实 npx 安装、命令、技能和本机启停,不触发真实 AI。Windows 已通过隔离测试;macOS/Linux 启动与默认浏览器仍需实机核验。
|
|
@@ -202,7 +204,7 @@ Windows 的 `executable` 填原生 `.exe`,不填 npm 的 `.cmd` / `.ps1` 包
|
|
|
202
204
|
| --- | --- | --- |
|
|
203
205
|
| `codex_cli` | `codex exec --json`,从 `thread.started` 读取实际 ID;后续 `exec resume ID`,不使用 `--last` | 首次与续接均显式 `--cd` 到用户指定项目;省略 cwd 时使用本场独立草稿目录。允许非 Git 目录;项目与草稿的写入权限由本次 permission profile 区分。项目名仅作元数据,不关联 App 项目分组 |
|
|
204
206
|
| `claude` | 现有 `claude -p --output-format json`;后续 `--resume ID` | 沿用上表中的 Claude 规则 |
|
|
205
|
-
| `antigravity_cli` | `agy -p --output-format stream-json`,从 `init.conversation_id` 读取实际 ID;后续 `--conversation ID`,不使用 `--continue` | 保持指定 cwd;省略 cwd
|
|
207
|
+
| `antigravity_cli` | `agy -p --output-format stream-json`,从 `init.conversation_id` 读取实际 ID;后续 `--conversation ID`,不使用 `--continue` | 保持指定 cwd;省略 cwd 时使用本场独立草稿目录。只在讨论服务新建的会话中执行:每位参与者一个独立原生讨论项目,续接复用该项目;不绑定外部已有会话 |
|
|
206
208
|
|
|
207
209
|
两种新适配器都不需要 `source_thread_id` 或 `ipc_directory`。实际 ID 在输出流到达时写入本机绑定和本次执行凭据,随后 AI 按任务文件自行 `execution enter` 并通过讨论 CLI 保存业务结果;程序不把终端回答直接代发。已有 App 绑定不能通过改适配器直接复用,须先暂停并使用独立 `data_directory` 明确重新接入。此路径不承诺 CLI 会话同步到桌面 App。
|
|
208
210
|
|
|
@@ -210,21 +212,26 @@ Windows 的 `executable` 填原生 `.exe`,不填 npm 的 `.cmd` / `.ps1` 包
|
|
|
210
212
|
|
|
211
213
|
每次执行只开放本场讨论所需的最小权限,不使用全部放行或绕过审批参数。任务文件的 `cli` 为 `codiscuss`,`subcommands` 列出唯一放行的网站子命令:`room`、`message`、`turn`、`state`、`floor`、`execution`、`health`、`schema`;`service`、`launch`、`run`、`api`、`setup`、`install`、`local` 不放行。提示词要求命令以 `codiscuss` 开头,不用 `python discuss.py`、`cd` 或管道。规则不含路径,重装或更换 Python 无需更新;子进程 PATH 中的 `codiscuss` 须指向本次安装,setup 的持久命令检查会核对。
|
|
212
214
|
|
|
213
|
-
项目文件权限 `project_access` 为 `read`(默认)或 `write`:启动文件顶层设本场值,`participants.NAME.project_access` 覆盖单个 AI,空字符串表示沿用本场值。它只随本次启动保存,不能存入本机默认配置;网页新建讨论在“启动”区直接提供项目目录与项目文件选项,单个 AI 的覆盖在“调整启动配置”中。三种 CLI 都在用户指定的 cwd 启动;省略 cwd 时才使用独立草稿目录。agy
|
|
215
|
+
项目文件权限 `project_access` 为 `read`(默认)或 `write`:启动文件顶层设本场值,`participants.NAME.project_access` 覆盖单个 AI,空字符串表示沿用本场值。它只随本次启动保存,不能存入本机默认配置;网页新建讨论在“启动”区直接提供项目目录与项目文件选项,单个 AI 的覆盖在“调整启动配置”中。三种 CLI 都在用户指定的 cwd 启动;省略 cwd 时才使用独立草稿目录。agy 的权限全部由其独立讨论项目执行,见下文。App 适配器的写权限由 App 自身决定,任务文件始终写明本场要求。
|
|
214
216
|
|
|
215
217
|
| 适配器 | 本次追加 | 只读 / 可写差异 |
|
|
216
218
|
| --- | --- | --- |
|
|
217
219
|
| `claude` | `--add-dir` 运行、草稿与技能目录;每个子命令各一条 `Bash(codiscuss room *)` 与 `PowerShell(codiscuss room *)`,仅对独立草稿增加 `Edit(//<草稿目录>/**)`,再追加用户的 `allowed_tools` | 可写时再加 `Edit(//<项目目录>/**)`;不再对运行目录新增 Edit 放行,已有用户规则仍生效;Windows 路径按 `//c/...` 匹配 |
|
|
218
220
|
| `codex_cli` | 每次创建唯一名称的 permission profile,通过 `-c default_permissions=...` 与 `permissions.<名称>=...` 传入,`approval_policy="never"`;运行目录只读,独立草稿目录可写 | 无项目或项目只读时继承 `:read-only`,项目可写时继承 `:workspace`;维持原有广泛读取范围和本次联网能力,未启用域名过滤 |
|
|
219
|
-
| `antigravity_cli` |
|
|
221
|
+
| `antigravity_cli` | 始终使用独立 `--project UUID` 与 `--mode accept-edits`;项目规则允许讨论命令和草稿写入,禁止运行目录、技能目录、`~/.gemini` 写入及 MCP 工具调用;每次交付前检查全局设置 | `read` 为项目目录添加 `write_file(目录)` deny,`write` 改为 allow;运行数据禁写不随项目授权放开 |
|
|
220
222
|
|
|
221
|
-
agy
|
|
223
|
+
agy 只用一套方法控制权限:
|
|
222
224
|
|
|
223
|
-
|
|
225
|
+
1. **只在本工具新建的会话中执行**:每位参与者对应一个独立原生讨论项目,权限只写在该项目里。外部已有会话(包括从当前 agy 对话发起主持、在启动配置中填写 `session_id`)会恢复其原项目,无法套用讨论项目权限,因此启动时即拒绝,交付时也再次拒绝。
|
|
226
|
+
2. **项目内规则**:allow 为受限的讨论命令规则与草稿目录写入(`write` 时加项目目录写入);deny 为项目目录写入(`read` 时)、运行目录、技能目录、`~/.gemini` 写入与 `mcp(*)`。规则按目录前缀递归匹配,不使用 `/**`;Deny > Ask > Allow,因此草稿不得位于受保护目录中,冲突在交付前报告。
|
|
227
|
+
3. **交付前检查全局设置**:CLI 的 `toolPermission` 须未设置或为 `request-review`;CLI 与共享全局 allow 中不得有任何 `command(...)`、`unsandboxed(...)`、`unsandboxed_command(...)` 或 `*` 放行,否则拒绝交付,因为全局放行会让 shell 绕过项目的文件规则。未授权 shell 在非交互模式下被拒绝。
|
|
228
|
+
4. **不改用户全局设置**:本工具不向 agy 全局设置写入任何规则;`setup` 只移除早期版本记录在 `agy-permission.json` 中的全局规则(先备份到服务目录 `agy-settings.backup.json`),其他设置与用户规则不变。
|
|
224
229
|
|
|
225
|
-
|
|
230
|
+
原生工具权限不是操作系统沙箱;工具被拒绝时 agy 仍可能退出码为 0,验收以实际文件和网站结果为准。[原生权限文档](https://antigravity.google/docs/permissions?tab=cli)、[CLI 设置](https://antigravity.google/docs/settings?tab=cli)。
|
|
226
231
|
|
|
227
|
-
|
|
232
|
+
独立讨论项目保存在 `~/.gemini/config/projects/<UUID>.json`,运行目录的 `agy-project-<参与者摘要>.json` 保存项目与实际会话绑定。所选原项目及用户全局配置不被改写;新项目只沿用资源目录,不继承原项目的命令放行。项目名为 `CoDiscuss · 讨论标题 · 参与者`,便于在 Antigravity 项目列表中区分。会话继续使用时保留这两份记录,不将生成的讨论会话手动迁入其他项目;讨论收束或存档后仍保留,以便之后在同一会话中继续讨论。只有网站确认房间已被删除(`房间不存在`)时,接入程序才删除该房间建立的讨论项目与记录,不触碰用户自己的项目。缺失已登记的原生项目或会话绑定不匹配时拒绝交付。[项目权限继承](https://antigravity.google/docs/projects?tab=cli)。
|
|
233
|
+
|
|
234
|
+
Codex 要求 CLI 0.163.0 或更新版本,使用 permission profiles 和本地 `app-server config/read`。配置检查先核对初始化回执中的版本;不支持或无法识别的版本拒绝交付。每次交付前只启动本地配置检查进程,不创建会话、不调用模型;发现已加载的旧 `sandbox_mode` / `sandbox_workspace_write` 或新配置未被采用就拒绝交付,避免旧设置覆盖本次限制。报错后需更新不支持的 CLI 或移除冲突的旧配置;接入程序不自动改写用户全局设置。首次与续接使用同一组权限参数,配置检查进程也随暂停或停止清理。[Codex 权限配置](https://learn.chatgpt.com/docs/permissions)
|
|
228
235
|
|
|
229
236
|
2026-10-18 本机实测(本轮权限调整前):Claude 主持、Codex 与 Gemini 专家完成一场最小讨论。Windows 的 Codex / agy 经 PowerShell 调用时优先执行 npm 生成的 `codiscuss.ps1`;Codex elevated 沙箱内即使当前用户为 RemoteSigned 也拒绝 `.ps1`,Codex 自行改用 `cmd.exe` 调用 `codiscuss.cmd` 后成功。`agy` 非交互模式下被拒绝的工具也可能伴随退出码 0,最终仍以网站业务保存为准。`agy` 显式使用 `--print-timeout 0`,由房间的单次执行时限和整场截止控制停止,避免额外的 CLI 默认时限提前截断。
|
|
230
237
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Actual software entrypoints; never call model service APIs. */
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { createInterface } from 'node:readline';
|
|
@@ -144,14 +144,14 @@ export function agy_rule() {
|
|
|
144
144
|
/** Read optional native settings; malformed settings must not silently disable a permission check. */
|
|
145
145
|
function read_json(path) { return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {}; }
|
|
146
146
|
|
|
147
|
-
/** Native headless review blocks unapproved shell commands;
|
|
147
|
+
/** Native headless review blocks unapproved shell commands; any global command grant could bypass the project rules. */
|
|
148
148
|
function check_agy_shell(user_home) {
|
|
149
149
|
const cli = read_json(join(user_home, '.gemini', 'antigravity-cli', 'settings.json'));
|
|
150
150
|
const shared = read_json(join(user_home, '.gemini', 'config', 'config.json'));
|
|
151
151
|
const grants = [...(cli.permissions?.allow || []), ...(shared.userSettings?.globalPermissionGrants?.allow || [])];
|
|
152
152
|
if ((cli.toolPermission && cli.toolPermission !== 'request-review')
|
|
153
|
-
|| grants.some(rule => rule
|
|
154
|
-
throw new Error('agy
|
|
153
|
+
|| grants.some(rule => rule === '*' || /^(?:command|unsandboxed|unsandboxed_command)\(/.test(rule))) {
|
|
154
|
+
throw new Error('agy 全局设置放行了命令或改了 toolPermission,会绕过讨论项目的权限;请移除全局 command / unsandboxed 放行并保留 request-review(本工具旧版写入的规则可运行 codiscuss setup 清理),未交付讨论任务');
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -183,12 +183,14 @@ function agy_project_grants(config, task, resources, user_home) {
|
|
|
183
183
|
deny: [...new Set(protected_paths.map(write_rule)), 'mcp(*)'] };
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
/**
|
|
186
|
+
/** Run agy only inside a private native project this tool created; external conversations keep their own project, so they are refused. */
|
|
187
187
|
export function prepare_agy_project(config, binding, task, user_home = homedir()) {
|
|
188
188
|
const key = createHash('sha256').update(task.participant).digest('hex');
|
|
189
189
|
const record_path = join(task.directory, `agy-project-${key}.json`);
|
|
190
190
|
const record = read_json(record_path);
|
|
191
|
-
if (binding.session_id && !record.project_id)
|
|
191
|
+
if (binding.session_id && !record.project_id) {
|
|
192
|
+
throw new Error('agy 只在本工具新建的讨论会话中自动执行;该会话不是本工具创建的,无法套用讨论项目权限,未交付讨论任务');
|
|
193
|
+
}
|
|
192
194
|
if (record.session_id && record.session_id !== binding.session_id) throw new Error('agy 项目记录与会话绑定不符,未交付讨论任务');
|
|
193
195
|
check_agy_shell(user_home);
|
|
194
196
|
const root = join(user_home, '.gemini', 'config');
|
|
@@ -200,13 +202,30 @@ export function prepare_agy_project(config, binding, task, user_home = homedir()
|
|
|
200
202
|
const current = read_json(path);
|
|
201
203
|
if (record.project_id && current.id !== project_id) throw new Error('agy 讨论项目已丢失或不匹配,未交付讨论任务');
|
|
202
204
|
mkdirSync(join(root, 'projects'), { recursive: true });
|
|
203
|
-
|
|
205
|
+
const name = ['CoDiscuss', task.room_title, task.participant].filter(Boolean).join(' · ');
|
|
206
|
+
atomic_json(path, { ...current, id: project_id, name,
|
|
204
207
|
projectResources: { resources }, settings: {}, permissionGrants: { permissionGrants: grants, v2Migrated: true } });
|
|
205
208
|
const saved = { project_id, session_id: record.session_id || '' };
|
|
206
209
|
atomic_json(record_path, saved);
|
|
207
210
|
return { project_id, bind: session_id => atomic_json(record_path, { ...saved, session_id }) };
|
|
208
211
|
}
|
|
209
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Delete the agy projects this room created after the room itself is deleted; the user's own projects are never touched.
|
|
215
|
+
* @returns {number} How many project records were cleared.
|
|
216
|
+
*/
|
|
217
|
+
export function remove_agy_projects(directory, user_home = homedir()) {
|
|
218
|
+
if (!existsSync(directory)) return 0;
|
|
219
|
+
const records = readdirSync(directory).filter(name => /^agy-project-[a-f0-9]{64}\.json$/.test(name));
|
|
220
|
+
for (const file of records) {
|
|
221
|
+
const { project_id } = read_json(join(directory, file));
|
|
222
|
+
const path = join(user_home, '.gemini', 'config', 'projects', `${project_id}.json`);
|
|
223
|
+
if (/^[a-f0-9-]{36}$/.test(project_id || '') && read_json(path).id === project_id) rmSync(path);
|
|
224
|
+
rmSync(join(directory, file));
|
|
225
|
+
}
|
|
226
|
+
return records.length;
|
|
227
|
+
}
|
|
228
|
+
|
|
210
229
|
/**
|
|
211
230
|
* Allow only listed codiscuss website subcommands and draft writes for this run; user rules are added after.
|
|
212
231
|
* Claude Code on Windows may run commands through its PowerShell tool instead of Bash, so both are listed.
|
|
@@ -328,11 +347,10 @@ function cli_invocation(config, binding, task, user_home) {
|
|
|
328
347
|
const project = prepare_agy_project(config, binding, task, user_home);
|
|
329
348
|
const args = ['--output-format', 'stream-json', '--print-timeout', '0', '--add-dir', task.directory,
|
|
330
349
|
'--add-dir', task.draft_directory, '--add-dir', task.skills_directory];
|
|
331
|
-
|
|
332
|
-
if (project) args.push('--project', project.project_id);
|
|
350
|
+
args.push('--mode', 'accept-edits', '--project', project.project_id);
|
|
333
351
|
if (binding.session_id) args.push('--conversation', binding.session_id);
|
|
334
352
|
args.push('-p', task.prompt);
|
|
335
|
-
return { args, project
|
|
353
|
+
return { args, project };
|
|
336
354
|
}
|
|
337
355
|
|
|
338
356
|
/** Persist an emitted identity before the agent reads its execution credential file. */
|
|
@@ -428,8 +446,7 @@ async function cli_delivery(config, binding, task, spawn_process, user_home) {
|
|
|
428
446
|
});
|
|
429
447
|
child.stdin.on('error', error => { if (error.code !== 'EPIPE') task.on_failure(error.message); });
|
|
430
448
|
child.stdin.end(invocation.input);
|
|
431
|
-
return { session_id: state.session_id, status: 'accepted'
|
|
432
|
-
...(invocation.permission_enforcement ? { permission_enforcement: invocation.permission_enforcement } : {}) };
|
|
449
|
+
return { session_id: state.session_id, status: 'accepted' };
|
|
433
450
|
}
|
|
434
451
|
|
|
435
452
|
/** Queue one immutable request for the official App-managed Sidecar. */
|
|
@@ -111,6 +111,8 @@ function launch_config(home, input, room, directory, options) {
|
|
|
111
111
|
const cwd = profile.cwd || input.cwd;
|
|
112
112
|
if (cwd && (!existsSync(cwd) || !statSync(cwd).isDirectory())) throw new Error(`${name}:工作目录不存在或不是文件夹,请核对本次目录配置`);
|
|
113
113
|
if (input.entry === 'conversation' && name === room.host) profile.session_id = input.current_session_id;
|
|
114
|
+
// agy runs only in sessions this tool creates, so its native discussion project always applies.
|
|
115
|
+
if (profile.adapter === 'antigravity_cli' && profile.session_id) throw new Error(`${name}:agy 不能绑定已有会话或从当前 agy 对话主持全自动讨论;请从网页或其他软件发起,agy 会由讨论服务新建会话`);
|
|
114
116
|
participants[name] = profile;
|
|
115
117
|
}
|
|
116
118
|
return validate_config({ site: input.site, room: room.id || 'preview', token: options.token, participants,
|
|
@@ -6,7 +6,7 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
7
|
import { parseArgs } from 'node:util';
|
|
8
8
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
9
|
-
import { atomic_json, deliver_task, setup_sidecar, sidecar_receipt, TASK_SUBCOMMANDS } from './discussion_adapters.mjs';
|
|
9
|
+
import { atomic_json, deliver_task, remove_agy_projects, setup_sidecar, sidecar_receipt, TASK_SUBCOMMANDS } from './discussion_adapters.mjs';
|
|
10
10
|
|
|
11
11
|
const POLL_MS = 2000;
|
|
12
12
|
const DELIVERY_RETRY_DELAYS_MS = [2000, 4000];
|
|
@@ -189,7 +189,7 @@ export class DiscussionRunner {
|
|
|
189
189
|
try {
|
|
190
190
|
const task = task_files(this.config, event, row.token, binding);
|
|
191
191
|
invoked = true;
|
|
192
|
-
const result = await this.deliver(adapter, binding, { ...task,
|
|
192
|
+
const result = await this.deliver(adapter, binding, { ...task, room_title: this.room_title,
|
|
193
193
|
on_binding: session => this.store.bind(event.participant, session),
|
|
194
194
|
on_process: child => { this.children.add(child); child.once('close', () => this.children.delete(child)); },
|
|
195
195
|
on_output: (path, chunk) => appendFileSync(path, chunk, { mode: 0o600 }),
|
|
@@ -256,12 +256,19 @@ export class DiscussionRunner {
|
|
|
256
256
|
/** Process one bounded iteration without waking models for empty checks. */
|
|
257
257
|
async tick() {
|
|
258
258
|
this.store.context(this.config);
|
|
259
|
-
let room
|
|
259
|
+
let room;
|
|
260
|
+
try { room = await api(this.config, ''); }
|
|
261
|
+
catch (error) {
|
|
262
|
+
// Only a permanently deleted room drops its private agy projects; concluded or archived rooms keep them for later continuation.
|
|
263
|
+
if (/^HTTP 404: 房间不存在/.test(error.message)) remove_agy_projects(this.config.directory);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
260
266
|
if (room.limits?.reached && (!room.limits.closing || room.limits.reason === 'time_limit') && room.runtime.status === 'running') {
|
|
261
267
|
const generation = room.runtime.generation;
|
|
262
268
|
room = await api(this.config, '/limits', { actor: '用户' });
|
|
263
269
|
if (room.runtime.generation !== generation) this.stop_children();
|
|
264
270
|
}
|
|
271
|
+
this.room_title = room.title;
|
|
265
272
|
const run = room.runtime;
|
|
266
273
|
if (run.run_id) this.store.scope(this.config, run.run_id);
|
|
267
274
|
if (run.mode !== 'automatic' || run.status !== 'running' || run.expired) {
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/** npm bootstrap and per-user setup; never logs in or starts an AI conversation. */
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
-
import { accessSync, constants, copyFileSync, existsSync,
|
|
3
|
+
import { accessSync, constants, copyFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs';
|
|
4
4
|
import { homedir, tmpdir } from 'node:os';
|
|
5
5
|
import { basename, delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { parseArgs } from 'node:util';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { atomic_json } from './discussion_adapters.mjs';
|
|
10
10
|
import { launch_settings, save_launch_settings } from './discussion_launch.mjs';
|
|
11
11
|
import { service_home } from './discussion_service.mjs';
|
|
12
12
|
import { install_skills } from './discussion_skills.mjs';
|
|
13
13
|
import { ask, checkbox_prompt, intro, outro, pad, short_path, spinner, step, styled, text_width, tone } from './discussion_tui.mjs';
|
|
14
|
-
export { agy_rule } from './discussion_adapters.mjs';
|
|
15
14
|
export { checkbox_prompt } from './discussion_tui.mjs';
|
|
16
15
|
|
|
17
16
|
const ROOT = fileURLToPath(new URL('../', import.meta.url));
|
|
@@ -198,62 +197,44 @@ async function choose_profile(client, current, options) {
|
|
|
198
197
|
return { client: client.name, status: 'configured', effective: detected };
|
|
199
198
|
}
|
|
200
199
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Ask once before adding the rules; --yes counts as consent, other noninteractive runs only report. */
|
|
209
|
-
async function agy_consent(path, rules, options) {
|
|
210
|
-
if (options.yes) return true;
|
|
211
|
-
if (!process.stdin.isTTY) return false;
|
|
212
|
-
const answer = await ask(`允许 Gemini(agy)在全自动讨论中执行讨论命令并写讨论草稿?将向 ${path} 的 permissions.allow 追加 ${rules.length} 条只匹配讨论命令与草稿目录的规则`, ' [Y/n]');
|
|
213
|
-
return !['n', 'no', '否'].includes(answer.trim().toLowerCase());
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Keep the installer-managed allow rules current, never touching other settings or user rules. */
|
|
217
|
-
export async function allow_agy_command(home, user_home, options) {
|
|
218
|
-
const path = join(user_home, ...AGY_SETTINGS);
|
|
200
|
+
/**
|
|
201
|
+
* Remove global agy rules that earlier versions added; agy permissions now live only in each discussion project.
|
|
202
|
+
* Only rules recorded by the installer are removed, after a backup; other settings and user rules stay untouched.
|
|
203
|
+
*/
|
|
204
|
+
export function remove_legacy_agy_rules(home, user_home) {
|
|
219
205
|
const record = join(home, AGY_RULE_RECORD);
|
|
220
|
-
|
|
206
|
+
if (!existsSync(record)) return null;
|
|
207
|
+
const path = join(user_home, ...AGY_SETTINGS);
|
|
208
|
+
const saved = JSON.parse(readFileSync(record, 'utf8'));
|
|
209
|
+
const legacy = saved.rules || (saved.rule ? [saved.rule] : []);
|
|
221
210
|
let settings = {};
|
|
222
211
|
try { if (existsSync(path)) settings = JSON.parse(readFileSync(path, 'utf8')); }
|
|
223
|
-
catch { return { status: 'invalid', path
|
|
212
|
+
catch { return { client: 'Gemini', status: 'invalid', path }; }
|
|
224
213
|
const allow = settings.permissions?.allow ?? [];
|
|
225
|
-
if (!Array.isArray(allow)) return { status: 'invalid', path
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (rules.every(rule => allow.includes(rule)) && !stale) {
|
|
231
|
-
if (previous.join('\n') !== rules.join('\n')) atomic_json(record, { rules });
|
|
232
|
-
return { status: 'unchanged', path, rules };
|
|
214
|
+
if (!Array.isArray(allow)) return { client: 'Gemini', status: 'invalid', path };
|
|
215
|
+
const next = allow.filter(rule => !legacy.includes(rule));
|
|
216
|
+
if (next.length !== allow.length) {
|
|
217
|
+
copyFileSync(path, join(home, AGY_SETTINGS_BACKUP));
|
|
218
|
+
atomic_json(path, { ...settings, permissions: { ...settings.permissions, allow: next } });
|
|
233
219
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (existsSync(path)) copyFileSync(path, join(home, AGY_SETTINGS_BACKUP));
|
|
237
|
-
const next = [...allow.filter(item => !previous.includes(item) && !rules.includes(item)), ...rules];
|
|
238
|
-
atomic_json(path, { ...settings, permissions: { ...settings.permissions, allow: next } });
|
|
239
|
-
atomic_json(record, { rules });
|
|
240
|
-
return { status: 'allowed', path, rules };
|
|
220
|
+
rmSync(record);
|
|
221
|
+
return { client: 'Gemini', status: next.length !== allow.length ? 'removed' : 'unchanged', path };
|
|
241
222
|
}
|
|
242
223
|
|
|
243
224
|
/** Save the selected effective profiles and copy complete skills without overwriting edits. */
|
|
244
225
|
async function configure_clients(home, selected, options, existing) {
|
|
245
226
|
const participants = { ...existing };
|
|
246
|
-
const skills = [], profiles = []
|
|
227
|
+
const skills = [], profiles = [];
|
|
247
228
|
const user_home = resolve(options['skills-home'] || homedir());
|
|
248
229
|
for (const client of selected) {
|
|
249
230
|
const profile = await choose_profile(client, participants[client.name], options);
|
|
250
231
|
profiles.push(profile);
|
|
251
232
|
if (profile.status === 'configured') participants[client.name] = { ...participants[client.name], ...profile.effective };
|
|
252
233
|
skills.push({ client: client.name, results: install_skills(join(user_home, ...client.skills)) });
|
|
253
|
-
if (client.key === 'antigravity') permissions.push({ client: client.name, ...await allow_agy_command(home, user_home, options) });
|
|
254
234
|
}
|
|
255
235
|
save_launch_settings(home, { participants });
|
|
256
|
-
|
|
236
|
+
const legacy = remove_legacy_agy_rules(home, user_home);
|
|
237
|
+
return { skills, profiles, permissions: legacy ? [legacy] : [] };
|
|
257
238
|
}
|
|
258
239
|
|
|
259
240
|
/** Exclude npm exec's temporary command wrappers when checking persistent availability. */
|
|
@@ -292,8 +273,7 @@ function setup_issues(result) {
|
|
|
292
273
|
if (skill.status === 'conflict') issues.push(`${group.client} 技能冲突,已保留 ${skill.path};请核对并移走需替换的旧副本,再运行 setup。`);
|
|
293
274
|
}
|
|
294
275
|
for (const permission of result.permissions) {
|
|
295
|
-
if (permission.status === 'invalid') issues.push(`${permission.client}
|
|
296
|
-
if (['declined', 'pending'].includes(permission.status)) issues.push(`${permission.client} 未放行讨论命令,全自动讨论会被拦截;运行 codiscuss setup --clients antigravity --yes 授权。`);
|
|
276
|
+
if (permission.status === 'invalid') issues.push(`${permission.client} 设置文件无法解析,未能移除旧版全局放行规则:${permission.path};修复后重新运行 setup,否则全自动讨论会拒绝交付给 agy。`);
|
|
297
277
|
}
|
|
298
278
|
if (result.command.status !== 'ready') issues.push(result.command.action);
|
|
299
279
|
return issues;
|
|
@@ -345,7 +325,7 @@ function report_setup(result, options = {}) {
|
|
|
345
325
|
? `${profile.client}:仅安装技能,可手动参与,尚不支持全自动接入。`
|
|
346
326
|
: `${profile.client}:${profile.status === 'configured' ? 'CLI 已配置' : '沿用已有配置'}(${profile.effective.adapter} / ${profile.effective.executable || '未设置路径'})`);
|
|
347
327
|
for (const permission of result.permissions) {
|
|
348
|
-
if (
|
|
328
|
+
if (permission.status === 'removed') lines.push(`${permission.client}:已移除旧版全局放行规则(${permission.path})`);
|
|
349
329
|
}
|
|
350
330
|
for (const group of result.skills) lines.push(`${group.client} Skill:${group.results.filter(skill => skill.status !== 'conflict').length}/${group.results.length} 份文件就绪,目录 ${dirname(group.results[0].path)}`);
|
|
351
331
|
if (result.command.path) lines.push(`持久命令:${result.command.path}(${result.command.status})`);
|
|
@@ -368,7 +348,7 @@ function report_styled(result, options) {
|
|
|
368
348
|
for (const profile of result.profiles) rows.push(profile.status === 'manual' ? [profile.client, '仅安装技能,可手动参与', '尚不支持全自动接入']
|
|
369
349
|
: [profile.client, `${profile.status === 'configured' ? 'CLI 已配置' : '沿用已有配置'} · ${profile.effective.adapter}`, short_path(profile.effective.executable) || '未设置路径']);
|
|
370
350
|
for (const permission of result.permissions) {
|
|
371
|
-
if (
|
|
351
|
+
if (permission.status === 'removed') rows.push([permission.client, '已移除旧版全局放行规则', short_path(permission.path)]);
|
|
372
352
|
}
|
|
373
353
|
if (result.skills.length) rows.push(['技能', result.skills.map(group => `${group.client} ${group.results.filter(skill => skill.status !== 'conflict').length}/${group.results.length}`).join(' · '), 'discuss-host · discuss-participant · discuss-report']);
|
|
374
354
|
if (result.command.path) rows.push(['命令', `codiscuss ${tone.dim(`(${result.command.status})`)}`, short_path(result.command.path)]);
|