dsh-packycode-api 1.1.1 → 1.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/CHANGELOG.md +8 -0
- package/README.en.md +4 -4
- package/README.md +4 -4
- package/lib/index.js +416 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.3
|
|
4
|
+
|
|
5
|
+
- **Plan review when the CLI has no ExitPlanMode.** Print mode often does not offer that tool, so a plan-mode turn wrote `~/.dsh/packycode-claude-cli/plans/*.md` and came back as ordinary text. A plan file written during the run is now submitted through DSH's `exit_plan_mode` when the CLI returns. An older file in that directory is ignored.
|
|
6
|
+
|
|
7
|
+
## 1.1.2
|
|
8
|
+
|
|
9
|
+
- **Claude CLI follows DSH plan mode.** While the current session's plan mode is on (including a selection that applies on this step but is not logged yet), a Claude model turn and the `claude_code` tool run the local CLI with `--permission-mode plan`. That wins over the session permission preset and over an explicit `claudeCliMode` pin. The CLI may explore read-only; other permission prompts are denied, and after eight denials the run stops instead of hanging. `ExitPlanMode` is never allowed inside the CLI process. A submitted plan becomes the assistant's `exit_plan_mode` tool call, so DSH's existing review approves it and the next step implements. The delegation tool returns the plan text for the parent model to submit. Session titles stay read-only. A host without the plan-mode service behaves as before.
|
|
10
|
+
|
|
3
11
|
## 1.1.1
|
|
4
12
|
|
|
5
13
|
- **Claude CLI search through MCP.** CLI-backed Claude turns and the `claude_code` delegation tool now get a temporary, authenticated, loopback HTTP MCP `web_search` tool calling the plugin's selected DeepSeek/GPT backend. Native CLI `WebSearch` is explicitly denied in every permission mode: it makes a separate Claude server-tool request that PackyCode can refuse even when ordinary turns work. The official CLI/system identity is preserved; no vendor headers or identities are forged. Backend selection applies to CLI searches even when DSH routing is set to Off/Do not take over; the DSH routing mode remains independent, and headless profiles need no `web` service. MCP callbacks capture their originating session ID before losing agent context and always call the direct PackyCode provider, never an auto-router fallback.
|
package/README.en.md
CHANGED
|
@@ -44,7 +44,7 @@ Open the desktop application's **Plugins** page, click **Add plugin**, and enter
|
|
|
44
44
|
dsh-packycode-api
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Choose the **official** install source, then click **Install**. To pin a version, enter `dsh-packycode-api@1.1.
|
|
47
|
+
Choose the **official** install source, then click **Install**. To pin a version, enter `dsh-packycode-api@1.1.3`.
|
|
48
48
|
|
|
49
49
|
Then **manually restart the desktop application**. Refreshing the page does not reload the plugin inside the Host process.
|
|
50
50
|
|
|
@@ -63,7 +63,7 @@ Choose a model from the `PackyCode` group. `deepseek-flash`, `gpt-6.1-sol`, and
|
|
|
63
63
|
|
|
64
64
|
The **Plugins** page shows the installed state and version, and is also where you enable, disable, or uninstall.
|
|
65
65
|
|
|
66
|
-
**For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.1.
|
|
66
|
+
**For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.1.3`. Do not use it for `desktop` — the CLI refuses it (`profile "desktop" is managed exclusively by the Electron application`).
|
|
67
67
|
|
|
68
68
|
A local checkout can be linked with `dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`.
|
|
69
69
|
|
|
@@ -153,14 +153,14 @@ A model in the list does not mean the current token group allows it. When changi
|
|
|
153
153
|
- The `cc` group accepts only the official Claude CLI. A normal API client gets `403 This API endpoint is only accessible via the official Claude CLI`. This plugin runs the local `claude` binary for that group instead of switching the token to `cc-sale` or pretending to be the official client. If the token has no group the official CLI may use, the three Claude models still will not run.
|
|
154
154
|
- Selecting a Claude model when the CLI is missing fails that turn with an install hint. It does not surface as a missing binary name alone.
|
|
155
155
|
- The CLI receives this plugin's endpoint and current key (`ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN`) and an isolated config directory, `~/.dsh/packycode-claude-cli`. Without that isolation, `env` in `~/.claude/settings.json` would override the plugin key.
|
|
156
|
-
- CLI permission follows the session by default: `read-only` stays read-only, `workspace-write` may edit, and `danger-full-access` with approval disabled may use `full`. The CLI cannot ask the DSH user for approval, so a session that still asks never gets `full`. `claudeCliMode` can pin `read-only`, `edit`, or `full`. An empty `claudeCliPath` is looked up automatically; an empty `claudeCliCwd` uses the session workspace.
|
|
156
|
+
- CLI permission follows the session by default: `read-only` stays read-only, `workspace-write` may edit, and `danger-full-access` with approval disabled may use `full`. The CLI cannot ask the DSH user for approval, so a session that still asks never gets `full`. `claudeCliMode` can pin `read-only`, `edit`, or `full`. DSH plan mode wins over that mapping and over the pin: the CLI runs with `--permission-mode plan`. Calling `ExitPlanMode`, or returning after writing the plan file when that tool is absent, submits the plan to DSH's review instead of editing in the same process. The previous mode returns after approval or `/plan off`. Session titles stay out of plan mode. An empty `claudeCliPath` is looked up automatically; an empty `claudeCliCwd` uses the session workspace.
|
|
157
157
|
- Thinking and tool activity (`▸ Bash`, `▸ Read`, and `✗` on failure) appear in that turn's reasoning block. They are progress, not DSH tool calls. The CLI's own tools are outside the DSH sandbox.
|
|
158
158
|
- The reasoning block opens with `▸ 启动 Claude Code CLI…` and then shows the CLI's version and model once it is up. When the gateway throttles or is busy (429, 503 `账号并发排队已满` / per-account wait queue full, and similar), the CLI retries up to 10 times on its own; each retry now shows as `↻ 网关返回 HTTP 503,CLI 自动重试 n/10` instead of looking hung. When the retries run out, the turn says PackyCode's Claude upstream is busy, not that the key is bad. A CLI that prints nothing for 2 minutes after starting is stopped and reported.
|
|
159
159
|
- The conversation and its images reach the CLI over stdin (`--input-format stream-json`), not argv: a Windows command line is length-limited and cannot carry an image. Images in the conversation are sent to Claude as images, at the HTTP route's request size (at most 2048×2048, about 1 MiB each; at most 20 images and 20 MiB per turn, dropping the oldest first). An image left out is named with its read-only local copy, which the CLI can open with Read.
|
|
160
160
|
- Every run carries `--append-system-prompt` (the delegation note). In `-p` mode the official CLI introduces itself as "You are Claude Code, Anthropic's official CLI for Claude …" when it has appended instructions and as "You are a Claude agent, built on Anthropic's Claude Agent SDK." when it has none; the latter was refused upstream as not coming from the official CLI (403 `only accepts requests from the official Claude Code CLI`). That is the official CLI's own behavior; the plugin does not rewrite its system prompt.
|
|
161
161
|
- While the session model is a Claude model, titles are generated with `claude-haiku-5-5`, read-only, in a temporary directory, instead of a full Opus turn.
|
|
162
162
|
- While the session model is `gpt-6-astra`, titles are generated with `gpt-6.1-sol` (same Codex group); the turns themselves stay on Astra. Other models title their own sessions.
|
|
163
|
-
- The `claude_code` tool can delegate one task to the local CLI while the session model is `gpt-6.1-sol`, `gpt-6-astra`, or `deepseek-flash`. The requested `mode` cannot exceed what the session preset allows.
|
|
163
|
+
- The `claude_code` tool can delegate one task to the local CLI while the session model is `gpt-6.1-sol`, `gpt-6-astra`, or `deepseek-flash`. The requested `mode` cannot exceed what the session preset allows. While the session is in plan mode the delegation plans too, and returns the plan text to the parent model.
|
|
164
164
|
- An effort level the model does not support is rejected before the request is sent. Do not edit a model's effort map casually.
|
|
165
165
|
|
|
166
166
|
</details>
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
dsh-packycode-api
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
在**安装源**中选择**官方源**,然后点击**安装**。需要固定版本时填入 `dsh-packycode-api@1.1.
|
|
51
|
+
在**安装源**中选择**官方源**,然后点击**安装**。需要固定版本时填入 `dsh-packycode-api@1.1.3`。
|
|
52
52
|
|
|
53
53
|
完成后**手动重启桌面应用**。仅刷新页面不会重新加载 Host 里的插件。
|
|
54
54
|
|
|
@@ -67,7 +67,7 @@ dsh-packycode-api
|
|
|
67
67
|
|
|
68
68
|
桌面应用的**插件**页可以看到已安装状态与版本,也可以在那里启用、停用或卸载。
|
|
69
69
|
|
|
70
|
-
**仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.1.
|
|
70
|
+
**仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.1.3`。不要对 `desktop` 使用它 —— CLI 会直接拒绝(`profile "desktop" is managed exclusively by the Electron application`)。
|
|
71
71
|
|
|
72
72
|
本地开发可以用绝对路径:`dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`。
|
|
73
73
|
|
|
@@ -157,14 +157,14 @@ Claude Code CLI → 本机临时 MCP web_search → 插件选定的 DeepSeek/GPT
|
|
|
157
157
|
- `cc`(Claude Code 专用)只接受官方 Claude CLI。用普通 API 客户端调 Claude 会 403:`This API endpoint is only accessible via the official Claude CLI`。本插件因此调用本机 `claude`,而不是改去 `cc-sale`,也不是伪造客户端身份。令牌如果没有开放给官方 CLI 的分组,这三个模型仍然不可用。
|
|
158
158
|
- 选了 Claude 模型但本机没有 CLI 时,这一回合会直接说明未安装,并指向设置页的安装按钮。
|
|
159
159
|
- CLI 使用插件配置的地址和当前 Key(`ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN`),以及独立配置目录 `~/.dsh/packycode-claude-cli`。不隔离的话,`~/.claude/settings.json` 里的 `env` 会盖掉插件 Key。
|
|
160
|
-
- CLI 权限默认跟随当前会话:`read-only` → 只读,`workspace-write` → 可编辑,`danger-full-access` 且不再审批 → `full`。CLI 不能向 DSH 弹审批,所以只要会话还要审批,就不会给它 `full`。也可把设置项 `claudeCliMode` 固定成 `read-only` / `edit` / `full
|
|
160
|
+
- CLI 权限默认跟随当前会话:`read-only` → 只读,`workspace-write` → 可编辑,`danger-full-access` 且不再审批 → `full`。CLI 不能向 DSH 弹审批,所以只要会话还要审批,就不会给它 `full`。也可把设置项 `claudeCliMode` 固定成 `read-only` / `edit` / `full`。DSH 计划模式优先于该映射和这个固定档:CLI 以 `--permission-mode plan` 只读探索。它调用 `ExitPlanMode`,或在没有该工具时把计划写进计划文件后返回,都会交给 DSH 审阅;批准或 `/plan off` 之后才恢复原来的档位。会话标题不进入计划模式。`claudeCliPath` 留空则自动查找;`claudeCliCwd` 留空则用当前会话工作区。
|
|
161
161
|
- 思考过程和工具调用(`▸ Bash`、`▸ Read`,失败时带 `✗`)会出现在这一回合的思考区。它们是进度,不是 DSH 的工具调用。CLI 自己的工具不受 DSH 沙箱约束。
|
|
162
162
|
- 思考区第一行是「▸ 启动 Claude Code CLI…」,CLI 起来后显示版本和模型。网关限流或繁忙(429、503 `账号并发排队已满` 等)时,CLI 会自己重试最多 10 次,每次重试都会显示成 `↻ 网关返回 HTTP 503,CLI 自动重试 n/10`,不再像卡住。重试用完仍失败时,会说明是 PackyCode 的 Claude 上游繁忙,不是 Key 的问题。CLI 启动后 2 分钟内没有任何输出会被停止并报告原因。
|
|
163
163
|
- 对话记录和图片通过 stdin 交给 CLI(`--input-format stream-json`),不走命令行参数:Windows 命令行长度有上限,而且命令行带不了图片。对话里的图片会以图片形式发给 Claude(与 HTTP 路由相同的压缩规格:每张不超过 2048×2048、约 1 MiB;一回合最多 20 张、合计 20 MiB,超出时先舍弃最早的)。没随消息发出的图片会写明本机只读副本的路径,CLI 需要时可以用 Read 读取。
|
|
164
164
|
- 每次运行都带 `--append-system-prompt`(委派说明)。在 `-p` 模式下,官方 CLI 有追加说明时会以 "You are Claude Code, Anthropic's official CLI for Claude …" 自我介绍,没有时是 "You are a Claude agent, built on Anthropic's Claude Agent SDK.";后者曾被上游当成非官方 CLI 拒绝(403 `only accepts requests from the official Claude Code CLI`)。这是官方 CLI 自己的行为,插件没有改写它的系统提示词。
|
|
165
165
|
- 会话跑在 Claude 模型上时,生成标题改用 `claude-haiku-5-5`,只读、临时目录,避免为起标题跑一整轮 Opus。
|
|
166
166
|
- 会话跑在 `gpt-6-astra` 上时,标题改由 `gpt-6.1-sol` 生成(同一个 Codex 分组);对话本身仍然用 Astra。其它模型的标题由会话模型自己生成。
|
|
167
|
-
- 另有 `claude_code` 工具:当前模型是 `gpt-6.1-sol`、`gpt-6-astra` 或 `deepseek-flash` 时,可以把一个任务委派给本机 Claude Code。请求的 `mode`
|
|
167
|
+
- 另有 `claude_code` 工具:当前模型是 `gpt-6.1-sol`、`gpt-6-astra` 或 `deepseek-flash` 时,可以把一个任务委派给本机 Claude Code。请求的 `mode` 不会超过会话权限允许的上限;会话处于计划模式时,这个委派也只规划,并把计划正文交回父模型。
|
|
168
168
|
- 声明了模型不支持的推理档位会被 harness 在发请求前拒绝,不要随手改档位映射。
|
|
169
169
|
|
|
170
170
|
</details>
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
|
-
import { isAbsolute, join, posix, win32 } from "node:path";
|
|
3
|
+
import { isAbsolute, join, posix, sep, win32 } from "node:path";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { HarnessError, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
8
8
|
import { mkdtemp, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
10
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
@@ -1427,10 +1427,10 @@ const plainDescription = (part) => part.kind === "image" ? `[图片 ${attachment
|
|
|
1427
1427
|
* @param cwd - the working directory the CLI will run in, when known.
|
|
1428
1428
|
* @returns the instructions text.
|
|
1429
1429
|
*/
|
|
1430
|
-
function delegationInstructions(options, cwd) {
|
|
1430
|
+
function delegationInstructions(options, cwd, { plan = false } = {}) {
|
|
1431
1431
|
const system = typeof options?.system === "string" && options.system.trim() !== "" ? options.system.trim() : "";
|
|
1432
1432
|
return [
|
|
1433
|
-
"本回合由 DeepSeek Harness 委派给 Claude Code 执行。用户消息是 DeepSeek Harness 会话的近期对话记录,最后一条是当前请求;其中的图片已作为图片随消息附上,「只读副本」路径也可以用 Read 工具读取。请用你自己的工具完成任务,直接输出最终结果;不要反问、不要复述流程。",
|
|
1433
|
+
plan ? "本回合 DeepSeek Harness 处于计划模式。只做只读探索,把完整实施计划写成以一级标题开头的 Markdown,写入 CLI 指定的计划文件。若有 ExitPlanMode 就调用它;没有也在写完后停下来。不要修改项目、不要开始实施。DeepSeek Harness 会用这份计划打开审阅。用户消息是 DeepSeek Harness 会话的近期对话记录,最后一条是当前请求;其中的图片已作为图片随消息附上,「只读副本」路径也可以用 Read 工具读取。" : "本回合由 DeepSeek Harness 委派给 Claude Code 执行。用户消息是 DeepSeek Harness 会话的近期对话记录,最后一条是当前请求;其中的图片已作为图片随消息附上,「只读副本」路径也可以用 Read 工具读取。请用你自己的工具完成任务,直接输出最终结果;不要反问、不要复述流程。",
|
|
1434
1434
|
cwd === void 0 ? void 0 : `工作目录:${cwd}`,
|
|
1435
1435
|
system === "" ? void 0 : `补充说明(来自 DeepSeek Harness,其中提到的工具你未必拥有):${system.slice(0, 2e3)}`
|
|
1436
1436
|
].filter((part) => part !== void 0).join("\n");
|
|
@@ -1642,6 +1642,34 @@ function createBlockWriter() {
|
|
|
1642
1642
|
if (open?.kind === "reasoning") open.sealed = false;
|
|
1643
1643
|
yield* write("reasoning", `${separator}${line}`);
|
|
1644
1644
|
},
|
|
1645
|
+
/** One complete tool call after whatever text or reasoning is open. */
|
|
1646
|
+
*toolCall(id, name, args) {
|
|
1647
|
+
yield* close();
|
|
1648
|
+
index += 1;
|
|
1649
|
+
const block = {
|
|
1650
|
+
type: "tool-call",
|
|
1651
|
+
id,
|
|
1652
|
+
name,
|
|
1653
|
+
arguments: args
|
|
1654
|
+
};
|
|
1655
|
+
yield {
|
|
1656
|
+
type: "block-start",
|
|
1657
|
+
index,
|
|
1658
|
+
blockType: "tool-call"
|
|
1659
|
+
};
|
|
1660
|
+
yield {
|
|
1661
|
+
type: "tool-call-delta",
|
|
1662
|
+
index,
|
|
1663
|
+
id,
|
|
1664
|
+
name,
|
|
1665
|
+
argumentsDelta: args
|
|
1666
|
+
};
|
|
1667
|
+
yield {
|
|
1668
|
+
type: "block-end",
|
|
1669
|
+
index,
|
|
1670
|
+
block
|
|
1671
|
+
};
|
|
1672
|
+
},
|
|
1645
1673
|
close
|
|
1646
1674
|
};
|
|
1647
1675
|
}
|
|
@@ -1706,15 +1734,16 @@ async function cliCall(options, { titleModel, cwdOf, modeOf, attachmentsOf, imag
|
|
|
1706
1734
|
cwd: tmpdir()
|
|
1707
1735
|
};
|
|
1708
1736
|
const cwd = cwdOf();
|
|
1737
|
+
const mode = modeOf();
|
|
1709
1738
|
return {
|
|
1710
1739
|
content: await buildDelegatedContent(options, {
|
|
1711
1740
|
attachments: attachmentsOf(),
|
|
1712
1741
|
limits: imageLimits,
|
|
1713
1742
|
signal: options?.signal
|
|
1714
1743
|
}),
|
|
1715
|
-
appendSystemPrompt: delegationInstructions(options, cwd),
|
|
1744
|
+
appendSystemPrompt: delegationInstructions(options, cwd, { plan: mode === "plan" }),
|
|
1716
1745
|
model: options.model,
|
|
1717
|
-
mode
|
|
1746
|
+
mode,
|
|
1718
1747
|
cwd
|
|
1719
1748
|
};
|
|
1720
1749
|
}
|
|
@@ -1816,6 +1845,8 @@ function createClaudeCliAdapter({ inner, runner, modelIds = CLI_BACKED_MODEL_IDS
|
|
|
1816
1845
|
let retries = 0;
|
|
1817
1846
|
/** The key this run uses, as the runner described it (never the token). */
|
|
1818
1847
|
let runKey;
|
|
1848
|
+
/** Set when the CLI submitted a plan; the turn then ends as DSH's review tool. */
|
|
1849
|
+
let submittedPlan;
|
|
1819
1850
|
try {
|
|
1820
1851
|
let finished;
|
|
1821
1852
|
let lastRequest;
|
|
@@ -1858,11 +1889,26 @@ function createClaudeCliAdapter({ inner, runner, modelIds = CLI_BACKED_MODEL_IDS
|
|
|
1858
1889
|
case "request-usage":
|
|
1859
1890
|
lastRequest = event.usage;
|
|
1860
1891
|
break;
|
|
1892
|
+
case "plan":
|
|
1893
|
+
if (typeof event.plan === "string" && event.plan !== "") submittedPlan = event.plan;
|
|
1894
|
+
break;
|
|
1861
1895
|
case "result":
|
|
1862
1896
|
finished = event.result;
|
|
1863
1897
|
break;
|
|
1864
1898
|
default: break;
|
|
1865
1899
|
}
|
|
1900
|
+
if (submittedPlan !== void 0) {
|
|
1901
|
+
yield* blocks.toolCall(`plan-${randomUUID()}`, "exit_plan_mode", JSON.stringify({ plan: submittedPlan }));
|
|
1902
|
+
yield {
|
|
1903
|
+
type: "usage",
|
|
1904
|
+
usage: turnUsage(lastRequest, finished)
|
|
1905
|
+
};
|
|
1906
|
+
yield {
|
|
1907
|
+
type: "finish",
|
|
1908
|
+
reason: { kind: "tool-calls" }
|
|
1909
|
+
};
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1866
1912
|
yield* blocks.close();
|
|
1867
1913
|
yield {
|
|
1868
1914
|
type: "usage",
|
|
@@ -2098,7 +2144,12 @@ const CLAUDE_CLI_MODES = Object.freeze({
|
|
|
2098
2144
|
"Write",
|
|
2099
2145
|
"NotebookEdit"
|
|
2100
2146
|
]) }),
|
|
2101
|
-
full: Object.freeze({ skipPermissions: true })
|
|
2147
|
+
full: Object.freeze({ skipPermissions: true }),
|
|
2148
|
+
/**
|
|
2149
|
+
* DSH plan mode. Not a permission rank: file edits stay blocked, and
|
|
2150
|
+
* `ExitPlanMode` is handed back to DSH instead of letting the CLI implement.
|
|
2151
|
+
*/
|
|
2152
|
+
plan: Object.freeze({ permissionMode: "plan" })
|
|
2102
2153
|
});
|
|
2103
2154
|
const DEFAULT_CLAUDE_MODE = "edit";
|
|
2104
2155
|
/** Isolated CLI home; the plugin owns the endpoint and key it runs with. */
|
|
@@ -2279,11 +2330,128 @@ function buildClaudeArgs({ mode = DEFAULT_CLAUDE_MODE, model, stream = false, ap
|
|
|
2279
2330
|
if (searchBridge !== void 0 && (!/^mcp__packycode_search_[a-f0-9]+__web_search$/u.test(searchBridge.toolName ?? "") || typeof searchBridge.configPath !== "string")) throw new Error("PackyCode CLI search bridge configuration is invalid");
|
|
2280
2331
|
args.push("--append-system-prompt", searchBridge === void 0 ? instructions : `${instructions}\n\n${searchBridge.instructions}`);
|
|
2281
2332
|
if (spec.skipPermissions === true) args.push("--dangerously-skip-permissions");
|
|
2282
|
-
else
|
|
2333
|
+
else if (typeof spec.permissionMode === "string") {
|
|
2334
|
+
args.push("--permission-mode", spec.permissionMode);
|
|
2335
|
+
if (searchBridge !== void 0) args.push("--allowedTools", searchBridge.toolName);
|
|
2336
|
+
} else args.push("--allowedTools", ...spec.allowedTools, ...searchBridge === void 0 ? [] : [searchBridge.toolName]);
|
|
2283
2337
|
if (disableNativeSearch || searchBridge !== void 0) args.push("--disallowedTools", "WebSearch");
|
|
2284
2338
|
if (searchBridge !== void 0) args.push("--mcp-config", searchBridge.configPath);
|
|
2285
2339
|
return args;
|
|
2286
2340
|
}
|
|
2341
|
+
/** How many permission prompts one plan-mode run will refuse before stopping. */
|
|
2342
|
+
const MAX_PLAN_DENIALS = 8;
|
|
2343
|
+
/** A plan file larger than this is not read into the turn. */
|
|
2344
|
+
const PLAN_FILE_MAX_BYTES = 2e5;
|
|
2345
|
+
const PLAN_FILE_CLOCK_SKEW_MS = 2e3;
|
|
2346
|
+
const PLAN_EXIT_DENY = "Write the plan to the plan file first, starting with a # heading, then call ExitPlanMode again.";
|
|
2347
|
+
const PLAN_ACTION_DENY = "Plan mode is active. Do not edit files or run commands that change the system. Continue read-only exploration, write the plan file, and call ExitPlanMode.";
|
|
2348
|
+
const PLAN_DIALOG_ERROR = "Plan mode cannot ask the user from the CLI. Decide and continue, or call ExitPlanMode.";
|
|
2349
|
+
const PLAN_DENY_LIMIT_MESSAGE = "claude_code plan mode stopped after too many denied permission prompts";
|
|
2350
|
+
/**
|
|
2351
|
+
* Markdown DSH's `exit_plan_mode` will accept. A body without a heading gets
|
|
2352
|
+
* one, because the review tool requires the plan to start with `#`.
|
|
2353
|
+
* @param value - candidate plan text.
|
|
2354
|
+
* @returns the plan, or undefined when there is nothing to submit.
|
|
2355
|
+
*/
|
|
2356
|
+
function planMarkdown(value) {
|
|
2357
|
+
if (typeof value !== "string") return void 0;
|
|
2358
|
+
const trimmed = value.trim();
|
|
2359
|
+
if (trimmed === "") return void 0;
|
|
2360
|
+
const bounded = trimmed.length > PLAN_FILE_MAX_BYTES ? trimmed.slice(0, PLAN_FILE_MAX_BYTES) : trimmed;
|
|
2361
|
+
return /^#\s+\S/u.test(bounded) ? bounded : `# Plan\n\n${bounded}`;
|
|
2362
|
+
}
|
|
2363
|
+
/** Whether `file` is the root itself or a path inside it, after resolving links. */
|
|
2364
|
+
function pathInside(root, file, realpath) {
|
|
2365
|
+
let rootReal;
|
|
2366
|
+
let fileReal;
|
|
2367
|
+
try {
|
|
2368
|
+
rootReal = realpath(root);
|
|
2369
|
+
fileReal = realpath(file);
|
|
2370
|
+
} catch {
|
|
2371
|
+
return false;
|
|
2372
|
+
}
|
|
2373
|
+
const prefix = rootReal.endsWith(sep) ? rootReal : `${rootReal}${sep}`;
|
|
2374
|
+
return fileReal === rootReal || fileReal.startsWith(prefix);
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
* The plan a CLI `ExitPlanMode` request is submitting.
|
|
2378
|
+
*
|
|
2379
|
+
* The tool's own schema does not carry the plan; the CLI injects `plan` and
|
|
2380
|
+
* `planFilePath` from the file it told the model to write. Text on the request
|
|
2381
|
+
* wins. A path is read only when it resolves inside this run's config home or
|
|
2382
|
+
* `~/.claude/plans`, is a markdown file written for this run, and is bounded.
|
|
2383
|
+
* Anything else is ignored — a model-supplied path is not a licence to read
|
|
2384
|
+
* the disk.
|
|
2385
|
+
* @param input - the control request's `input`.
|
|
2386
|
+
* @param options - config home, when the run started, and file-system seams.
|
|
2387
|
+
* @returns plan markdown, or undefined when the request has none yet.
|
|
2388
|
+
*/
|
|
2389
|
+
function readSubmittedPlan(input, { configDir, startedAt = Date.now(), home = homedir(), readFile = (file) => readFileSync(file, "utf8"), stat = statSync, realpath = realpathSync } = {}) {
|
|
2390
|
+
const inline = planMarkdown(input?.plan);
|
|
2391
|
+
if (inline !== void 0) return inline;
|
|
2392
|
+
const requested = typeof input?.planFilePath === "string" ? input.planFilePath : "";
|
|
2393
|
+
if (requested === "") return void 0;
|
|
2394
|
+
if (![configDir, join(home, ".claude", "plans")].filter((root) => typeof root === "string" && root !== "").some((root) => pathInside(root, requested, realpath))) return void 0;
|
|
2395
|
+
let file;
|
|
2396
|
+
try {
|
|
2397
|
+
file = realpath(requested);
|
|
2398
|
+
const info = stat(file);
|
|
2399
|
+
if (!info.isFile() || info.size > PLAN_FILE_MAX_BYTES || info.mtimeMs < startedAt - PLAN_FILE_CLOCK_SKEW_MS) return void 0;
|
|
2400
|
+
} catch {
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
if (!file.toLowerCase().endsWith(".md")) return void 0;
|
|
2404
|
+
try {
|
|
2405
|
+
return planMarkdown(readFile(file));
|
|
2406
|
+
} catch {
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* The plan file a print-mode CLI wrote and then returned without calling
|
|
2412
|
+
* `ExitPlanMode`. `-p` often has no interactive plan tool, so the file is
|
|
2413
|
+
* what DSH reviews. Only a markdown file under this run's config `plans`
|
|
2414
|
+
* directory or `~/.claude/plans`, newer than the run, is eligible. The newest
|
|
2415
|
+
* one wins.
|
|
2416
|
+
* @param options - config home, when the run started, and file-system seams.
|
|
2417
|
+
* @returns plan markdown, or undefined when this run wrote none.
|
|
2418
|
+
*/
|
|
2419
|
+
function findPlanWrittenDuringRun({ configDir, startedAt = Date.now(), home = homedir(), readdir = readdirSync, readFile = (file) => readFileSync(file, "utf8"), stat = statSync, realpath = realpathSync } = {}) {
|
|
2420
|
+
const roots = [typeof configDir === "string" && configDir !== "" ? join(configDir, "plans") : "", join(home, ".claude", "plans")].filter((root) => root !== "");
|
|
2421
|
+
let newest;
|
|
2422
|
+
let newestMtime = -Infinity;
|
|
2423
|
+
for (const root of roots) {
|
|
2424
|
+
let names;
|
|
2425
|
+
try {
|
|
2426
|
+
names = readdir(root);
|
|
2427
|
+
} catch {
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
for (const name of names) {
|
|
2431
|
+
if (typeof name !== "string" || !name.toLowerCase().endsWith(".md")) continue;
|
|
2432
|
+
const candidate = join(root, name);
|
|
2433
|
+
let info;
|
|
2434
|
+
try {
|
|
2435
|
+
info = stat(candidate);
|
|
2436
|
+
} catch {
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
if (!info.isFile() || info.mtimeMs < startedAt - PLAN_FILE_CLOCK_SKEW_MS || info.mtimeMs < newestMtime) continue;
|
|
2440
|
+
const text = readSubmittedPlan({ planFilePath: candidate }, {
|
|
2441
|
+
configDir,
|
|
2442
|
+
startedAt,
|
|
2443
|
+
home,
|
|
2444
|
+
readFile,
|
|
2445
|
+
stat,
|
|
2446
|
+
realpath
|
|
2447
|
+
});
|
|
2448
|
+
if (text === void 0) continue;
|
|
2449
|
+
newest = text;
|
|
2450
|
+
newestMtime = info.mtimeMs;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
return newest;
|
|
2454
|
+
}
|
|
2287
2455
|
/**
|
|
2288
2456
|
* Build what the CLI reads from stdin for one run.
|
|
2289
2457
|
*
|
|
@@ -2557,13 +2725,16 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2557
2725
|
if (disposed || signal?.aborted) throw new Error("claude_code was aborted");
|
|
2558
2726
|
const cancellation = new AbortController();
|
|
2559
2727
|
const runSignal = signal === void 0 ? cancellation.signal : AbortSignal.any([signal, cancellation.signal]);
|
|
2728
|
+
const startedAt = Date.now();
|
|
2560
2729
|
const state = {
|
|
2561
2730
|
stdout: "",
|
|
2562
2731
|
stderr: "",
|
|
2563
2732
|
timedOut: false,
|
|
2564
2733
|
silent: false,
|
|
2565
2734
|
sawOutput: false,
|
|
2566
|
-
settled: false
|
|
2735
|
+
settled: false,
|
|
2736
|
+
denies: 0,
|
|
2737
|
+
startedAt
|
|
2567
2738
|
};
|
|
2568
2739
|
let child;
|
|
2569
2740
|
let bridge;
|
|
@@ -2704,12 +2875,126 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2704
2875
|
}
|
|
2705
2876
|
child.stdout?.setEncoding?.("utf8");
|
|
2706
2877
|
child.stderr?.setEncoding?.("utf8");
|
|
2878
|
+
let controlBuffer = "";
|
|
2879
|
+
let stdinEnded = false;
|
|
2880
|
+
const endStdin = () => {
|
|
2881
|
+
if (stdinEnded) return;
|
|
2882
|
+
stdinEnded = true;
|
|
2883
|
+
try {
|
|
2884
|
+
child.stdin?.end?.();
|
|
2885
|
+
} catch {}
|
|
2886
|
+
};
|
|
2887
|
+
const writeStdin = (frame) => {
|
|
2888
|
+
try {
|
|
2889
|
+
child.stdin?.write?.(frame);
|
|
2890
|
+
} catch {}
|
|
2891
|
+
};
|
|
2892
|
+
const controlResponse = (requestId, response) => {
|
|
2893
|
+
writeStdin(`${JSON.stringify({
|
|
2894
|
+
type: "control_response",
|
|
2895
|
+
response: {
|
|
2896
|
+
subtype: "success",
|
|
2897
|
+
request_id: requestId,
|
|
2898
|
+
response
|
|
2899
|
+
}
|
|
2900
|
+
})}\n`);
|
|
2901
|
+
};
|
|
2902
|
+
const stopForDenials = () => {
|
|
2903
|
+
state.denyExhausted = true;
|
|
2904
|
+
endStdin();
|
|
2905
|
+
kill();
|
|
2906
|
+
};
|
|
2907
|
+
const refuseControl = (requestId, kind) => {
|
|
2908
|
+
state.denies += 1;
|
|
2909
|
+
if (state.denies > MAX_PLAN_DENIALS) {
|
|
2910
|
+
stopForDenials();
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
if (kind === "dialog") {
|
|
2914
|
+
writeStdin(`${JSON.stringify({
|
|
2915
|
+
type: "control_response",
|
|
2916
|
+
response: {
|
|
2917
|
+
subtype: "error",
|
|
2918
|
+
request_id: requestId,
|
|
2919
|
+
error: PLAN_DIALOG_ERROR
|
|
2920
|
+
}
|
|
2921
|
+
})}\n`);
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
controlResponse(requestId, {
|
|
2925
|
+
behavior: "deny",
|
|
2926
|
+
message: PLAN_ACTION_DENY
|
|
2927
|
+
});
|
|
2928
|
+
};
|
|
2929
|
+
/**
|
|
2930
|
+
* Plan mode keeps stdin open so a permission prompt can be answered.
|
|
2931
|
+
* `ExitPlanMode` with a plan stops the child; it is never allowed,
|
|
2932
|
+
* because allowing it would make the CLI start editing in this process.
|
|
2933
|
+
*/
|
|
2934
|
+
const handlePlanLine = (line) => {
|
|
2935
|
+
const trimmed = line.trim();
|
|
2936
|
+
if (!trimmed.startsWith("{")) return;
|
|
2937
|
+
let message;
|
|
2938
|
+
try {
|
|
2939
|
+
message = JSON.parse(trimmed);
|
|
2940
|
+
} catch {
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
if (message?.type === "result") {
|
|
2944
|
+
endStdin();
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
if (message?.type !== "control_request") return;
|
|
2948
|
+
const requestId = message.request_id;
|
|
2949
|
+
const request = message.request;
|
|
2950
|
+
if (typeof requestId !== "string" || request === null || typeof request !== "object") {
|
|
2951
|
+
state.denies += 1;
|
|
2952
|
+
if (state.denies > MAX_PLAN_DENIALS) stopForDenials();
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
if (request.subtype !== "can_use_tool") {
|
|
2956
|
+
refuseControl(requestId, "dialog");
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
if (request.tool_name === "ExitPlanMode") {
|
|
2960
|
+
const plan = readSubmittedPlan(request.input, {
|
|
2961
|
+
configDir,
|
|
2962
|
+
startedAt
|
|
2963
|
+
});
|
|
2964
|
+
if (plan !== void 0) {
|
|
2965
|
+
state.plan = scrub(plan);
|
|
2966
|
+
endStdin();
|
|
2967
|
+
kill();
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
state.denies += 1;
|
|
2971
|
+
if (state.denies > MAX_PLAN_DENIALS) {
|
|
2972
|
+
stopForDenials();
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
controlResponse(requestId, {
|
|
2976
|
+
behavior: "deny",
|
|
2977
|
+
message: PLAN_EXIT_DENY
|
|
2978
|
+
});
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2981
|
+
refuseControl(requestId, "tool");
|
|
2982
|
+
};
|
|
2707
2983
|
child.stdout?.on("data", (chunk) => {
|
|
2708
2984
|
state.sawOutput = true;
|
|
2709
2985
|
clearStartup();
|
|
2710
2986
|
const text = String(chunk);
|
|
2711
2987
|
state.stdout = scrub((state.stdout + text).slice(-4194304));
|
|
2712
2988
|
onStdout?.(text);
|
|
2989
|
+
if (mode !== "plan") return;
|
|
2990
|
+
controlBuffer += text;
|
|
2991
|
+
let index = controlBuffer.indexOf("\n");
|
|
2992
|
+
while (index !== -1) {
|
|
2993
|
+
const line = controlBuffer.slice(0, index);
|
|
2994
|
+
controlBuffer = controlBuffer.slice(index + 1);
|
|
2995
|
+
handlePlanLine(line);
|
|
2996
|
+
index = controlBuffer.indexOf("\n");
|
|
2997
|
+
}
|
|
2713
2998
|
});
|
|
2714
2999
|
child.stderr?.on("data", (chunk) => {
|
|
2715
3000
|
state.stderr = scrub((state.stderr + String(chunk)).slice(-4194304));
|
|
@@ -2728,6 +3013,12 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2728
3013
|
const detail = state.stderr.trim().split("\n").slice(-4).join(" ").slice(0, 400);
|
|
2729
3014
|
return reject(/* @__PURE__ */ new Error(`Claude Code CLI printed nothing within ${Math.round(startupTimeoutMs / 1e3)}s of starting and was stopped${detail === "" ? "" : `: ${detail}`}`));
|
|
2730
3015
|
}
|
|
3016
|
+
if (signal?.aborted) return reject(/* @__PURE__ */ new Error("claude_code was aborted"));
|
|
3017
|
+
if (state.denyExhausted === true) return reject(/* @__PURE__ */ new Error(PLAN_DENY_LIMIT_MESSAGE));
|
|
3018
|
+
if (typeof state.plan === "string") return resolve({
|
|
3019
|
+
code,
|
|
3020
|
+
killSignal
|
|
3021
|
+
});
|
|
2731
3022
|
if (runSignal.aborted) return reject(/* @__PURE__ */ new Error("claude_code was aborted"));
|
|
2732
3023
|
resolve({
|
|
2733
3024
|
code,
|
|
@@ -2743,7 +3034,8 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2743
3034
|
});
|
|
2744
3035
|
closed.catch(() => void 0);
|
|
2745
3036
|
child.stdin?.on?.("error", () => void 0);
|
|
2746
|
-
|
|
3037
|
+
if (mode === "plan") writeStdin(input);
|
|
3038
|
+
else child.stdin?.end?.(input);
|
|
2747
3039
|
if (runSignal.aborted) kill();
|
|
2748
3040
|
return {
|
|
2749
3041
|
child,
|
|
@@ -2769,7 +3061,30 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2769
3061
|
* @param options - prompt, mode, model, working directory, and cancellation.
|
|
2770
3062
|
* @returns the CLI's answer plus its own accounting.
|
|
2771
3063
|
*/
|
|
2772
|
-
async run({ prompt, mode = DEFAULT_CLAUDE_MODE, model, appendSystemPrompt, cwd, signal, enableSearch } = {}) {
|
|
3064
|
+
async run({ prompt, content, mode = DEFAULT_CLAUDE_MODE, model, appendSystemPrompt, cwd, signal, enableSearch } = {}) {
|
|
3065
|
+
if (mode === "plan") {
|
|
3066
|
+
let finished;
|
|
3067
|
+
for await (const event of this.stream({
|
|
3068
|
+
prompt,
|
|
3069
|
+
content,
|
|
3070
|
+
mode,
|
|
3071
|
+
model,
|
|
3072
|
+
appendSystemPrompt,
|
|
3073
|
+
cwd,
|
|
3074
|
+
signal,
|
|
3075
|
+
enableSearch
|
|
3076
|
+
})) if (event.type === "result") finished = event.result;
|
|
3077
|
+
if (finished === void 0) throw new Error("claude_code plan mode ended without a result");
|
|
3078
|
+
return {
|
|
3079
|
+
result: finished.result,
|
|
3080
|
+
model: finished.model,
|
|
3081
|
+
mode: "plan",
|
|
3082
|
+
costUsd: finished.costUsd,
|
|
3083
|
+
durationMs: finished.durationMs,
|
|
3084
|
+
inputTokens: finished.inputTokens,
|
|
3085
|
+
outputTokens: finished.outputTokens
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
2773
3088
|
const startedAt = Date.now();
|
|
2774
3089
|
const { state, closed } = await start({
|
|
2775
3090
|
prompt,
|
|
@@ -2902,6 +3217,42 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2902
3217
|
const event = readClaudeStreamEvents(JSON.parse(scrub(buffer.trim()))).find((item) => item.type === "result");
|
|
2903
3218
|
if (event !== void 0) sawResult = event.result;
|
|
2904
3219
|
} catch {}
|
|
3220
|
+
if (mode === "plan" && typeof state.plan !== "string" && exitStatus?.code === 0) {
|
|
3221
|
+
const written = findPlanWrittenDuringRun({
|
|
3222
|
+
configDir,
|
|
3223
|
+
startedAt: state.startedAt
|
|
3224
|
+
});
|
|
3225
|
+
if (written !== void 0) state.plan = scrub(written);
|
|
3226
|
+
}
|
|
3227
|
+
if (typeof state.plan === "string") {
|
|
3228
|
+
if (deltaKind !== void 0) {
|
|
3229
|
+
const tail = deltaRedactor.flush({ boundary: true });
|
|
3230
|
+
if (tail !== "") yield {
|
|
3231
|
+
type: deltaKind,
|
|
3232
|
+
text: tail
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
const usage = sawResult?.usage ?? {};
|
|
3236
|
+
yield {
|
|
3237
|
+
type: "plan",
|
|
3238
|
+
plan: state.plan
|
|
3239
|
+
};
|
|
3240
|
+
yield {
|
|
3241
|
+
type: "result",
|
|
3242
|
+
result: {
|
|
3243
|
+
result: state.plan,
|
|
3244
|
+
model: typeof sawResult?.model === "string" && sawResult.model !== "" ? sawResult.model : model ?? "",
|
|
3245
|
+
mode: "plan",
|
|
3246
|
+
costUsd: typeof sawResult?.total_cost_usd === "number" ? sawResult.total_cost_usd : 0,
|
|
3247
|
+
durationMs: typeof sawResult?.duration_ms === "number" ? sawResult.duration_ms : Date.now() - startedAt,
|
|
3248
|
+
inputTokens: typeof usage.input_tokens === "number" ? usage.input_tokens : 0,
|
|
3249
|
+
outputTokens: typeof usage.output_tokens === "number" ? usage.output_tokens : 0,
|
|
3250
|
+
cacheReadTokens: typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : 0,
|
|
3251
|
+
cacheWriteTokens: typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : 0
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
2905
3256
|
if (sawResult === void 0) throw new Error(failureMessage(void 0, void 0, state.stderr));
|
|
2906
3257
|
if (sawResult.is_error === true || exitStatus?.code !== 0) throw new Error(failureMessage(sawResult, exitStatus?.code ?? exitStatus?.killSignal, state.stderr));
|
|
2907
3258
|
const usage = sawResult.usage ?? {};
|
|
@@ -3733,6 +4084,36 @@ function claudeModeFor(sandbox, approval) {
|
|
|
3733
4084
|
}
|
|
3734
4085
|
}
|
|
3735
4086
|
/**
|
|
4087
|
+
* Whether this session should run the CLI in plan mode.
|
|
4088
|
+
*
|
|
4089
|
+
* The model request runs inside `agent/pre-step`, before the plan-mode service
|
|
4090
|
+
* appends `plan/mode`. `pending` is that not-yet-logged selection and wins over
|
|
4091
|
+
* the logged `active` bit, matching the `plan:policy` section. Without the
|
|
4092
|
+
* service, the logged projection is the only signal. A missing session, a
|
|
4093
|
+
* missing service, or a throw means plan mode is off — never a failed turn.
|
|
4094
|
+
* @param ctx - the plugin context; services are read without an inject requirement.
|
|
4095
|
+
* @param session - the session whose plan state is read.
|
|
4096
|
+
* @returns true when the CLI must plan instead of acting.
|
|
4097
|
+
*/
|
|
4098
|
+
function planModeForSession(ctx, session) {
|
|
4099
|
+
if (session === void 0 || session === null) return false;
|
|
4100
|
+
try {
|
|
4101
|
+
const agent = ctx.get?.("agents")?.currentInitiator?.();
|
|
4102
|
+
if (agent?.session === session) {
|
|
4103
|
+
const state = ctx.get?.("planMode")?.get?.(agent);
|
|
4104
|
+
if (state !== void 0 && state !== null) return planSelected(state);
|
|
4105
|
+
}
|
|
4106
|
+
return ctx.get?.("sessionProjections")?.stateOf?.(session, "plan")?.active === true;
|
|
4107
|
+
} catch {
|
|
4108
|
+
return false;
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
/** `pending` is the selection the in-flight request was assembled with. */
|
|
4112
|
+
function planSelected(state) {
|
|
4113
|
+
if (typeof state?.pending === "boolean") return state.pending;
|
|
4114
|
+
return state?.active === true;
|
|
4115
|
+
}
|
|
4116
|
+
/**
|
|
3736
4117
|
* Read a session's CLI mode from the host's permission services.
|
|
3737
4118
|
* @param ctx - the plugin context; services are read without an inject requirement.
|
|
3738
4119
|
* @param session - the session whose permission state is read.
|
|
@@ -3768,16 +4149,25 @@ function capClaudeMode(requested, ceiling) {
|
|
|
3768
4149
|
*
|
|
3769
4150
|
* @module dsh-packycode-api/claude-tool
|
|
3770
4151
|
*/
|
|
4152
|
+
/** Modes a caller of `claude_code` may ask for. Plan mode is not one of them. */
|
|
4153
|
+
const CLAUDE_TOOL_MODES = Object.freeze([
|
|
4154
|
+
"read-only",
|
|
4155
|
+
"edit",
|
|
4156
|
+
"full"
|
|
4157
|
+
]);
|
|
3771
4158
|
/** What a `claude_code` tool run is told about where its task came from. */
|
|
3772
4159
|
const CLAUDE_TOOL_INSTRUCTIONS = "This task was delegated to Claude Code by another agent in DeepSeek Harness through its claude_code tool. The task text is self-contained; complete it with your own tools and reply with the final result only.";
|
|
4160
|
+
/** What the same run is told while the parent session is in DSH plan mode. */
|
|
4161
|
+
const CLAUDE_TOOL_PLAN_INSTRUCTIONS = "This task was delegated to Claude Code while DeepSeek Harness is in plan mode. Explore read-only, write a complete markdown plan that starts with a # heading to the CLI plan file, then call ExitPlanMode. Do not edit the project or start implementing. Reply with the plan text only.";
|
|
3773
4162
|
/**
|
|
3774
|
-
* @param options - the CLI runner,
|
|
4163
|
+
* @param options - the CLI runner, the most a run may do for a session, and
|
|
4164
|
+
* whether that session is in DSH plan mode.
|
|
3775
4165
|
* @returns the tool definition to register.
|
|
3776
4166
|
*/
|
|
3777
|
-
function createClaudeCodeTool({ runner, modeCeiling }) {
|
|
4167
|
+
function createClaudeCodeTool({ runner, modeCeiling, planActive }) {
|
|
3778
4168
|
return defineTool({
|
|
3779
4169
|
name: "claude_code",
|
|
3780
|
-
description: "Delegate a task to the local Claude Code CLI (the official `claude` binary), which runs it with its own tools and returns its final answer. Use it when the user asks for Claude or when a task suits Claude Code. The child runs with this plugin's PackyCode endpoint and API key, in an isolated config home. `mode` bounds what it may do on its own: \"read-only\" (Read/Grep/Glob), \"edit\" (also Edit/Write), \"full\" (bypasses the CLI's permission checks, including shell commands); it defaults to, and is capped at, what this session's permission preset allows. It starts a fresh CLI session per call, takes tens of seconds, and bills through this plugin's stored PackyCode keys. Web searches use the selected DeepSeek/GPT backend through a read-only MCP tool rather than native WebSearch. Its internal tool calls are not part of this session's transcript — only the returned answer is.",
|
|
4170
|
+
description: "Delegate a task to the local Claude Code CLI (the official `claude` binary), which runs it with its own tools and returns its final answer. Use it when the user asks for Claude or when a task suits Claude Code. The child runs with this plugin's PackyCode endpoint and API key, in an isolated config home. `mode` bounds what it may do on its own: \"read-only\" (Read/Grep/Glob), \"edit\" (also Edit/Write), \"full\" (bypasses the CLI's permission checks, including shell commands); it defaults to, and is capped at, what this session's permission preset allows. While this session is in DSH plan mode the CLI plans instead, whatever `mode` says, and the returned result is the plan for you to submit with exit_plan_mode — do not treat it as permission to edit. It starts a fresh CLI session per call, takes tens of seconds, and bills through this plugin's stored PackyCode keys. Web searches use the selected DeepSeek/GPT backend through a read-only MCP tool rather than native WebSearch. Its internal tool calls are not part of this session's transcript — only the returned answer is.",
|
|
3781
4171
|
parameters: {
|
|
3782
4172
|
prompt: {
|
|
3783
4173
|
type: "string",
|
|
@@ -3786,7 +4176,7 @@ function createClaudeCodeTool({ runner, modeCeiling }) {
|
|
|
3786
4176
|
},
|
|
3787
4177
|
mode: {
|
|
3788
4178
|
type: "string",
|
|
3789
|
-
enum:
|
|
4179
|
+
enum: [...CLAUDE_TOOL_MODES],
|
|
3790
4180
|
description: "What the CLI may do on its own. Defaults to the most this session's permission preset allows; a wider mode is capped to it. Ask for \"full\" only when the task genuinely needs shell commands."
|
|
3791
4181
|
},
|
|
3792
4182
|
model: {
|
|
@@ -3834,11 +4224,13 @@ function createClaudeCodeTool({ runner, modeCeiling }) {
|
|
|
3834
4224
|
const sessionCwd = exec?.agent?.session?.header?.cwd;
|
|
3835
4225
|
const requested = typeof args.cwd === "string" ? args.cwd.trim() : "";
|
|
3836
4226
|
const cwd = requested === "" ? sessionCwd : isAbsolute(requested) ? requested : sessionCwd === void 0 ? requested : join(sessionCwd, requested);
|
|
4227
|
+
const session = exec?.agent?.session;
|
|
4228
|
+
const planning = planActive?.(session) === true;
|
|
3837
4229
|
return await runner.run({
|
|
3838
4230
|
prompt: args.prompt,
|
|
3839
|
-
mode: capClaudeMode(args.mode, modeCeiling(
|
|
4231
|
+
mode: planning ? "plan" : capClaudeMode(args.mode, modeCeiling(session)) ?? "edit",
|
|
3840
4232
|
model: args.model,
|
|
3841
|
-
appendSystemPrompt: CLAUDE_TOOL_INSTRUCTIONS,
|
|
4233
|
+
appendSystemPrompt: planning ? CLAUDE_TOOL_PLAN_INSTRUCTIONS : CLAUDE_TOOL_INSTRUCTIONS,
|
|
3842
4234
|
cwd,
|
|
3843
4235
|
signal: exec?.signal
|
|
3844
4236
|
});
|
|
@@ -6036,7 +6428,10 @@ function apply(ctx, config) {
|
|
|
6036
6428
|
resolveAttachments: attachments
|
|
6037
6429
|
}),
|
|
6038
6430
|
runner: claudeRunner,
|
|
6039
|
-
resolveMode: () =>
|
|
6431
|
+
resolveMode: () => {
|
|
6432
|
+
const session = currentSession();
|
|
6433
|
+
return planModeForSession(ctx, session) ? "plan" : cliModeCeiling(session) ?? "edit";
|
|
6434
|
+
},
|
|
6040
6435
|
resolveCwd: resolveCliCwd,
|
|
6041
6436
|
resolveAttachments: attachments,
|
|
6042
6437
|
explainKey: (runKey) => keys.describe(runKey)
|
|
@@ -6135,9 +6530,10 @@ function apply(ctx, config) {
|
|
|
6135
6530
|
ctx.inject(["tools"], (toolsCtx) => {
|
|
6136
6531
|
ctx.effect(() => toolsCtx.tools.register(createClaudeCodeTool({
|
|
6137
6532
|
runner: claudeRunner,
|
|
6138
|
-
modeCeiling: cliModeCeiling
|
|
6533
|
+
modeCeiling: cliModeCeiling,
|
|
6534
|
+
planActive: (session) => planModeForSession(ctx, session)
|
|
6139
6535
|
})), "packycode-api: claude_code tool");
|
|
6140
6536
|
});
|
|
6141
6537
|
}
|
|
6142
6538
|
//#endregion
|
|
6143
|
-
export { AUTO_CLAUDE_MODE, CHANNEL, CLAUDE_CLI_MODES, CLAUDE_INSTALL_COMMAND, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLAUDE_TOOL_INSTRUCTIONS, CLI_BACKED_MODEL_IDS, CLI_IMAGE_LIMITS, Config, DEFAULT_API_HOST, DEFAULT_APPEND_SYSTEM_PROMPT, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DISPLAY_NAME, LEGACY_STATUS_HOSTS, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TITLE_MODEL_ID, TITLE_MODEL_ROUTES, accountHostOrder, apply, assertApiKey, buildClaudeArgs, buildClaudeEnvironment, buildClaudeInput, buildDelegatedContent, buildSearchRequest, capClaudeMode, catalogCoverage, classifyClientPolicy, classifyGatewayFailure, claudeBinaryCandidates, claudeModeFor, claudeModeForSession, cliSearchInstructions, createBalanceReader, createClaudeCliAdapter, createClaudeCliManager, createClaudeCodeTool, createClaudeRunner, createClaudeSearchBridge, createGroupReader, createKeyChooser, createPackyCodeAutoSearchProvider, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, delegationInstructions, describeRunKey, explainCliClientPolicyFailure, explainCliGroupFailure, explainClientPolicyFailure, explainNpmFailure, findClaudeBinary, guardKeyRotation, inject, keyNote, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, npmCandidates, parseBillingSubscription, parseBillingUsage, parseClaudeResult, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, readClaudeStreamEvents, readSearchChoice, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, statusHostOrder, summarizeToolUse };
|
|
6539
|
+
export { AUTO_CLAUDE_MODE, CHANNEL, CLAUDE_CLI_MODES, CLAUDE_INSTALL_COMMAND, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLAUDE_TOOL_INSTRUCTIONS, CLI_BACKED_MODEL_IDS, CLI_IMAGE_LIMITS, Config, DEFAULT_API_HOST, DEFAULT_APPEND_SYSTEM_PROMPT, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DISPLAY_NAME, LEGACY_STATUS_HOSTS, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TITLE_MODEL_ID, TITLE_MODEL_ROUTES, accountHostOrder, apply, assertApiKey, buildClaudeArgs, buildClaudeEnvironment, buildClaudeInput, buildDelegatedContent, buildSearchRequest, capClaudeMode, catalogCoverage, classifyClientPolicy, classifyGatewayFailure, claudeBinaryCandidates, claudeModeFor, claudeModeForSession, cliSearchInstructions, createBalanceReader, createClaudeCliAdapter, createClaudeCliManager, createClaudeCodeTool, createClaudeRunner, createClaudeSearchBridge, createGroupReader, createKeyChooser, createPackyCodeAutoSearchProvider, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, delegationInstructions, describeRunKey, explainCliClientPolicyFailure, explainCliGroupFailure, explainClientPolicyFailure, explainNpmFailure, findClaudeBinary, guardKeyRotation, inject, keyNote, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, npmCandidates, parseBillingSubscription, parseBillingUsage, parseClaudeResult, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, planModeForSession, readClaudeStreamEvents, readSearchChoice, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, statusHostOrder, summarizeToolUse };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-packycode-api",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"packageManager": "pnpm@10.32.1",
|
|
5
5
|
"description": "Use a PackyCode (PackyAPI) API key in DeepSeek Harness through a built-in multi-protocol model route with key rotation and balance display",
|
|
6
6
|
"type": "module",
|