dsh-agy-link 0.1.4 → 0.2.0
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/CHANGELOG.md +25 -45
- package/README.md +135 -73
- package/dist/bridge.mjs +128 -0
- package/dist/index.js +475 -9
- package/docs/KNOWN-GAPS.md +15 -7
- package/package.json +1 -2
- package/README.zh.md +0 -112
package/CHANGELOG.md
CHANGED
|
@@ -1,48 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.2.0 (2026-08-19)
|
|
4
|
+
|
|
5
|
+
- Multimodal (path-based): DSH image attachments are staged to a local media
|
|
6
|
+
directory (config `mediaDir`, TTL sweep `mediaTtlMs`, caps `mediaMaxBytes` /
|
|
7
|
+
`mediaMaxImages`) and referenced by absolute path in the agy prompt with
|
|
8
|
+
`--add-dir` - agy views them with its own tools. Env: `DSH_AGY_MEDIA_DIR`,
|
|
9
|
+
`DSH_AGY_MEDIA_TTL_MS`.
|
|
10
|
+
- `agy_ask` gains `readPaths` (inline text files into the one-shot prompt;
|
|
11
|
+
binaries skipped with a note) and `schema` (JSON Schema enforced via
|
|
12
|
+
`--json-schema`).
|
|
13
|
+
- MCP reverse bridge (experimental, `mcpBridge: true` / `DSH_AGY_MCP_BRIDGE=1`):
|
|
14
|
+
agy can call DSH-side tools through a loopback, token-guarded endpoint plus a
|
|
15
|
+
zero-dep stdio MCP server merged into the workspace `.mcp.json`.
|
|
16
|
+
`mcpToolAllowlist` restricts the exposed set; `run_code` / `agy_ask` are
|
|
17
|
+
never bridged.
|
|
18
|
+
- Abort semantics locked by a regression test: everything the model produced
|
|
19
|
+
before the caller hits stop is preserved (open block closed, then failure
|
|
20
|
+
finish).
|
|
21
|
+
- 12 new tests (51 total).
|
|
22
|
+
|
|
23
|
+
## 0.1.5 (2026-08-19)
|
|
24
|
+
|
|
25
|
+
- README restructured into a single bilingual page: Chinese first, then
|
|
26
|
+
English (README.zh.md removed; package files list updated).
|
|
27
|
+
- Releases now publish to npm automatically (NPM_TOKEN secret configured).
|
|
4
28
|
|
|
5
|
-
- READMEs: added the author promo block (Chinese first, then English).
|
|
6
|
-
- Housekeeping: internal spec documents are no longer part of the repo.
|
|
7
|
-
|
|
8
|
-
## 0.1.3 (2026-08-19)
|
|
9
|
-
|
|
10
|
-
- Fixed the client slot registration: the sidebar footer slot entry must
|
|
11
|
-
register under the slot name `sidebar.footer.action` (not `ui-sidebar`),
|
|
12
|
-
matching the ui-sidebar slot table - the wrong name failed plugin loading
|
|
13
|
-
with: slot ui-sidebar is not declared.
|
|
14
|
-
|
|
15
|
-
## 0.1.2 (2026-08-18)
|
|
16
|
-
|
|
17
|
-
- Fixed prompt assembly for returning sessions: the last-assistant scan now
|
|
18
|
-
stops at the newest assistant message, and the history digest covers exactly
|
|
19
|
-
the foreign turns since the watermark (previously it could re-send stale
|
|
20
|
-
history or swallow intermediate turns into the trailing prompt).
|
|
21
|
-
- Hardened the stream loop: spawn failures release the concurrency slot and
|
|
22
|
-
surface as `PROCESS_EXIT` instead of hanging; the collector is fail-safe.
|
|
23
|
-
- `/agy doctor` now exports the real last-run stdout ring (parser wiring).
|
|
24
|
-
- `agy_ask` one-shot runs pass `--print-timeout` consistent with the watchdog.
|
|
25
|
-
|
|
26
|
-
## 0.1.1 (2026-08-18)
|
|
27
|
-
|
|
28
|
-
- Fixed cordis activation in app-less profiles (optional services are now
|
|
29
|
-
resolved via `ctx.get`, not `?`-suffixed inject entries).
|
|
30
|
-
- Added `providerRetryPolicy`: AUTH failures fail fast (no quota-burning
|
|
31
|
-
retries); transient process failures retry once.
|
|
32
|
-
|
|
33
|
-
## 0.1.0 (2026-08-18)
|
|
34
|
-
|
|
35
|
-
Initial release.
|
|
36
|
-
|
|
37
|
-
- `antigravity` LLM provider route over `agy -p --output-format stream-json`.
|
|
38
|
-
- Streaming text / thinking / tool-activity mapping with token usage.
|
|
39
|
-
- Session ↔ conversation binding with history digest on first contact.
|
|
40
|
-
- Model discovery (`agy models`) with Gemini effort folding and fallback
|
|
41
|
-
catalog.
|
|
42
|
-
- Google login: `/agy auth` + `/agy auth-code` + GUI panel (QR + paste).
|
|
43
|
-
- `/agy` command family: status / auth / auth-code / models / mode /
|
|
44
|
-
effort / clear / doctor / help.
|
|
45
|
-
- `agy_ask` delegation tool (optional).
|
|
46
|
-
- Permission modes: plan / accept-edits / skip with red-flag UI on skip.
|
|
47
|
-
- `/agy doctor` diagnostics export with auth redaction.
|
|
48
|
-
- Auxiliary calls (compaction / session-title) via one-shot plan-mode turns.
|
package/README.md
CHANGED
|
@@ -1,72 +1,153 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">🛰️ dsh-agy-link</h1>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<b>DeepSeek Harness × Google Antigravity</b> — 用官方 agy CLI 把 Antigravity 模型接进 DSH / bring Google Antigravity models into DSH via the official agy CLI
|
|
5
|
+
</p>
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/dsh-agy-link"><img src="https://img.shields.io/npm/v/dsh-agy-link?color=cb3837&label=npm&logo=npm" alt="npm"/></a>
|
|
9
|
+
<a href="https://github.com/amlyczz/dsh-agy-link/actions/workflows/ci.yml"><img src="https://github.com/amlyczz/dsh-agy-link/actions/workflows/ci.yml/badge.svg" alt="CI"/></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/license-MIT-lightgrey" alt="MIT"/>
|
|
11
|
+
<img src="https://img.shields.io/badge/node-%3E%3D24-green" alt="node"/>
|
|
12
|
+
</p>
|
|
10
13
|
|
|
11
14
|
---
|
|
12
15
|
|
|
16
|
+
# 中文
|
|
17
|
+
|
|
13
18
|
> 全网统一昵称:**小斯syzs** · B站 [@小斯syzs](https://space.bilibili.com/390211071) · 抖音 · 小红书 · 快手(全网同名)
|
|
14
19
|
>
|
|
15
20
|
> 💬 **小斯syzs 邀请你加入飞书群** —— [点此一键加入](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=553i2f4a-5cc8-487f-95d3-3c4095bec0d9)
|
|
16
21
|
|
|
22
|
+
把 **Google Antigravity 模型接入 DeepSeek Harness(DSH)** —— 由官方
|
|
23
|
+
`agy` CLI 驱动,完整支持对话输出、思考(thinking)、工具活动与 token
|
|
24
|
+
用量,全部走你的 Antigravity 订阅。
|
|
25
|
+
|
|
26
|
+
## ✨ 你能得到什么
|
|
27
|
+
|
|
28
|
+
| 能力 | 说明 |
|
|
29
|
+
| ---- | ---- |
|
|
30
|
+
| 🔌 **模型路由** | 在 DSH 模型配置里注册 `antigravity` 提供方,`/model` 选择器直接挑任意 Antigravity 模型(Gemini / Claude / GPT-OSS) |
|
|
31
|
+
| 🌊 **完整流式** | 文本、思考、agy 工具活动(注记为 reasoning 块)全部映射到 DSH 原生流协议 |
|
|
32
|
+
| 🔗 **会话连续** | 每个 DSH 会话绑定原生 agy 会话(`--conversation`),多轮上下文由 agy 历史承载,不重发全量 |
|
|
33
|
+
| 📊 **token 用量** | 输入/输出/思考/缓存 token 全部进入 DSH 用量统计 |
|
|
34
|
+
| 🔐 **GUI 内 Google 登录** | `/agy auth` 打印授权 URL;侧栏面板提供二维码 + 授权码粘贴框 |
|
|
35
|
+
| 🤝 **`agy_ask` 工具** | 任何 DSH 模型都可以把一次性任务委托给 Antigravity 模型(AskAntigravity 模式) |
|
|
36
|
+
| ⌨️ **`/agy` 命令族** | `status` / `auth` / `auth-code` / `models` / `mode` / `effort` / `clear` / `doctor` / `help` |
|
|
37
|
+
| 🖼 **图片多模态(v0.2)** | 图片落盘到本地媒体目录(TTL 清理),prompt 以绝对路径引用 + `--add-dir` 授权,agy 用自己的工具看图 |
|
|
38
|
+
| 📎 **文件内联 + 结构化输出(v0.2)** | `agy_ask` 新增 `readPaths`(文本文件内联)与 `schema`(`--json-schema` 强约束答案) |
|
|
39
|
+
| 🌉 **MCP 反向桥(v0.2 实验)** | `mcpBridge: true` 开启后 agy 可直接调 DSH 侧工具(回环 + token 守卫端点 + 零依赖 stdio MCP 服务器,`.mcp.json` 合并写入/禁用还原) |
|
|
40
|
+
| 😴 **休眠安全** | 没装 agy?没登录?插件照常加载并告诉你怎么修 |
|
|
41
|
+
|
|
42
|
+
## 🚀 快速开始
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 1. 安装(npm 官方包,预构建产物,无需构建许可):
|
|
46
|
+
dsh plugin --profile web add dsh-agy-link
|
|
47
|
+
# 升级:
|
|
48
|
+
# dsh plugin --profile web update dsh-agy-link --latest
|
|
49
|
+
|
|
50
|
+
# 2. 重启 DSH Web GUI,输入框执行:
|
|
51
|
+
# /agy status ← 确认 agy 已被检测到
|
|
52
|
+
|
|
53
|
+
# 3. 登录(一次性):
|
|
54
|
+
# /agy auth ← 打开授权 URL,批准并复制授权码
|
|
55
|
+
# /agy auth-code <授权码> ← 或直接用侧栏面板的二维码 + 粘贴框
|
|
56
|
+
|
|
57
|
+
# 4. /model 选择器里选 antigravity 模型,开聊
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
> 插件绝不读取/复制/移动 `~/.gemini/antigravity-cli/antigravity-oauth-token`;登录完全通过官方 CLI 自己的流程完成。
|
|
61
|
+
|
|
62
|
+
## ⚙️ 配置
|
|
63
|
+
|
|
64
|
+
配置在 `agy-link` 插件条目里(`/plugin` 或 profile patch 层编辑),环境变量优先:
|
|
65
|
+
|
|
66
|
+
| 键 | 环境变量 | 默认值 | 含义 |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| enabled | `DSH_AGY_ENABLED` | `true` | 总开关 |
|
|
69
|
+
| agyBin | `DSH_AGY_BIN` | 自动 | 显式 agy 路径 |
|
|
70
|
+
| permissionMode | `DSH_AGY_MODE` | `skip` | `skip` / `plan` / `accept-edits`(见下) |
|
|
71
|
+
| defaultModel | `DSH_AGY_DEFAULT_MODEL` | `(agy 默认)` | 模型 slug |
|
|
72
|
+
| defaultEffort | `DSH_AGY_DEFAULT_EFFORT` | `(模型默认)` | `low` / `medium` / `high` |
|
|
73
|
+
| timeoutMs | `DSH_AGY_TIMEOUT_MS` | `600000` | 单轮看门狗 |
|
|
74
|
+
| extraArgs | `DSH_AGY_EXTRA_ARGS` | — | 附加 agy 参数(空格分隔) |
|
|
75
|
+
|
|
76
|
+
### ⚠️ 权限模式——必读
|
|
77
|
+
|
|
78
|
+
agy 有**自己的**工具循环,会自己做文件编辑和 shell 执行。桥接层映射为三种模式:
|
|
79
|
+
|
|
80
|
+
- **plan** —— 只读,体验桥接的安全默认。
|
|
81
|
+
- **accept-edits** —— agy 可以不经询问改文件。
|
|
82
|
+
- **skip** —— `--dangerously-skip-permissions`:agy **所有**工具免审批。非交互 DSH 轮次需要它(或 plan),因为 agy 的权限提示会卡死 print 模式;但这意味着真实的无人值守 写/执行 权限。设置面板里这个按钮是红色的。
|
|
83
|
+
|
|
84
|
+
DSH 侧的工具与权限不受影响——这里只约束 agy 子进程在自己工作区能做什么。
|
|
85
|
+
|
|
86
|
+
## 🧩 工作原理
|
|
87
|
+
|
|
88
|
+
一次 DSH 模型调用 = 一个短生命周期 `agy -p --output-format stream-json` 进程。NDJSON 事件流被解析、归一化并映射为 DSH StreamChunk:思考 → reasoning 块,文本 → text 块,工具活动 → 注记 reasoning 块,结果信封 → usage + finish。会话 id 优先取自流本身,conversations 目录快照对比兜底。**不逆向数据库、不解码 protobuf、不碰 token 文件**——只启动官方未修改的 agy 二进制。
|
|
89
|
+
|
|
90
|
+
## 📋 已知差距
|
|
91
|
+
|
|
92
|
+
见 [docs/KNOWN-GAPS.md](docs/KNOWN-GAPS.md)。要点:图片不转发(agy print 模式纯文本);DSH 工具不暴露给 agy(MCP 反向桥接是后续工作)。
|
|
93
|
+
|
|
94
|
+
## ⚠️ 免责与风险提示
|
|
95
|
+
|
|
96
|
+
本插件只启动**官方未修改**的 `agy` 二进制并消费其公开 print 输出,不解码内部数据库、不触碰 OAuth token。自动化订阅 CLI 仍可能与提供方服务条款冲突(Google Antigravity ToS §6 语境);风险自负,被要求停止时请停止。
|
|
97
|
+
|
|
98
|
+
## 许可证
|
|
99
|
+
|
|
100
|
+
MIT
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
# English
|
|
105
|
+
|
|
17
106
|
> Known online as **小斯syzs** — Bilibili [@小斯syzs](https://space.bilibili.com/390211071) · Douyin · Xiaohongshu · Kuaishou (same handle on every platform).
|
|
18
107
|
>
|
|
19
108
|
> 💬 **Join the Feishu community group** hosted by 小斯syzs — [one-click invite](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=553i2f4a-5cc8-487f-95d3-3c4095bec0d9)
|
|
20
109
|
|
|
110
|
+
Bring **Google Antigravity models into DeepSeek Harness (DSH)** — chat, thinking, tool activity, and token usage from your Antigravity subscription, driven by the official `agy` CLI.
|
|
21
111
|
|
|
22
|
-
## What you get
|
|
23
|
-
|
|
24
|
-
- **A model route, not a proxy** — registers the `antigravity` provider in
|
|
25
|
-
DSH's model config; pick any Antigravity model from the `/model` picker.
|
|
26
|
-
- **Full streaming** — text, thinking (reasoning), and agy tool activity
|
|
27
|
-
(annotated as reasoning blocks), mapped onto DSH's native chunk protocol.
|
|
28
|
-
- **Session continuity** — each DSH session binds to a native agy
|
|
29
|
-
conversation (`--conversation`), so multi-turn context rides agy history
|
|
30
|
-
instead of re-sending everything.
|
|
31
|
-
- **Token usage** — input/output/thinking/cache tokens surface in DSH
|
|
32
|
-
usage accounting.
|
|
33
|
-
- **In-GUI Google login** — `/agy auth` prints the consent URL; the sidebar
|
|
34
|
-
panel adds a QR code and a paste box for the authorization code.
|
|
35
|
-
- **`agy_ask` tool** — let any DSH model delegate a one-shot task to an
|
|
36
|
-
Antigravity model (the AskAntigravity pattern).
|
|
37
|
-
- **`/agy` commands** — `status`, `auth`, `auth-code`, `models`, `mode`,
|
|
38
|
-
`effort`, `clear`, `doctor`, `help`.
|
|
39
|
-
- **Dormant-safe** — no agy binary? Not signed in? The plugin loads anyway
|
|
40
|
-
and tells you what to fix.
|
|
112
|
+
## ✨ What you get
|
|
41
113
|
|
|
42
|
-
|
|
114
|
+
| Capability | Description |
|
|
115
|
+
| ---- | ---- |
|
|
116
|
+
| 🔌 **A model route, not a proxy** | Registers the `antigravity` provider in DSH's model config; pick any Antigravity model (Gemini / Claude / GPT-OSS) from the `/model` picker |
|
|
117
|
+
| 🌊 **Full streaming** | Text, thinking (reasoning), and agy tool activity (annotated reasoning blocks) mapped onto DSH's native chunk protocol |
|
|
118
|
+
| 🔗 **Session continuity** | Each DSH session binds to a native agy conversation (`--conversation`); multi-turn context rides agy history instead of re-sending everything |
|
|
119
|
+
| 📊 **Token usage** | Input/output/thinking/cache tokens surface in DSH usage accounting |
|
|
120
|
+
| 🔐 **In-GUI Google login** | `/agy auth` prints the consent URL; the sidebar panel adds a QR code and a paste box for the authorization code |
|
|
121
|
+
| 🤝 **`agy_ask` tool** | Let any DSH model delegate a one-shot task to an Antigravity model (the AskAntigravity pattern) |
|
|
122
|
+
| ⌨️ **`/agy` commands** | `status`, `auth`, `auth-code`, `models`, `mode`, `effort`, `clear`, `doctor`, `help` |
|
|
123
|
+
| 😴 **Dormant-safe** | No agy binary? Not signed in? The plugin loads anyway and tells you what to fix |
|
|
124
|
+
| 🖼 **Image multimodal (v0.2)** | image attachments staged to a TTL-swept local dir, referenced by absolute path with `--add-dir`; agy views them with its own tools |
|
|
125
|
+
| 📎 **File inlining + structured output (v0.2)** | `agy_ask` gains `readPaths` (inline text files) and `schema` (enforced via `--json-schema`) |
|
|
126
|
+
| 🌉 **MCP reverse bridge (v0.2, experimental)** | with `mcpBridge: true`, agy calls DSH-side tools directly (loopback token-guarded endpoint + zero-dep stdio MCP server merged into `.mcp.json`, restored on disable) |
|
|
43
127
|
|
|
44
|
-
|
|
45
|
-
- Node.js >= 24
|
|
46
|
-
- The official `agy` CLI >= 1.1.8, installed and on `PATH`
|
|
47
|
-
|
|
48
|
-
## Install
|
|
128
|
+
## 🚀 Quick start
|
|
49
129
|
|
|
50
130
|
```bash
|
|
131
|
+
# 1. Install (npm, prebuilt — no build approval needed):
|
|
51
132
|
dsh plugin --profile web add dsh-agy-link
|
|
52
|
-
|
|
133
|
+
# Upgrade:
|
|
134
|
+
# dsh plugin --profile web update dsh-agy-link --latest
|
|
53
135
|
|
|
54
|
-
Restart DSH, then
|
|
136
|
+
# 2. Restart the DSH Web GUI, then in the composer:
|
|
137
|
+
# /agy status <- confirm agy was detected
|
|
55
138
|
|
|
56
|
-
|
|
139
|
+
# 3. Login (one time):
|
|
140
|
+
# /agy auth <- open the consent URL, approve, copy the code
|
|
141
|
+
# /agy auth-code <code> <- or use the sidebar panel (QR + paste box)
|
|
57
142
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
3. `/agy auth-code <code>` — or use the sidebar panel (QR + paste box).
|
|
143
|
+
# 4. Pick an antigravity model in /model and chat
|
|
144
|
+
```
|
|
61
145
|
|
|
62
|
-
The plugin never reads, copies, or moves
|
|
63
|
-
`~/.gemini/antigravity-cli/antigravity-oauth-token`; login is driven
|
|
64
|
-
entirely through the official CLI's own flow.
|
|
146
|
+
> The plugin never reads, copies, or moves `~/.gemini/antigravity-cli/antigravity-oauth-token`; login is driven entirely through the official CLI's own flow.
|
|
65
147
|
|
|
66
|
-
## Configuration
|
|
148
|
+
## ⚙️ Configuration
|
|
67
149
|
|
|
68
|
-
Config lives in the `agy-link` plugin entry (edit via `/plugin` or the
|
|
69
|
-
profile patch layer). Environment variables override the file:
|
|
150
|
+
Config lives in the `agy-link` plugin entry (edit via `/plugin` or the profile patch layer). Environment variables override the file:
|
|
70
151
|
|
|
71
152
|
| Key | Env | Default | Meaning |
|
|
72
153
|
| --- | --- | --- | --- |
|
|
@@ -78,47 +159,28 @@ profile patch layer). Environment variables override the file:
|
|
|
78
159
|
| timeoutMs | `DSH_AGY_TIMEOUT_MS` | `600000` | per-turn watchdog |
|
|
79
160
|
| extraArgs | `DSH_AGY_EXTRA_ARGS` | — | extra agy flags, space-separated |
|
|
80
161
|
|
|
81
|
-
### Permission modes — read this
|
|
162
|
+
### ⚠️ Permission modes — read this
|
|
82
163
|
|
|
83
|
-
agy runs its **own** tool loop with its own file edits and shell commands.
|
|
84
|
-
The bridge maps that to one of three modes:
|
|
164
|
+
agy runs its **own** tool loop with its own file edits and shell commands. The bridge maps that to one of three modes:
|
|
85
165
|
|
|
86
166
|
- **plan** — read-only; the safe default for trying the bridge.
|
|
87
167
|
- **accept-edits** — agy may edit files without asking.
|
|
88
|
-
- **skip** — `--dangerously-skip-permissions`: agy runs **every** tool
|
|
89
|
-
without approval. Non-interactive DSH turns need this (or plan) because
|
|
90
|
-
agy permission prompts hang print mode, but it means real unattended
|
|
91
|
-
write/exec access. The settings panel shows this button in red.
|
|
168
|
+
- **skip** — `--dangerously-skip-permissions`: agy runs **every** tool without approval. Non-interactive DSH turns need this (or plan) because agy permission prompts hang print mode, but it means real unattended write/exec access. The settings panel shows this button in red.
|
|
92
169
|
|
|
93
|
-
DSH-side tools and permissions are unaffected — this only governs what the
|
|
94
|
-
spawned agy process may do in its workspace.
|
|
170
|
+
DSH-side tools and permissions are unaffected — this only governs what the spawned agy process may do in its workspace.
|
|
95
171
|
|
|
96
|
-
## How it works
|
|
172
|
+
## 🧩 How it works
|
|
97
173
|
|
|
98
|
-
One DSH model call = one short-lived `agy -p --output-format stream-json`
|
|
99
|
-
process. The NDJSON event stream is parsed, normalized, and mapped to
|
|
100
|
-
DSH StreamChunks: thinking → reasoning blocks, text → text blocks, tool
|
|
101
|
-
activity → annotated reasoning blocks, and the result envelope → usage +
|
|
102
|
-
finish. Conversation ids are discovered from the stream itself with a
|
|
103
|
-
conversations-directory snapshot diff as fallback. No reverse-engineered
|
|
104
|
-
database scraping, no protobuf decoding, no token file access.
|
|
174
|
+
One DSH model call = one short-lived `agy -p --output-format stream-json` process. The NDJSON event stream is parsed, normalized, and mapped to DSH StreamChunks: thinking → reasoning blocks, text → text blocks, tool activity → annotated reasoning blocks, and the result envelope → usage + finish. Conversation ids come from the stream itself, with a conversations-directory snapshot diff as fallback. **No reverse-engineered database scraping, no protobuf decoding, no token-file access** — only the official unmodified agy binary is spawned.
|
|
105
175
|
|
|
106
|
-
## Known gaps
|
|
176
|
+
## 📋 Known gaps
|
|
107
177
|
|
|
108
|
-
See [docs/KNOWN-GAPS.md](docs/KNOWN-GAPS.md). Notably: images are not
|
|
109
|
-
forwarded (agy print mode is text-only), and DSH tools are not exposed to
|
|
110
|
-
agy (its MCP reverse bridge is future work).
|
|
178
|
+
See [docs/KNOWN-GAPS.md](docs/KNOWN-GAPS.md). Notably: images are not forwarded (agy print mode is text-only), and DSH tools are not exposed to agy (its MCP reverse bridge is future work).
|
|
111
179
|
|
|
112
|
-
## Disclaimer & risk note
|
|
180
|
+
## ⚠️ Disclaimer & risk note
|
|
113
181
|
|
|
114
|
-
This plugin spawns the **official, unmodified** `agy` binary and consumes
|
|
115
|
-
its public print-mode output. It does not decode internal databases or
|
|
116
|
-
touch OAuth tokens. Automating a subscription CLI may still conflict with
|
|
117
|
-
the provider's terms of service (Google Antigravity ToS §6 context); use
|
|
118
|
-
it at your own risk, and stop if asked to.
|
|
182
|
+
This plugin spawns the **official, unmodified** `agy` binary and consumes its public print-mode output. It does not decode internal databases or touch OAuth tokens. Automating a subscription CLI may still conflict with the provider's terms of service (Google Antigravity ToS §6 context); use it at your own risk, and stop if asked to.
|
|
119
183
|
|
|
120
184
|
## License
|
|
121
185
|
|
|
122
186
|
MIT
|
|
123
|
-
|
|
124
|
-
[DeepSeek Harness (DSH)]: https://npmjs.com/package/@deepseek-ai/dsh
|
package/dist/bridge.mjs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dsh-agy-link MCP bridge script: a minimal stdio MCP server with zero
|
|
3
|
+
// dependencies. agy launches it per the workspace .mcp.json; it forwards
|
|
4
|
+
// tools/list and tools/call to the plugin loopback endpoint whose URL and
|
|
5
|
+
// token arrive via env. Protocol: newline-delimited JSON-RPC 2.0.
|
|
6
|
+
import { request as httpRequest } from 'node:http'
|
|
7
|
+
import process from 'node:process'
|
|
8
|
+
import { createInterface } from 'node:readline'
|
|
9
|
+
|
|
10
|
+
const URL_BASE = process.env.DSH_MCP_URL || ''
|
|
11
|
+
const TOKEN = process.env.DSH_MCP_TOKEN || ''
|
|
12
|
+
const PROTOCOL = '2024-11-05'
|
|
13
|
+
|
|
14
|
+
function post(path, body) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
if (URL_BASE === '' || TOKEN === '') {
|
|
17
|
+
reject(new Error('bridge not configured (missing DSH_MCP_URL/DSH_MCP_TOKEN)'))
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const data = JSON.stringify(body)
|
|
21
|
+
const u = new URL(URL_BASE)
|
|
22
|
+
const req = httpRequest({
|
|
23
|
+
hostname: u.hostname,
|
|
24
|
+
port: u.port || 80,
|
|
25
|
+
method: 'POST',
|
|
26
|
+
path,
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'Content-Length': Buffer.byteLength(data),
|
|
30
|
+
Authorization: 'Bearer ' + TOKEN,
|
|
31
|
+
},
|
|
32
|
+
timeout: 600000,
|
|
33
|
+
});
|
|
34
|
+
req.on('response', (res) => {
|
|
35
|
+
const chunks = []
|
|
36
|
+
res.on('data', (c) => chunks.push(c))
|
|
37
|
+
res.on('end', () => {
|
|
38
|
+
try {
|
|
39
|
+
resolve({ status: res.statusCode, body: JSON.parse(Buffer.concat(chunks).toString('utf8')) })
|
|
40
|
+
} catch (e) {
|
|
41
|
+
reject(e)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
});
|
|
45
|
+
req.on('timeout', () => req.destroy(new Error('bridge request timed out')))
|
|
46
|
+
req.on('error', reject)
|
|
47
|
+
req.end(data)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let toolCache = null;
|
|
52
|
+
|
|
53
|
+
async function listTools() {
|
|
54
|
+
if (toolCache) return toolCache;
|
|
55
|
+
const res = await post('/tools', {})
|
|
56
|
+
if (res.status !== 200) throw new Error('bridge /tools failed: ' + JSON.stringify(res.body))
|
|
57
|
+
toolCache = (res.body.tools || []).map((t) => ({
|
|
58
|
+
name: t.name,
|
|
59
|
+
description: t.description || t.dshName,
|
|
60
|
+
inputSchema: t.inputSchema || { type: 'object' },
|
|
61
|
+
_dshName: t.dshName,
|
|
62
|
+
}))
|
|
63
|
+
return toolCache;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function send(msg) {
|
|
67
|
+
process.stdout.write(JSON.stringify(msg) + '\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function handle(id, method, params) {
|
|
71
|
+
if (method === 'initialize') {
|
|
72
|
+
send({ jsonrpc: '2.0', id, result: { protocolVersion: PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: 'dsh-agy-link-bridge', version: '0.2.0' } } })
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (method.startsWith('notifications/')) return;
|
|
76
|
+
if (method === 'tools/list') {
|
|
77
|
+
const tools = await listTools()
|
|
78
|
+
send({ jsonrpc: '2.0', id, result: { tools: tools.map(({ _dshName, ...t }) => t) } })
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (method === 'tools/call') {
|
|
82
|
+
const name = params && params.name
|
|
83
|
+
const args = (params && params.arguments) || {}
|
|
84
|
+
const tools = await listTools()
|
|
85
|
+
const hit = tools.find((t) => t.name === name)
|
|
86
|
+
if (!hit) {
|
|
87
|
+
send({ jsonrpc: '2.0', id, error: { code: -32602, message: 'unknown tool: ' + name } })
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const res = await post('/call', { dshName: hit._dshName, arguments: args })
|
|
92
|
+
if (res.status !== 200 || res.body.ok !== true) {
|
|
93
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: 'ERROR: ' + JSON.stringify(res.body) }], isError: true } })
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: res.body.text || '' }] } })
|
|
97
|
+
} catch (e) {
|
|
98
|
+
send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: 'ERROR: ' + String(e) }], isError: true } })
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (method === 'ping') {
|
|
103
|
+
send({ jsonrpc: '2.0', id, result: {} })
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found: ' + method } })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const rl = createInterface({ input: process.stdin, terminal: false })
|
|
110
|
+
rl.on('line', (line) => {
|
|
111
|
+
const text = line.trim()
|
|
112
|
+
if (text === '') return
|
|
113
|
+
let msg;
|
|
114
|
+
try {
|
|
115
|
+
msg = JSON.parse(text)
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!msg || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') return
|
|
120
|
+
if (msg.id === undefined) {
|
|
121
|
+
void handle(null, msg.method, msg.params).catch(() => {})
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
void handle(msg.id, msg.method, msg.params).catch((e) => {
|
|
125
|
+
send({ jsonrpc: '2.0', id: msg.id, error: { code: -32603, message: String(e) } })
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
process.stdin.on('end', () => process.exit(0))
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { delimiter, dirname, join } from "node:path";
|
|
2
|
-
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
2
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
6
|
import { execFileSync, spawn } from "node:child_process";
|
|
6
7
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
import { randomBytes } from "node:crypto";
|
|
7
10
|
//#region src/common/types.ts
|
|
8
11
|
const PROVIDER_ID = "antigravity";
|
|
9
12
|
const PLUGIN_ID = "agy-link";
|
|
@@ -46,7 +49,13 @@ function defaultConfig() {
|
|
|
46
49
|
compactionMaxChars: 8e5,
|
|
47
50
|
workspaceRoot: "",
|
|
48
51
|
fallbackModels: DEFAULT_FALLBACK_MODELS,
|
|
49
|
-
askTool: false
|
|
52
|
+
askTool: false,
|
|
53
|
+
mediaDir: "",
|
|
54
|
+
mediaTtlMs: 864e5,
|
|
55
|
+
mediaMaxBytes: 10485760,
|
|
56
|
+
mediaMaxImages: 8,
|
|
57
|
+
mcpBridge: false,
|
|
58
|
+
mcpToolAllowlist: ""
|
|
50
59
|
};
|
|
51
60
|
}
|
|
52
61
|
const Err = {
|
|
@@ -138,7 +147,13 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
138
147
|
compactionMaxChars: asNum(get("compactionMaxChars")) ?? base.compactionMaxChars,
|
|
139
148
|
workspaceRoot: asString(get("workspaceRoot")) ?? base.workspaceRoot,
|
|
140
149
|
fallbackModels: Array.isArray(get("fallbackModels")) ? get("fallbackModels").filter((x) => !!x && typeof x === "object" && typeof x.id === "string") : base.fallbackModels,
|
|
141
|
-
askTool: asBool(get("askTool")) ?? base.askTool
|
|
150
|
+
askTool: asBool(get("askTool")) ?? base.askTool,
|
|
151
|
+
mediaDir: asString(get("mediaDir")) ?? base.mediaDir,
|
|
152
|
+
mediaTtlMs: asNum(get("mediaTtlMs")) ?? base.mediaTtlMs,
|
|
153
|
+
mediaMaxBytes: asNum(get("mediaMaxBytes")) ?? base.mediaMaxBytes,
|
|
154
|
+
mediaMaxImages: asNum(get("mediaMaxImages")) ?? base.mediaMaxImages,
|
|
155
|
+
mcpBridge: asBool(get("mcpBridge")) ?? base.mcpBridge,
|
|
156
|
+
mcpToolAllowlist: asString(get("mcpToolAllowlist")) ?? base.mcpToolAllowlist
|
|
142
157
|
};
|
|
143
158
|
if (env.DSH_AGY_ENABLED !== void 0) cfg.enabled = asBool(env.DSH_AGY_ENABLED) ?? cfg.enabled;
|
|
144
159
|
if (env.DSH_AGY_BIN) cfg.agyBin = env.DSH_AGY_BIN;
|
|
@@ -157,6 +172,16 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
157
172
|
if (t && t > 0) cfg.timeoutMs = t;
|
|
158
173
|
}
|
|
159
174
|
if (env.DSH_AGY_EXTRA_ARGS) cfg.extraArgs = env.DSH_AGY_EXTRA_ARGS.split(/\s+/).filter(Boolean);
|
|
175
|
+
if (env.DSH_AGY_MEDIA_DIR) cfg.mediaDir = env.DSH_AGY_MEDIA_DIR;
|
|
176
|
+
if (env.DSH_AGY_MEDIA_TTL_MS) {
|
|
177
|
+
const t = asNum(env.DSH_AGY_MEDIA_TTL_MS);
|
|
178
|
+
if (t && t > 0) cfg.mediaTtlMs = t;
|
|
179
|
+
}
|
|
180
|
+
if (env.DSH_AGY_MCP_BRIDGE !== void 0) {
|
|
181
|
+
const b = asBool(env.DSH_AGY_MCP_BRIDGE);
|
|
182
|
+
if (b !== void 0) cfg.mcpBridge = b;
|
|
183
|
+
}
|
|
184
|
+
if (env.DSH_AGY_MCP_TOOL_ALLOWLIST) cfg.mcpToolAllowlist = env.DSH_AGY_MCP_TOOL_ALLOWLIST;
|
|
160
185
|
return cfg;
|
|
161
186
|
}
|
|
162
187
|
//#endregion
|
|
@@ -892,6 +917,104 @@ var StreamJsonParser = class {
|
|
|
892
917
|
return ev;
|
|
893
918
|
}
|
|
894
919
|
};
|
|
920
|
+
//#endregion
|
|
921
|
+
//#region src/host/media.ts
|
|
922
|
+
const EXT = {
|
|
923
|
+
"image/png": "png",
|
|
924
|
+
"image/jpeg": "jpg",
|
|
925
|
+
"image/webp": "webp",
|
|
926
|
+
"image/gif": "gif"
|
|
927
|
+
};
|
|
928
|
+
/** Deterministic staged path: same session+turn+index reuses the file. */
|
|
929
|
+
function stagedPath(dir, key, index, mediaType) {
|
|
930
|
+
const ext = EXT[mediaType] ?? "png";
|
|
931
|
+
return join(dir, key + "-" + String(index) + "." + ext);
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Stage up to `maxImages` images into `dir`, returning prompt lines that
|
|
935
|
+
* reference them by absolute path. Deterministic names mean a retried or
|
|
936
|
+
* replayed turn overwrites the same file instead of accumulating copies.
|
|
937
|
+
*/
|
|
938
|
+
async function stageImages(opts) {
|
|
939
|
+
const staged = [];
|
|
940
|
+
const lines = [];
|
|
941
|
+
let skipped = 0;
|
|
942
|
+
const budget = Math.max(0, opts.maxImages);
|
|
943
|
+
if (budget > 0 && opts.images.length > 0) {
|
|
944
|
+
await mkdir(opts.dir, { recursive: true });
|
|
945
|
+
let i = 0;
|
|
946
|
+
for (const ref of opts.images) {
|
|
947
|
+
if (staged.length >= budget) {
|
|
948
|
+
skipped += opts.images.length - staged.length - skipped;
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
const label = ref.name ?? ref.attachmentId;
|
|
952
|
+
if (ref.bytes > opts.maxBytes) {
|
|
953
|
+
skipped++;
|
|
954
|
+
lines.push("[image skipped: \"" + label + "\" exceeds mediaMaxBytes]");
|
|
955
|
+
i++;
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
let data = null;
|
|
959
|
+
try {
|
|
960
|
+
data = await opts.readImage(ref);
|
|
961
|
+
} catch {
|
|
962
|
+
data = null;
|
|
963
|
+
}
|
|
964
|
+
if (!data) {
|
|
965
|
+
skipped++;
|
|
966
|
+
lines.push("[image unavailable: \"" + label + "\" could not be read from attachment storage]");
|
|
967
|
+
i++;
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const path = stagedPath(opts.dir, opts.key, i, ref.mediaType);
|
|
971
|
+
await writeFile(path, data);
|
|
972
|
+
staged.push({
|
|
973
|
+
path,
|
|
974
|
+
name: label,
|
|
975
|
+
width: ref.width,
|
|
976
|
+
height: ref.height,
|
|
977
|
+
bytes: ref.bytes
|
|
978
|
+
});
|
|
979
|
+
lines.push("[image attached: \"" + label + "\" — view it with your file/vision tools at " + path + " (" + ref.width + "x" + ref.height + ", " + ref.bytes + " bytes)]");
|
|
980
|
+
i++;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return {
|
|
984
|
+
promptSuffix: lines.join("\n"),
|
|
985
|
+
staged,
|
|
986
|
+
skipped
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Delete staged files older than `ttlMs`. Returns the removed count.
|
|
991
|
+
* Never throws: a failed sweep logs nothing and retries on the next pass.
|
|
992
|
+
*/
|
|
993
|
+
async function sweepDir(dir, ttlMs, now = Date.now()) {
|
|
994
|
+
if (ttlMs <= 0) return 0;
|
|
995
|
+
let removed = 0;
|
|
996
|
+
let entries;
|
|
997
|
+
try {
|
|
998
|
+
entries = await readdir(dir);
|
|
999
|
+
} catch {
|
|
1000
|
+
return 0;
|
|
1001
|
+
}
|
|
1002
|
+
for (const name of entries) {
|
|
1003
|
+
const full = join(dir, name);
|
|
1004
|
+
try {
|
|
1005
|
+
const st = await stat(full);
|
|
1006
|
+
if (st.isFile() && now - st.mtimeMs > ttlMs) {
|
|
1007
|
+
await unlink(full);
|
|
1008
|
+
removed++;
|
|
1009
|
+
}
|
|
1010
|
+
} catch {}
|
|
1011
|
+
}
|
|
1012
|
+
return removed;
|
|
1013
|
+
}
|
|
1014
|
+
/** Default media directory: <dsh home>/agy-link/media. */
|
|
1015
|
+
function defaultMediaDir(stateDir) {
|
|
1016
|
+
return join(stateDir, "media");
|
|
1017
|
+
}
|
|
895
1018
|
function resolveAgyBin(cfg) {
|
|
896
1019
|
const candidates = [];
|
|
897
1020
|
if (cfg.agyBin !== "") candidates.push(cfg.agyBin);
|
|
@@ -1163,6 +1286,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
1163
1286
|
if (opts.model !== "") args.push("--model", opts.model);
|
|
1164
1287
|
if (opts.effort && opts.effort !== "") args.push("--effort", opts.effort);
|
|
1165
1288
|
if (opts.conversationId) args.push("--conversation", opts.conversationId);
|
|
1289
|
+
for (const d of opts.addDirs ?? []) args.push("--add-dir", d);
|
|
1166
1290
|
args.push(...opts.extraArgs);
|
|
1167
1291
|
args.push("-p", opts.prompt);
|
|
1168
1292
|
return args;
|
|
@@ -1219,8 +1343,29 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
1219
1343
|
if (span.some((m) => m.role === "assistant")) prompt = buildDigest(span, 0, cfg.digestMaxChars) + prompt;
|
|
1220
1344
|
}
|
|
1221
1345
|
}
|
|
1222
|
-
if (prompt.trim() === "") throw new LlmError("request carries no user text to forward to agy", Err.AGY_ERROR);
|
|
1223
1346
|
if (cfg.forwardSystemPrompt && options.system) prompt = "System instructions:\n" + options.system + "\n\n" + prompt;
|
|
1347
|
+
let stagedDirs = [];
|
|
1348
|
+
if (!isAux) {
|
|
1349
|
+
const imageRefs = [];
|
|
1350
|
+
for (const m of trailingUser) for (const b of m.content ?? []) {
|
|
1351
|
+
const blk = b;
|
|
1352
|
+
if (blk && blk.type === "image" && blk.attachment) imageRefs.push(blk.attachment);
|
|
1353
|
+
}
|
|
1354
|
+
if (imageRefs.length > 0 && this.deps.readImage) {
|
|
1355
|
+
const dir = cfg.mediaDir !== "" ? cfg.mediaDir : defaultMediaDir(stateDir());
|
|
1356
|
+
const res = await stageImages({
|
|
1357
|
+
dir,
|
|
1358
|
+
key: (sessionKey !== "" ? sessionKey.replace(/[^a-zA-Z0-9_-]+/g, "_") : "anon") + "-" + messages.length,
|
|
1359
|
+
images: imageRefs,
|
|
1360
|
+
readImage: this.deps.readImage,
|
|
1361
|
+
maxImages: cfg.mediaMaxImages,
|
|
1362
|
+
maxBytes: cfg.mediaMaxBytes
|
|
1363
|
+
});
|
|
1364
|
+
if (res.promptSuffix !== "") prompt = prompt === "" ? res.promptSuffix : prompt + "\n\n" + res.promptSuffix;
|
|
1365
|
+
if (res.staged.length > 0) stagedDirs = [dir];
|
|
1366
|
+
}
|
|
1367
|
+
if (prompt.trim() === "") throw new LlmError("request carries no user text or images to forward to agy", Err.AGY_ERROR);
|
|
1368
|
+
} else if (prompt.trim() === "") throw new LlmError("request carries no user text to forward to agy", Err.AGY_ERROR);
|
|
1224
1369
|
const before = snapshotConversations();
|
|
1225
1370
|
const mapper = new EventMapper({ toolOutput: this.deps.toolOutput });
|
|
1226
1371
|
const parser = new StreamJsonParser();
|
|
@@ -1234,7 +1379,8 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
1234
1379
|
conversationId: !isAux && binding !== void 0 ? binding.conversationId : void 0,
|
|
1235
1380
|
permissionMode: isAux ? "plan" : cfg.permissionMode,
|
|
1236
1381
|
timeoutMs: cfg.timeoutMs,
|
|
1237
|
-
extraArgs: cfg.extraArgs
|
|
1382
|
+
extraArgs: cfg.extraArgs,
|
|
1383
|
+
addDirs: stagedDirs
|
|
1238
1384
|
});
|
|
1239
1385
|
const release = await this.deps.acquire();
|
|
1240
1386
|
let released = false;
|
|
@@ -1327,6 +1473,59 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
1327
1473
|
};
|
|
1328
1474
|
//#endregion
|
|
1329
1475
|
//#region src/host/oneshot.ts
|
|
1476
|
+
/** Per-file inline cap; larger files are truncated. */
|
|
1477
|
+
const INLINE_MAX_BYTES = 262144;
|
|
1478
|
+
/** Text-detection: anything that is very likely not binary. */
|
|
1479
|
+
function looksTextual(head) {
|
|
1480
|
+
if (head === "") return true;
|
|
1481
|
+
let suspicious = 0;
|
|
1482
|
+
const n = Math.min(head.length, 2e3);
|
|
1483
|
+
for (let i = 0; i < n; i++) {
|
|
1484
|
+
const c = head.charCodeAt(i);
|
|
1485
|
+
if (c === 0 || c < 9 && c !== 0 || c > 13 && c < 32) suspicious++;
|
|
1486
|
+
}
|
|
1487
|
+
return suspicious / n < .05;
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Inline text files into the prompt (v0.2): each readable textual file
|
|
1491
|
+
* becomes a fenced section. Paths resolve against the oneshot cwd;
|
|
1492
|
+
* absolute paths pass through.
|
|
1493
|
+
*/
|
|
1494
|
+
async function inlineFiles(prompt, paths, cwd) {
|
|
1495
|
+
if (paths.length === 0) return prompt;
|
|
1496
|
+
const sections = [];
|
|
1497
|
+
for (const raw of paths) {
|
|
1498
|
+
const p = isAbsolute(raw) ? raw : resolve(cwd, raw);
|
|
1499
|
+
let note = "";
|
|
1500
|
+
let body = "";
|
|
1501
|
+
try {
|
|
1502
|
+
const buf = await readFile(p);
|
|
1503
|
+
if (!looksTextual(buf.subarray(0, Math.min(buf.length, 4096)).toString("utf8"))) note = "(skipped: binary file - reference it by path and let agy read it with its own tools)";
|
|
1504
|
+
else if (buf.byteLength > INLINE_MAX_BYTES) {
|
|
1505
|
+
note = "(truncated to the first 262144 bytes)";
|
|
1506
|
+
body = buf.subarray(0, INLINE_MAX_BYTES).toString("utf8");
|
|
1507
|
+
} else body = buf.toString("utf8");
|
|
1508
|
+
} catch {
|
|
1509
|
+
note = "(skipped: unreadable - " + raw + ")";
|
|
1510
|
+
}
|
|
1511
|
+
sections.push("--- file: " + p + " " + note + " ---\n" + body);
|
|
1512
|
+
}
|
|
1513
|
+
return prompt + "\n\n" + sections.join("\n\n");
|
|
1514
|
+
}
|
|
1515
|
+
/** Write a JSON schema to a temp file; returns the --json-schema argv tail. */
|
|
1516
|
+
async function schemaArgs(schema) {
|
|
1517
|
+
const text = JSON.stringify(schema);
|
|
1518
|
+
const dir = await mkdtemp(join(tmpdir(), "agy-schema-"));
|
|
1519
|
+
const file = join(dir, "schema.json");
|
|
1520
|
+
await writeFile(file, text, "utf8");
|
|
1521
|
+
return {
|
|
1522
|
+
args: ["--json-schema", file],
|
|
1523
|
+
cleanup: () => rm(dir, {
|
|
1524
|
+
recursive: true,
|
|
1525
|
+
force: true
|
|
1526
|
+
})
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1330
1529
|
async function runAgyOnce(deps, req) {
|
|
1331
1530
|
const cfg = deps.cfg();
|
|
1332
1531
|
const bin = deps.bin();
|
|
@@ -1349,7 +1548,15 @@ async function runAgyOnce(deps, req) {
|
|
|
1349
1548
|
else args.push("--mode", mode);
|
|
1350
1549
|
if (req.model) args.push("--model", req.model);
|
|
1351
1550
|
if (req.effort) args.push("--effort", req.effort);
|
|
1352
|
-
|
|
1551
|
+
let prompt = req.prompt;
|
|
1552
|
+
if (req.readPaths && req.readPaths.length > 0) prompt = await inlineFiles(prompt, req.readPaths, cfg.workspaceRoot !== "" ? cfg.workspaceRoot : process.cwd());
|
|
1553
|
+
let cleanup = null;
|
|
1554
|
+
if (req.schema !== void 0 && req.schema !== null) {
|
|
1555
|
+
const sa = await schemaArgs(req.schema);
|
|
1556
|
+
args.push(...sa.args);
|
|
1557
|
+
cleanup = sa.cleanup;
|
|
1558
|
+
}
|
|
1559
|
+
args.push("-p", prompt);
|
|
1353
1560
|
const parser = new StreamJsonParser();
|
|
1354
1561
|
const textParts = [];
|
|
1355
1562
|
let resultText = "";
|
|
@@ -1371,6 +1578,7 @@ async function runAgyOnce(deps, req) {
|
|
|
1371
1578
|
}
|
|
1372
1579
|
}
|
|
1373
1580
|
}).outcome;
|
|
1581
|
+
if (cleanup) cleanup().catch(() => {});
|
|
1374
1582
|
for (const ev of parser.flush()) if (ev.kind === "result") {
|
|
1375
1583
|
if (ev.conversationId !== "") conversationId = ev.conversationId;
|
|
1376
1584
|
resultText = ev.response;
|
|
@@ -1484,6 +1692,14 @@ function defineAgyAskTool(deps) {
|
|
|
1484
1692
|
timeoutMinutes: {
|
|
1485
1693
|
type: "number",
|
|
1486
1694
|
description: "Optional timeout budget in minutes (default 10)."
|
|
1695
|
+
},
|
|
1696
|
+
readPaths: {
|
|
1697
|
+
type: "string",
|
|
1698
|
+
description: "Optional space/comma-separated text files to inline into the prompt (workspace-relative or absolute; binaries are skipped with a note)."
|
|
1699
|
+
},
|
|
1700
|
+
schema: {
|
|
1701
|
+
type: "string",
|
|
1702
|
+
description: "Optional JSON Schema (as a JSON string) enforcing the final answer via agy --json-schema."
|
|
1487
1703
|
}
|
|
1488
1704
|
},
|
|
1489
1705
|
output: {
|
|
@@ -1498,13 +1714,22 @@ function defineAgyAskTool(deps) {
|
|
|
1498
1714
|
exec.signal;
|
|
1499
1715
|
const cfg = deps.cfg();
|
|
1500
1716
|
const model = resolveAskModel(args.model ?? "", deps.catalog(), cfg.defaultModel);
|
|
1717
|
+
let parsedSchema;
|
|
1718
|
+
if (typeof args.schema === "string" && args.schema.trim() !== "") try {
|
|
1719
|
+
parsedSchema = JSON.parse(args.schema);
|
|
1720
|
+
} catch (e) {
|
|
1721
|
+
throw new Error("agy_ask: schema is not valid JSON: " + String(e));
|
|
1722
|
+
}
|
|
1723
|
+
const readPaths = typeof args.readPaths === "string" && args.readPaths.trim() !== "" ? args.readPaths.split(/[ ,]+/).filter(Boolean) : [];
|
|
1501
1724
|
const res = await runAgyOnce(deps, {
|
|
1502
1725
|
prompt: args.prompt,
|
|
1503
1726
|
model: model === "" ? void 0 : model,
|
|
1504
1727
|
effort: args.effort,
|
|
1505
1728
|
mode: args.mode,
|
|
1506
1729
|
timeoutMs: args.timeoutMinutes ? args.timeoutMinutes * 6e4 : void 0,
|
|
1507
|
-
signal: exec.signal
|
|
1730
|
+
signal: exec.signal,
|
|
1731
|
+
readPaths,
|
|
1732
|
+
schema: parsedSchema
|
|
1508
1733
|
});
|
|
1509
1734
|
if (!res.ok) throw new Error("agy_ask failed: " + (res.error ?? "unknown error"));
|
|
1510
1735
|
const footer = res.conversationId ? "\n\n(agy conversation: " + res.conversationId + " — continue it with: agy --conversation " + res.conversationId + ")" : "";
|
|
@@ -1958,6 +2183,193 @@ function require$$basename(p) {
|
|
|
1958
2183
|
return i >= 0 ? p.slice(i + 1) : p;
|
|
1959
2184
|
}
|
|
1960
2185
|
//#endregion
|
|
2186
|
+
//#region src/host/mcp-bridge.ts
|
|
2187
|
+
const MCP_SERVER_KEY = "dsh-tools";
|
|
2188
|
+
/** MCP tool names are [a-zA-Z0-9_-]; DSH names may contain dots. */
|
|
2189
|
+
function toMcpName(name) {
|
|
2190
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2191
|
+
}
|
|
2192
|
+
function readBody(req) {
|
|
2193
|
+
return new Promise((resolve) => {
|
|
2194
|
+
const chunks = [];
|
|
2195
|
+
req.on("data", (c) => chunks.push(c));
|
|
2196
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
2197
|
+
req.on("error", () => resolve(""));
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
function sendJson(res, status, body) {
|
|
2201
|
+
const text = JSON.stringify(body);
|
|
2202
|
+
res.writeHead(status, {
|
|
2203
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
2204
|
+
"Content-Length": Buffer.byteLength(text)
|
|
2205
|
+
});
|
|
2206
|
+
res.end(text);
|
|
2207
|
+
}
|
|
2208
|
+
/** Best-effort extraction of readable text from an execution result. */
|
|
2209
|
+
function resultText(result) {
|
|
2210
|
+
if (result === null || result === void 0) return "";
|
|
2211
|
+
const r = result;
|
|
2212
|
+
if (Array.isArray(r.content)) {
|
|
2213
|
+
const parts = [];
|
|
2214
|
+
for (const b of r.content) {
|
|
2215
|
+
const blk = b;
|
|
2216
|
+
if (blk && blk.type === "text" && typeof blk.text === "string") parts.push(blk.text);
|
|
2217
|
+
}
|
|
2218
|
+
if (parts.length > 0) return parts.join("\n");
|
|
2219
|
+
}
|
|
2220
|
+
if (typeof r.text === "string") return r.text;
|
|
2221
|
+
if (typeof r.output === "string") return r.output;
|
|
2222
|
+
try {
|
|
2223
|
+
return JSON.stringify(result, null, 2);
|
|
2224
|
+
} catch {
|
|
2225
|
+
return String(result);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Start the loopback endpoint. Resolves once listening. The tools service
|
|
2230
|
+
* may arrive later (optional service): pass a thunk.
|
|
2231
|
+
*/
|
|
2232
|
+
function startMcpBridge(opts) {
|
|
2233
|
+
const token = randomBytes(24).toString("hex");
|
|
2234
|
+
let callSeq = 0;
|
|
2235
|
+
const server = createServer((req, res) => {
|
|
2236
|
+
(async () => {
|
|
2237
|
+
const url = (req.url ?? "").split("?")[0];
|
|
2238
|
+
if (String(req.headers["authorization"] ?? "") !== "Bearer " + token) {
|
|
2239
|
+
sendJson(res, 401, { error: "unauthorized" });
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
if ((req.method === "GET" || req.method === "POST") && (url === "/tools" || url === "/mcp/tools")) {
|
|
2243
|
+
const svc = opts.tools();
|
|
2244
|
+
if (!svc) {
|
|
2245
|
+
sendJson(res, 503, { error: "tools service unavailable" });
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
const allow = opts.allowlist().split(",").map((s) => s.trim()).filter(Boolean);
|
|
2249
|
+
const allowSet = new Set(allow);
|
|
2250
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2251
|
+
sendJson(res, 200, { tools: svc.schemas().filter((t) => allow.length === 0 || allowSet.has(t.name)).filter((t) => {
|
|
2252
|
+
if (t.name === "run_code" || t.name === "agy_ask") return false;
|
|
2253
|
+
const mapped = toMcpName(t.name);
|
|
2254
|
+
if (seen.has(mapped)) return false;
|
|
2255
|
+
seen.add(mapped);
|
|
2256
|
+
return true;
|
|
2257
|
+
}).map((t) => ({
|
|
2258
|
+
name: toMcpName(t.name),
|
|
2259
|
+
dshName: t.name,
|
|
2260
|
+
description: t.description,
|
|
2261
|
+
inputSchema: {
|
|
2262
|
+
type: "object",
|
|
2263
|
+
...t.parameters
|
|
2264
|
+
}
|
|
2265
|
+
})) });
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
if (req.method === "POST" && (url === "/call" || url === "/mcp/call")) {
|
|
2269
|
+
const body = await readBody(req);
|
|
2270
|
+
let parsed = {};
|
|
2271
|
+
try {
|
|
2272
|
+
parsed = JSON.parse(body);
|
|
2273
|
+
} catch {
|
|
2274
|
+
sendJson(res, 400, { error: "invalid JSON" });
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
const svc = opts.tools();
|
|
2278
|
+
if (!svc) {
|
|
2279
|
+
sendJson(res, 503, { error: "tools service unavailable" });
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
const dshName = typeof parsed.dshName === "string" ? parsed.dshName : "";
|
|
2283
|
+
if (dshName === "" || dshName === "run_code" || dshName === "agy_ask") {
|
|
2284
|
+
sendJson(res, 400, { error: "bad tool name" });
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
callSeq++;
|
|
2288
|
+
try {
|
|
2289
|
+
sendJson(res, 200, {
|
|
2290
|
+
ok: true,
|
|
2291
|
+
text: resultText(await svc.execute({
|
|
2292
|
+
callId: "agy-mcp-" + callSeq,
|
|
2293
|
+
name: dshName,
|
|
2294
|
+
arguments: parsed.arguments ?? {}
|
|
2295
|
+
}))
|
|
2296
|
+
});
|
|
2297
|
+
} catch (e) {
|
|
2298
|
+
sendJson(res, 200, {
|
|
2299
|
+
ok: false,
|
|
2300
|
+
error: String(e)
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
sendJson(res, 404, { error: "not found" });
|
|
2306
|
+
})().catch(() => {
|
|
2307
|
+
try {
|
|
2308
|
+
sendJson(res, 500, { error: "internal" });
|
|
2309
|
+
} catch {}
|
|
2310
|
+
});
|
|
2311
|
+
});
|
|
2312
|
+
return new Promise((resolve, reject) => {
|
|
2313
|
+
server.once("error", reject);
|
|
2314
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2315
|
+
const addr = server.address();
|
|
2316
|
+
const port = typeof addr === "object" && addr !== null ? addr.port : 0;
|
|
2317
|
+
opts.log?.("mcp bridge listening on 127.0.0.1:" + port);
|
|
2318
|
+
resolve({
|
|
2319
|
+
bridgeScript: opts.bridgeScript,
|
|
2320
|
+
token,
|
|
2321
|
+
url: "http://127.0.0.1:" + port,
|
|
2322
|
+
port,
|
|
2323
|
+
close: () => new Promise((done) => server.close(() => done()))
|
|
2324
|
+
});
|
|
2325
|
+
});
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Merge our server entry into the workspace .mcp.json. Returns a restore
|
|
2330
|
+
* function that puts the previous content back (or deletes the file we
|
|
2331
|
+
* created). Never throws.
|
|
2332
|
+
*/
|
|
2333
|
+
function writeMcpConfig(workspaceRoot, bridge) {
|
|
2334
|
+
const file = join(workspaceRoot, ".mcp.json");
|
|
2335
|
+
let previous = null;
|
|
2336
|
+
try {
|
|
2337
|
+
if (existsSync(file)) previous = readFileSync(file, "utf8");
|
|
2338
|
+
} catch {
|
|
2339
|
+
previous = null;
|
|
2340
|
+
}
|
|
2341
|
+
let root = {};
|
|
2342
|
+
if (previous !== null) try {
|
|
2343
|
+
const v = JSON.parse(previous);
|
|
2344
|
+
if (v && typeof v === "object") root = v;
|
|
2345
|
+
} catch {
|
|
2346
|
+
root = {};
|
|
2347
|
+
}
|
|
2348
|
+
const servers = root.mcpServers && typeof root.mcpServers === "object" ? root.mcpServers : {};
|
|
2349
|
+
servers[MCP_SERVER_KEY] = {
|
|
2350
|
+
type: "stdio",
|
|
2351
|
+
command: process.execPath,
|
|
2352
|
+
args: [bridge.bridgeScript],
|
|
2353
|
+
env: {
|
|
2354
|
+
DSH_MCP_URL: bridge.url,
|
|
2355
|
+
DSH_MCP_TOKEN: bridge.token
|
|
2356
|
+
}
|
|
2357
|
+
};
|
|
2358
|
+
root.mcpServers = servers;
|
|
2359
|
+
try {
|
|
2360
|
+
writeFileSync(file, JSON.stringify(root, null, 2) + "\n", "utf8");
|
|
2361
|
+
} catch {
|
|
2362
|
+
return () => {};
|
|
2363
|
+
}
|
|
2364
|
+
return () => {
|
|
2365
|
+
try {
|
|
2366
|
+
if (previous === null) {
|
|
2367
|
+
if (existsSync(file)) unlinkSync(file);
|
|
2368
|
+
} else writeFileSync(file, previous, "utf8");
|
|
2369
|
+
} catch {}
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
//#endregion
|
|
1961
2373
|
//#region src/index.ts
|
|
1962
2374
|
const name = "dsh-agy-link";
|
|
1963
2375
|
const inject = ["llm", "commands"];
|
|
@@ -2036,6 +2448,15 @@ function apply(ctx, entryConfig = {}) {
|
|
|
2036
2448
|
},
|
|
2037
2449
|
onParser: (p) => {
|
|
2038
2450
|
lastParser = p;
|
|
2451
|
+
},
|
|
2452
|
+
readImage: async (ref) => {
|
|
2453
|
+
const svc = ctx.attachments;
|
|
2454
|
+
if (!svc || typeof svc.readImage !== "function") return null;
|
|
2455
|
+
try {
|
|
2456
|
+
return (await svc.readImage(ref))?.data ?? null;
|
|
2457
|
+
} catch {
|
|
2458
|
+
return null;
|
|
2459
|
+
}
|
|
2039
2460
|
}
|
|
2040
2461
|
});
|
|
2041
2462
|
const setOverride = (key, value) => {
|
|
@@ -2275,9 +2696,54 @@ function apply(ctx, entryConfig = {}) {
|
|
|
2275
2696
|
}
|
|
2276
2697
|
});
|
|
2277
2698
|
}
|
|
2699
|
+
const mediaDir = () => {
|
|
2700
|
+
const cfg = getConfig();
|
|
2701
|
+
return cfg.mediaDir !== "" ? cfg.mediaDir : defaultMediaDir(stateDir());
|
|
2702
|
+
};
|
|
2703
|
+
ctx.effect(() => {
|
|
2704
|
+
const timer = setInterval(() => {
|
|
2705
|
+
sweepDir(mediaDir(), getConfig().mediaTtlMs).catch(() => void 0);
|
|
2706
|
+
}, Math.max(6e4, Math.min(getConfig().mediaTtlMs, 36e5)));
|
|
2707
|
+
return () => clearInterval(timer);
|
|
2708
|
+
});
|
|
2709
|
+
const bridgeState = {
|
|
2710
|
+
bridge: null,
|
|
2711
|
+
restore: null
|
|
2712
|
+
};
|
|
2713
|
+
const syncMcpBridge = () => {
|
|
2714
|
+
const cfg = getConfig();
|
|
2715
|
+
const want = cfg.mcpBridge && cfg.enabled;
|
|
2716
|
+
if (want && bridgeState.bridge === null) (async () => {
|
|
2717
|
+
try {
|
|
2718
|
+
const script = new URL("./bridge.mjs", import.meta.url).pathname;
|
|
2719
|
+
const toolsSvc = ctx.get("tools");
|
|
2720
|
+
const bridge = await startMcpBridge({
|
|
2721
|
+
bridgeScript: script,
|
|
2722
|
+
tools: () => ctx.get("tools"),
|
|
2723
|
+
allowlist: () => getConfig().mcpToolAllowlist,
|
|
2724
|
+
log
|
|
2725
|
+
});
|
|
2726
|
+
bridgeState.bridge = bridge;
|
|
2727
|
+
const root = cfg.workspaceRoot !== "" ? cfg.workspaceRoot : process.cwd();
|
|
2728
|
+
bridgeState.restore = writeMcpConfig(root, bridge);
|
|
2729
|
+
log("mcp bridge ready at " + bridge.url + (toolsSvc ? "" : " (tools service not yet available)"));
|
|
2730
|
+
} catch (e) {
|
|
2731
|
+
log("mcp bridge failed to start: " + String(e));
|
|
2732
|
+
}
|
|
2733
|
+
})();
|
|
2734
|
+
else if (!want && bridgeState.bridge !== null) {
|
|
2735
|
+
bridgeState.restore?.();
|
|
2736
|
+
bridgeState.bridge.close();
|
|
2737
|
+
bridgeState.bridge = null;
|
|
2738
|
+
bridgeState.restore = null;
|
|
2739
|
+
}
|
|
2740
|
+
};
|
|
2741
|
+
syncMcpBridge();
|
|
2278
2742
|
ctx.effect(() => {
|
|
2279
2743
|
auth.dispose();
|
|
2280
2744
|
if (askToolDispose.current !== null) askToolDispose.current();
|
|
2745
|
+
bridgeState.restore?.();
|
|
2746
|
+
bridgeState.bridge?.close();
|
|
2281
2747
|
return () => void 0;
|
|
2282
2748
|
});
|
|
2283
2749
|
}
|
package/docs/KNOWN-GAPS.md
CHANGED
|
@@ -4,13 +4,21 @@ Deliberate v1 boundaries and upstream-behavior notes.
|
|
|
4
4
|
|
|
5
5
|
## Not forwarded to agy
|
|
6
6
|
|
|
7
|
-
- **Images
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- **
|
|
13
|
-
|
|
7
|
+
- **Images as true multimodal blocks** - agy print mode (`-p`) has no image
|
|
8
|
+
input flag. Since v0.2 the bridge stages DSH image attachments to a local
|
|
9
|
+
media directory (`mediaDir`, TTL-swept) and references them by absolute
|
|
10
|
+
path in the prompt, granting `--add-dir` so agy can view them with its own
|
|
11
|
+
file/vision tools. Same approach as the pi extension.
|
|
12
|
+
- **DSH tools to agy (reverse MCP bridge)** - available since v0.2 behind the
|
|
13
|
+
`mcpBridge` config flag (experimental): the plugin runs a loopback-only,
|
|
14
|
+
token-guarded HTTP endpoint and registers a zero-dependency stdio MCP
|
|
15
|
+
server (`dsh-tools`) in the workspace `.mcp.json` (merged in, restored on
|
|
16
|
+
disable). `run_code` and `agy_ask` are never bridged; `mcpToolAllowlist`
|
|
17
|
+
restricts the set further.
|
|
18
|
+
- **Structured outputs** - `agy_ask` accepts a `schema` parameter (JSON
|
|
19
|
+
Schema as a JSON string) enforced via `--json-schema` since v0.2. Wiring
|
|
20
|
+
schema enforcement into DSH-native tool-call generation remains future
|
|
21
|
+
work: DSH `GenerateOptions` has no schema field to map onto.
|
|
14
22
|
|
|
15
23
|
## Mapping choices
|
|
16
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy-link",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Google Antigravity (agy CLI) models for DeepSeek Harness — stream Gemini/Claude/GPT-OSS subscriptions into DSH with thinking, tool activity, token usage and in-GUI Google OAuth login.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -75,7 +75,6 @@
|
|
|
75
75
|
"dist",
|
|
76
76
|
"cordis.patch.yml",
|
|
77
77
|
"README.md",
|
|
78
|
-
"README.zh.md",
|
|
79
78
|
"LICENSE",
|
|
80
79
|
"CHANGELOG.md",
|
|
81
80
|
"docs"
|
package/README.zh.md
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# dsh-agy-link(简体中文)
|
|
2
|
-
|
|
3
|
-
把 **Google Antigravity 模型接入 [DeepSeek Harness (DSH)]** —— 由官方
|
|
4
|
-
`agy` CLI 驱动,完整支持对话输出、思考(thinking)、工具活动与 token
|
|
5
|
-
用量,全部走你的 Antigravity 订阅。
|
|
6
|
-
|
|
7
|
-
[English](README.md) | 简体中文
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
> 全网统一昵称:**小斯syzs** · B站 [@小斯syzs](https://space.bilibili.com/390211071) · 抖音 · 小红书 · 快手(全网同名)
|
|
12
|
-
>
|
|
13
|
-
> 💬 **小斯syzs 邀请你加入飞书群** —— [点此一键加入](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=553i2f4a-5cc8-487f-95d3-3c4095bec0d9)
|
|
14
|
-
|
|
15
|
-
> Known online as **小斯syzs** — Bilibili [@小斯syzs](https://space.bilibili.com/390211071) · Douyin · Xiaohongshu · Kuaishou (same handle on every platform).
|
|
16
|
-
>
|
|
17
|
-
> 💬 **Join the Feishu community group** hosted by 小斯syzs — [one-click invite](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=553i2f4a-5cc8-487f-95d3-3c4095bec0d9)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## 你能得到什么
|
|
21
|
-
|
|
22
|
-
- **模型路由而非代理** —— 在 DSH 模型配置里注册 `antigravity` 提供方,
|
|
23
|
-
`/model` 选择器里直接挑任意 Antigravity 模型。
|
|
24
|
-
- **完整流式输出** —— 文本、思考、agy 工具活动(注记为 reasoning 块),
|
|
25
|
-
全部映射到 DSH 原生流协议。
|
|
26
|
-
- **会话连续性** —— 每个 DSH 会话绑定原生 agy 会话(`--conversation`),
|
|
27
|
-
多轮上下文由 agy 自己的历史承载,不重发全量。
|
|
28
|
-
- **token 用量** —— 输入/输出/思考/缓存 token 全部进入 DSH 用量统计。
|
|
29
|
-
- **GUI 内 Google 登录** —— `/agy auth` 打印授权 URL;侧栏面板提供
|
|
30
|
-
二维码 + 授权码粘贴框。
|
|
31
|
-
- **`agy_ask` 工具** —— 任何 DSH 模型都可以把一次性任务委托给
|
|
32
|
-
Antigravity 模型(AskAntigravity 模式)。
|
|
33
|
-
- **`/agy` 命令族** —— `status` / `auth` / `auth-code` / `models` /
|
|
34
|
-
`mode` / `effort` / `clear` / `doctor` / `help`。
|
|
35
|
-
- **休眠安全** —— 没装 agy?没登录?插件照常加载并告诉你怎么修。
|
|
36
|
-
|
|
37
|
-
## 环境要求
|
|
38
|
-
|
|
39
|
-
- DSH(DeepSeek Harness)web profile
|
|
40
|
-
- Node.js >= 24
|
|
41
|
-
- 官方 `agy` CLI >= 1.1.8,已安装并在 `PATH` 上
|
|
42
|
-
|
|
43
|
-
## 安装
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
dsh plugin --profile web add dsh-agy-link
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
重启 DSH,在 GUI 里执行 `/agy status`。
|
|
50
|
-
|
|
51
|
-
## 登录(一次性)
|
|
52
|
-
|
|
53
|
-
1. `/agy auth` —— 打印 Google 授权 URL。
|
|
54
|
-
2. 打开链接、批准访问、复制授权码。
|
|
55
|
-
3. `/agy auth-code <授权码>` —— 或用侧栏面板(二维码 + 粘贴框)。
|
|
56
|
-
|
|
57
|
-
插件绝不读取/复制/移动
|
|
58
|
-
`~/.gemini/antigravity-cli/antigravity-oauth-token`;登录完全通过官方
|
|
59
|
-
CLI 自己的流程完成。
|
|
60
|
-
|
|
61
|
-
## 配置
|
|
62
|
-
|
|
63
|
-
配置在 `agy-link` 插件条目里(`/plugin` 或 profile patch 层编辑)。
|
|
64
|
-
环境变量优先于文件:
|
|
65
|
-
|
|
66
|
-
| 键 | 环境变量 | 默认值 | 含义 |
|
|
67
|
-
| --- | --- | --- | --- |
|
|
68
|
-
| enabled | `DSH_AGY_ENABLED` | `true` | 总开关 |
|
|
69
|
-
| agyBin | `DSH_AGY_BIN` | 自动 | 显式 agy 路径 |
|
|
70
|
-
| permissionMode | `DSH_AGY_MODE` | `skip` | `skip` / `plan` / `accept-edits`(见下) |
|
|
71
|
-
| defaultModel | `DSH_AGY_DEFAULT_MODEL` | `(agy 默认)` | 模型 slug |
|
|
72
|
-
| defaultEffort | `DSH_AGY_DEFAULT_EFFORT` | `(模型默认)` | `low` / `medium` / `high` |
|
|
73
|
-
| timeoutMs | `DSH_AGY_TIMEOUT_MS` | `600000` | 单轮看门狗 |
|
|
74
|
-
| extraArgs | `DSH_AGY_EXTRA_ARGS` | — | 附加 agy 参数(空格分隔) |
|
|
75
|
-
|
|
76
|
-
### 权限模式 —— 必读
|
|
77
|
-
|
|
78
|
-
agy 有**自己的**工具循环,会自己做文件编辑和 shell 执行。桥接层映射为
|
|
79
|
-
三种模式:
|
|
80
|
-
|
|
81
|
-
- **plan** —— 只读,体验桥接的安全默认。
|
|
82
|
-
- **accept-edits** —— agy 可以不经询问改文件。
|
|
83
|
-
- **skip** —— `--dangerously-skip-permissions`:agy **所有**工具免审批。
|
|
84
|
-
非交互 DSH 轮次需要它(或 plan),因为 agy 的权限提示会卡死 print
|
|
85
|
-
模式;但这意味着真实的无人值守 写/执行 权限。设置面板里这个按钮是
|
|
86
|
-
红色的。
|
|
87
|
-
|
|
88
|
-
DSH 侧的工具与权限不受影响 —— 这里只约束 agy 子进程在自己工作区能做什么。
|
|
89
|
-
|
|
90
|
-
## 工作原理
|
|
91
|
-
|
|
92
|
-
一次 DSH 模型调用 = 一个短生命周期 `agy -p --output-format stream-json`
|
|
93
|
-
进程。NDJSON 事件流被解析、归一化并映射为 DSH StreamChunk:思考 →
|
|
94
|
-
reasoning 块,文本 → text 块,工具活动 → 注记 reasoning 块,结果信封 →
|
|
95
|
-
usage + finish。会话 id 优先取自流本身,conversations 目录快照对比作为
|
|
96
|
-
兜底。不逆向数据库、不解码 protobuf、不碰 token 文件。
|
|
97
|
-
|
|
98
|
-
## 已知差距
|
|
99
|
-
|
|
100
|
-
见 [docs/KNOWN-GAPS.md](docs/KNOWN-GAPS.md)。要点:图片不转发(agy print
|
|
101
|
-
模式纯文本);DSH 工具不暴露给 agy(MCP 反向桥接是后续工作)。
|
|
102
|
-
|
|
103
|
-
## 免责与风险提示
|
|
104
|
-
|
|
105
|
-
本插件只启动**官方未修改**的 `agy` 二进制并消费其公开 print 输出,
|
|
106
|
-
不解码内部数据库、不触碰 OAuth token。自动化订阅 CLI 仍可能与提供方
|
|
107
|
-
服务条款冲突(Google Antigravity ToS §6 语境);风险自负,被要求停止时
|
|
108
|
-
请停止。
|
|
109
|
-
|
|
110
|
-
## 许可证
|
|
111
|
-
|
|
112
|
-
MIT
|