dsh-packycode-api 1.1.2 → 1.1.4
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 +15 -0
- package/README.en.md +5 -3
- package/README.md +5 -3
- package/lib/index.js +422 -85
- package/package.json +7 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.4
|
|
4
|
+
|
|
5
|
+
- **Carry the reviewed plan into the next CLI turn.** The plan previously existed only in `exit_plan_mode` arguments, which the delegated transcript dropped; a fresh CLI then saw only “Plan approved”. The latest complete plan and its matching review result are now recovered from durable native or adapter-generated PTC tool calls, so approval, revision, resume and fork keep their constraints. They are reserved before trimming older chat; an oversized plan/request fails explicitly rather than silently losing constraints. Unrelated tool arguments and arbitrary PTC programs are not forwarded.
|
|
6
|
+
- **Use the actual CLI permission protocol.** Plan runs now request `--permission-prompt-tool stdio` as well as `--permission-mode plan`, making `ExitPlanMode` available to the official print-mode CLI and routing its permission request to the host. A targeted ask rule prevents an existing `ExitPlanMode` allow rule from skipping the handoff. Repeated permission refusals are bounded, including native plan-mode tool-result denials, without counting ordinary execution errors or counting one refusal twice.
|
|
7
|
+
- **Plans belong to one invocation.** Each plan run creates one private temporary planning-artifact directory inside its working directory (the official CLI rejects custom plan directories outside the project root), and supplies it through additive `plansDirectory` settings while preserving the credential/config home and other permission/MCP settings. It never changes project code or project settings; an unwritable workspace fails explicitly rather than using a shared fallback. File fallback and submitted paths are restricted to that directory; shared/global plans and a prior run's recent file are never candidates. Owned resources are cleaned on completion, cancellation, startup failure and unload.
|
|
8
|
+
- **Review follows the host's tool presentation.** Native catalogs receive `exit_plan_mode`; PTC catalogs receive the real `run_code` transport calling `tools.exit_plan_mode`. Missing review transports fail closed instead of inventing an unavailable direct tool call.
|
|
9
|
+
- **Leave plan mode at the correct boundary.** Model preparation can follow a selected pending mode, but `claude_code` execution reads the currently committed mode. Approval's pending exit no longer gives later tools in the same batch edit/full permission before the next accepted step.
|
|
10
|
+
- **Secret-free release preflight.** The OIDC diagnostic now prints only identity claims and the registry exchange status, never the returned credential body, and fails when the publisher is rejected. Truncating a body before regex redaction could previously expose a token prefix. Added regressions for long credential bodies and the registry's successful `201` response. Tag publication now packs and publishes the verified versioned tarball, matching the locally inspected artifact rather than independently repacking the workspace.
|
|
11
|
+
- Added `scripts/cli-plan-check.mjs`, a real-official-CLI/local-fake-gateway check for stdio handoff, private plan files, concurrent ownership, native denials, MCP search and cleanup. It uses no real credentials or quota and does not start DSH.
|
|
12
|
+
- Validation: full offline suite, both bundles and the versioned tarball; official CLI 2.1.295 against local fixtures passed stdio plan handoff, existing-allow-rule interception, concurrent/private plan ownership, native denial limits, MCP and cleanup, plus the existing read-only/edit/full search checks. Actual bundled DSH 0.2.0-rc.2 plan/tool controllers and its Node PTC subprocess passed 50 offline assertions. The built HTTP route answered through `deepseek-flash`, `gpt-6.1-sol` and `gpt-6-astra`, and key rotation and secret-free RPC projections passed the gateway check. The available key does not cover Claude: actual Claude upstream and a running desktop DSH UI integration are not claimed.
|
|
13
|
+
|
|
14
|
+
## 1.1.3
|
|
15
|
+
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
3
18
|
## 1.1.2
|
|
4
19
|
|
|
5
20
|
- **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.
|
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.4`.
|
|
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.4`. 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,7 +153,7 @@ 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`. DSH plan mode wins over that mapping and over the pin: the CLI runs with `--permission-mode plan`, and `ExitPlanMode`
|
|
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 read-only with `--permission-mode plan --permission-prompt-tool stdio`, using one private hidden temporary plan directory inside the working directory per invocation (the CLI rejects custom plan directories outside the project root). This stores only planning artifacts, changes no project code or settings, and fails explicitly for an unwritable workspace. `ExitPlanMode` hands the plan to DSH for review; it never starts implementation in that CLI process. File fallback reads only this invocation's directory, which is cleaned afterwards. Review works in native and PTC tool modes, and the complete plan and review feedback reach the next turn; exceeding the conversation budget fails explicitly instead of silently truncating constraints. Approval or `/plan off` takes effect from the next step; delegations in the same tool batch keep the current plan state. 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.
|
|
@@ -214,6 +214,8 @@ pnpm run check
|
|
|
214
214
|
|
|
215
215
|
`node scripts/cli-search-check.mjs` runs the real CLI against a local test gateway and MCP bridge; Claude responses and search data are mocked by default, with no quota consumed. It checks all three permission modes, native-search exclusion and temporary-resource cleanup. `--live --backend deepseek` or `--live --backend gpt` sends the read-only mode's one search to the real backend (edit/full remain mocked). This does not verify actual Claude upstream turns or a running DSH integration.
|
|
216
216
|
|
|
217
|
+
`node scripts/cli-plan-check.mjs` checks plan mode with the real CLI and a local fake gateway: stdio handoff, concurrent plan ownership, invocation-only file fallback, stale-plan exclusion, actual denial limits and cleanup. It reads no real credentials, spends no quota and starts no DSH instance.
|
|
218
|
+
|
|
217
219
|
Read [AGENTS.md](AGENTS.md) before agent-assisted changes or a release. Publishing is the [Publish workflow](.github/workflows/publish.yml), triggered by pushing a `v*` tag. Do not run `npm publish` locally.
|
|
218
220
|
|
|
219
221
|
## License
|
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.4`。
|
|
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.4`。不要对 `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,7 +157,7 @@ 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`。DSH 计划模式优先于该映射和这个固定档:CLI 以 `--permission-mode plan`
|
|
160
|
+
- CLI 权限默认跟随当前会话:`read-only` → 只读,`workspace-write` → 可编辑,`danger-full-access` 且不再审批 → `full`。CLI 不能向 DSH 弹审批,所以只要会话还要审批,就不会给它 `full`。也可把设置项 `claudeCliMode` 固定成 `read-only` / `edit` / `full`。DSH 计划模式优先于该映射和这个固定档:CLI 以 `--permission-mode plan --permission-prompt-tool stdio` 只读探索,每轮在工作目录内创建一个独占的隐藏临时计划目录(CLI 不接受工作区外的自定义计划目录);只存计划工件,不改项目代码或设置,工作区不可写时明确失败。`ExitPlanMode` 交给 DSH 审阅,不允许 CLI 在同一进程开始实施;只在本轮目录中查找未显式提交的计划,结束时清理。原生和 PTC 工具模式都支持审阅,完整计划及审阅反馈会带到下一轮;超过对话预算会明确报错,不静默截断约束。批准或 `/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 读取。
|
|
@@ -218,6 +218,8 @@ pnpm run check
|
|
|
218
218
|
|
|
219
219
|
Claude CLI 的 MCP 接口验证用 `node scripts/cli-search-check.mjs`:运行真实 CLI,普通 Claude 响应和搜索数据默认由本地测试服务模拟,不消费额度;会检查三种权限模式、原生搜索禁用和临时资源清理。加 `--live --backend deepseek` 或 `--live --backend gpt` 会让只读模式的那一次搜索走真实后端(其它两种模式仍模拟)。这不等于验证了真实 Claude 上游对话或运行中的 DSH 集成。
|
|
220
220
|
|
|
221
|
+
计划模式另用 `node scripts/cli-plan-check.mjs` 验证:真实 CLI 对接本地模拟网关,检查 stdio 计划提交、并发计划归属、本轮文件回退、旧计划隔离、实际拒绝次数上限和清理;不读取真实凭据、不消费额度、不启动 DSH。
|
|
222
|
+
|
|
221
223
|
使用 Agent 修改或发布时,先读 [AGENTS.md](AGENTS.md)。发布由推送 `v*` 标签触发 [Publish workflow](.github/workflows/publish.yml),不要在本机执行 `npm publish`。
|
|
222
224
|
|
|
223
225
|
## License
|
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, sep, win32 } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, join, posix, resolve, 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, realpathSync, statSync } from "node:fs";
|
|
7
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, 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";
|
|
@@ -1275,12 +1275,104 @@ function hostPath(lookup) {
|
|
|
1275
1275
|
return;
|
|
1276
1276
|
}
|
|
1277
1277
|
}
|
|
1278
|
+
/** The only PTC program this adapter emits; its JSON literal also works in Python. */
|
|
1279
|
+
const PLAN_REVIEW_PROGRAM_PREFIX = "return await tools.exit_plan_mode(";
|
|
1280
|
+
/** Parse durable arguments without evaluating model-authored code. */
|
|
1281
|
+
function parsedArguments(value) {
|
|
1282
|
+
try {
|
|
1283
|
+
return typeof value === "string" ? JSON.parse(value) : value;
|
|
1284
|
+
} catch {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Recover only the public plan, never arbitrary tool arguments or PTC code.
|
|
1290
|
+
* The PTC allowlist is deliberately the exact, single-call program we emit.
|
|
1291
|
+
*/
|
|
1292
|
+
function planOfCall(block) {
|
|
1293
|
+
if (block?.type !== "tool-call") return void 0;
|
|
1294
|
+
const args = parsedArguments(block.arguments);
|
|
1295
|
+
let plan;
|
|
1296
|
+
if (block.name === "exit_plan_mode") plan = args?.plan;
|
|
1297
|
+
else if (block.name === "run_code" && typeof args?.code === "string") {
|
|
1298
|
+
const code = args.code;
|
|
1299
|
+
if (!code.startsWith(PLAN_REVIEW_PROGRAM_PREFIX) || !code.endsWith(")")) return void 0;
|
|
1300
|
+
const review = parsedArguments(code.slice(34, -1));
|
|
1301
|
+
if (review === null || typeof review !== "object" || Object.keys(review).length !== 1) return void 0;
|
|
1302
|
+
plan = review.plan;
|
|
1303
|
+
}
|
|
1304
|
+
return typeof plan === "string" && plan.trim() !== "" ? plan : void 0;
|
|
1305
|
+
}
|
|
1306
|
+
/** Keep the latest complete plan and its review from the persisted model transcript. */
|
|
1307
|
+
function planReviewContext(messages) {
|
|
1308
|
+
let latest;
|
|
1309
|
+
for (const message of messages) {
|
|
1310
|
+
if (message?.role !== "assistant") continue;
|
|
1311
|
+
for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
1312
|
+
const plan = planOfCall(block);
|
|
1313
|
+
if (plan !== void 0) latest = {
|
|
1314
|
+
plan,
|
|
1315
|
+
id: block.id
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (latest === void 0) return void 0;
|
|
1320
|
+
let review;
|
|
1321
|
+
for (const message of messages) {
|
|
1322
|
+
if (message?.role !== "user") continue;
|
|
1323
|
+
for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
1324
|
+
if (block?.type !== "tool-result" || latest.id === void 0 || block.toolCallId !== latest.id) continue;
|
|
1325
|
+
const text = typeof block.content === "string" ? block.content : (Array.isArray(block.content) ? block.content : []).filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
|
|
1326
|
+
review = `[计划审阅${block.isError === true ? "未批准/失败" : "结果"}]\n${text}`;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return {
|
|
1330
|
+
id: latest.id,
|
|
1331
|
+
turn: {
|
|
1332
|
+
label: "计划审阅上下文",
|
|
1333
|
+
parts: [{
|
|
1334
|
+
kind: "text",
|
|
1335
|
+
text: `[最近提交的完整计划;提交本身不代表已批准]\n${latest.plan}\n\n${review ?? "[尚无计划审阅结果;不要把提交视为实施许可]"}`
|
|
1336
|
+
}]
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Select the review transport from this request's actual direct-tool catalog.
|
|
1342
|
+
* `run_code` binds `tools` in both supported host language flavors.
|
|
1343
|
+
*/
|
|
1344
|
+
function planReviewCall(options, plan) {
|
|
1345
|
+
if (plan.trim() === "") throw new Error("Claude Code submitted an empty plan; no review or implementation was authorized.");
|
|
1346
|
+
const context = planReviewContext([{
|
|
1347
|
+
role: "assistant",
|
|
1348
|
+
content: [{
|
|
1349
|
+
type: "tool-call",
|
|
1350
|
+
name: "exit_plan_mode",
|
|
1351
|
+
arguments: { plan }
|
|
1352
|
+
}]
|
|
1353
|
+
}]).turn;
|
|
1354
|
+
if (textLength(context.parts) + context.label.length + 3 >= MAX_PROMPT_CHARS$1) throw new Error("The submitted plan exceeds the CLI delegation budget (48,000 characters). Shorten or split it before review; no plan constraints were truncated and no implementation was authorized.");
|
|
1355
|
+
const tools = Array.isArray(options?.tools) ? options.tools : [];
|
|
1356
|
+
if (tools.some((tool) => tool?.name === "exit_plan_mode")) return {
|
|
1357
|
+
name: "exit_plan_mode",
|
|
1358
|
+
args: JSON.stringify({ plan })
|
|
1359
|
+
};
|
|
1360
|
+
if (tools.some((tool) => tool?.name === "run_code")) return {
|
|
1361
|
+
name: "run_code",
|
|
1362
|
+
args: JSON.stringify({
|
|
1363
|
+
code: `${PLAN_REVIEW_PROGRAM_PREFIX}${JSON.stringify({ plan })})`,
|
|
1364
|
+
description: "Present completed plan for user review"
|
|
1365
|
+
})
|
|
1366
|
+
};
|
|
1367
|
+
throw new Error("Claude Code submitted a plan, but this request has neither exit_plan_mode nor run_code available for host review; no implementation was authorized.");
|
|
1368
|
+
}
|
|
1278
1369
|
/**
|
|
1279
1370
|
* Flatten one message's content into ordered parts: text, image, and file.
|
|
1280
1371
|
* Tool calls and results become short text so a turn that started on another
|
|
1281
|
-
* model still reads coherently; reasoning is not carried.
|
|
1372
|
+
* model still reads coherently; reasoning is not carried. The latest review
|
|
1373
|
+
* result is carried in full separately, not through the general 8k tool cap.
|
|
1282
1374
|
*/
|
|
1283
|
-
function collectParts(blocks, parts) {
|
|
1375
|
+
function collectParts(blocks, parts, reviewId) {
|
|
1284
1376
|
for (const block of Array.isArray(blocks) ? blocks : []) {
|
|
1285
1377
|
if (block === null || typeof block !== "object") continue;
|
|
1286
1378
|
switch (block.type) {
|
|
@@ -1314,6 +1406,7 @@ function collectParts(blocks, parts) {
|
|
|
1314
1406
|
});
|
|
1315
1407
|
break;
|
|
1316
1408
|
case "tool-result": {
|
|
1409
|
+
if (reviewId !== void 0 && block.toolCallId === reviewId) break;
|
|
1317
1410
|
if (typeof block.content === "string") {
|
|
1318
1411
|
parts.push({
|
|
1319
1412
|
kind: "text",
|
|
@@ -1355,10 +1448,12 @@ const textLength = (parts) => parts.reduce((sum, part) => sum + (part.kind === "
|
|
|
1355
1448
|
*/
|
|
1356
1449
|
function delegatedTurns(options) {
|
|
1357
1450
|
const messages = Array.isArray(options?.messages) ? options.messages : [];
|
|
1451
|
+
const review = planReviewContext(messages);
|
|
1452
|
+
const reserved = review === void 0 ? 0 : textLength(review.turn.parts) + review.turn.label.length + 3;
|
|
1358
1453
|
const turns = [];
|
|
1359
1454
|
for (const message of messages) {
|
|
1360
1455
|
if (message === null || typeof message !== "object" || message.role === "system") continue;
|
|
1361
|
-
const parts = collectParts(message.content, []);
|
|
1456
|
+
const parts = collectParts(message.content, [], review?.id);
|
|
1362
1457
|
if (parts.length === 0 || textLength(parts) === 0 && !parts.some((part) => part.kind !== "text")) continue;
|
|
1363
1458
|
const label = message.role === "user" ? "用户" : message.role === "assistant" ? "助手" : String(message.role);
|
|
1364
1459
|
turns.push({
|
|
@@ -1366,8 +1461,11 @@ function delegatedTurns(options) {
|
|
|
1366
1461
|
parts
|
|
1367
1462
|
});
|
|
1368
1463
|
}
|
|
1464
|
+
const newest = turns.at(-1);
|
|
1465
|
+
const newestSize = newest === void 0 ? 0 : textLength(newest.parts) + newest.label.length + 3;
|
|
1466
|
+
if (review !== void 0 && reserved + newestSize > MAX_PROMPT_CHARS$1) throw new Error("The complete plan, its review, and the latest request exceed the CLI delegation budget (48,000 characters). Shorten or split the plan/request before continuing; plan constraints were not truncated.");
|
|
1369
1467
|
const kept = [];
|
|
1370
|
-
let used =
|
|
1468
|
+
let used = reserved;
|
|
1371
1469
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
1372
1470
|
const turn = turns[index];
|
|
1373
1471
|
const size = textLength(turn.parts) + turn.label.length + 3;
|
|
@@ -1383,9 +1481,11 @@ function delegatedTurns(options) {
|
|
|
1383
1481
|
});
|
|
1384
1482
|
break;
|
|
1385
1483
|
}
|
|
1484
|
+
const truncated = kept.length < turns.length || kept[0]?.clipped === true;
|
|
1485
|
+
if (review !== void 0) kept.unshift(review.turn);
|
|
1386
1486
|
return {
|
|
1387
1487
|
turns: kept,
|
|
1388
|
-
truncated
|
|
1488
|
+
truncated
|
|
1389
1489
|
};
|
|
1390
1490
|
}
|
|
1391
1491
|
/** Keep the last `budget` characters of a turn's text, and every part after them. */
|
|
@@ -1430,7 +1530,7 @@ const plainDescription = (part) => part.kind === "image" ? `[图片 ${attachment
|
|
|
1430
1530
|
function delegationInstructions(options, cwd, { plan = false } = {}) {
|
|
1431
1531
|
const system = typeof options?.system === "string" && options.system.trim() !== "" ? options.system.trim() : "";
|
|
1432
1532
|
return [
|
|
1433
|
-
plan ? "本回合 DeepSeek Harness 处于计划模式。只做只读探索,把完整实施计划写成以一级标题开头的 Markdown,写入 CLI
|
|
1533
|
+
plan ? "本回合 DeepSeek Harness 处于计划模式。只做只读探索,把完整实施计划写成以一级标题开头的 Markdown,写入 CLI 指定的计划文件。若有 ExitPlanMode 就调用它;没有也在写完后停下来。不要修改项目、不要开始实施。DeepSeek Harness 会用这份计划打开审阅。用户消息是 DeepSeek Harness 会话的近期对话记录,最后一条是当前请求;其中的图片已作为图片随消息附上,「只读副本」路径也可以用 Read 工具读取。" : "本回合由 DeepSeek Harness 委派给 Claude Code 执行。用户消息是 DeepSeek Harness 会话的近期对话记录,最后一条是当前请求;其中的图片已作为图片随消息附上,「只读副本」路径也可以用 Read 工具读取。请用你自己的工具完成任务,直接输出最终结果;不要反问、不要复述流程。",
|
|
1434
1534
|
cwd === void 0 ? void 0 : `工作目录:${cwd}`,
|
|
1435
1535
|
system === "" ? void 0 : `补充说明(来自 DeepSeek Harness,其中提到的工具你未必拥有):${system.slice(0, 2e3)}`
|
|
1436
1536
|
].filter((part) => part !== void 0).join("\n");
|
|
@@ -1898,7 +1998,8 @@ function createClaudeCliAdapter({ inner, runner, modelIds = CLI_BACKED_MODEL_IDS
|
|
|
1898
1998
|
default: break;
|
|
1899
1999
|
}
|
|
1900
2000
|
if (submittedPlan !== void 0) {
|
|
1901
|
-
|
|
2001
|
+
const review = planReviewCall(options, submittedPlan);
|
|
2002
|
+
yield* blocks.toolCall(`plan-${randomUUID()}`, review.name, review.args);
|
|
1902
2003
|
yield {
|
|
1903
2004
|
type: "usage",
|
|
1904
2005
|
usage: turnUsage(lastRequest, finished)
|
|
@@ -2304,10 +2405,11 @@ function resolveSpawnCommand(binary, { platform = process.platform, exists = exi
|
|
|
2304
2405
|
* consumes the streaming protocol, and instructions appended to the CLI's
|
|
2305
2406
|
* own system prompt ({@link DEFAULT_APPEND_SYSTEM_PROMPT} when blank). The
|
|
2306
2407
|
* CLI's system prompt and tool set are never replaced: they are part of
|
|
2307
|
-
* what the official client sends.
|
|
2408
|
+
* what the official client sends. Plan runs supply an absolute private
|
|
2409
|
+
* planDirectory below the child's working directory.
|
|
2308
2410
|
* @returns argv after the binary name.
|
|
2309
2411
|
*/
|
|
2310
|
-
function buildClaudeArgs({ mode = DEFAULT_CLAUDE_MODE, model, stream = false, appendSystemPrompt, searchBridge, disableNativeSearch = false } = {}) {
|
|
2412
|
+
function buildClaudeArgs({ mode = DEFAULT_CLAUDE_MODE, model, stream = false, appendSystemPrompt, searchBridge, disableNativeSearch = false, planDirectory } = {}) {
|
|
2311
2413
|
const spec = CLAUDE_CLI_MODES[mode];
|
|
2312
2414
|
if (spec === void 0) throw new Error(`claude_code mode must be one of ${Object.keys(CLAUDE_CLI_MODES).join(", ")}`);
|
|
2313
2415
|
const args = stream ? [
|
|
@@ -2334,16 +2436,31 @@ function buildClaudeArgs({ mode = DEFAULT_CLAUDE_MODE, model, stream = false, ap
|
|
|
2334
2436
|
args.push("--permission-mode", spec.permissionMode);
|
|
2335
2437
|
if (searchBridge !== void 0) args.push("--allowedTools", searchBridge.toolName);
|
|
2336
2438
|
} else args.push("--allowedTools", ...spec.allowedTools, ...searchBridge === void 0 ? [] : [searchBridge.toolName]);
|
|
2439
|
+
if (mode === "plan") {
|
|
2440
|
+
args.push("--permission-prompt-tool", "stdio");
|
|
2441
|
+
if (planDirectory !== void 0 && (typeof planDirectory !== "string" || !isAbsolute(planDirectory))) throw new Error("claude_code plan directory must be an absolute path");
|
|
2442
|
+
args.push("--settings", JSON.stringify({
|
|
2443
|
+
...planDirectory === void 0 ? {} : { plansDirectory: planDirectory },
|
|
2444
|
+
permissions: { ask: ["ExitPlanMode"] }
|
|
2445
|
+
}));
|
|
2446
|
+
}
|
|
2337
2447
|
if (disableNativeSearch || searchBridge !== void 0) args.push("--disallowedTools", "WebSearch");
|
|
2338
2448
|
if (searchBridge !== void 0) args.push("--mcp-config", searchBridge.configPath);
|
|
2339
2449
|
return args;
|
|
2340
2450
|
}
|
|
2341
|
-
/** How many permission
|
|
2451
|
+
/** How many permission denials one plan-mode run may recover from. */
|
|
2342
2452
|
const MAX_PLAN_DENIALS = 8;
|
|
2343
|
-
/** A plan file larger than this is not
|
|
2453
|
+
/** A plan file or submitted plan larger than this is not returned into the turn. */
|
|
2344
2454
|
const PLAN_FILE_MAX_BYTES = 2e5;
|
|
2345
|
-
const
|
|
2455
|
+
const PLAN_DIRECTORY_PREFIX = ".dsh-packycode-plan-";
|
|
2456
|
+
const PLAN_FS = Object.freeze({
|
|
2457
|
+
mkdtempSync,
|
|
2458
|
+
realpathSync,
|
|
2459
|
+
lstatSync,
|
|
2460
|
+
rmSync
|
|
2461
|
+
});
|
|
2346
2462
|
const PLAN_EXIT_DENY = "Write the plan to the plan file first, starting with a # heading, then call ExitPlanMode again.";
|
|
2463
|
+
const PLAN_TOO_LARGE_DENY = "The plan exceeds the 200000-byte review limit. Write a complete, shorter plan without dropping constraints, then call ExitPlanMode again.";
|
|
2347
2464
|
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
2465
|
const PLAN_DIALOG_ERROR = "Plan mode cannot ask the user from the CLI. Decide and continue, or call ExitPlanMode.";
|
|
2349
2466
|
const PLAN_DENY_LIMIT_MESSAGE = "claude_code plan mode stopped after too many denied permission prompts";
|
|
@@ -2357,11 +2474,58 @@ function planMarkdown(value) {
|
|
|
2357
2474
|
if (typeof value !== "string") return void 0;
|
|
2358
2475
|
const trimmed = value.trim();
|
|
2359
2476
|
if (trimmed === "") return void 0;
|
|
2360
|
-
const
|
|
2361
|
-
return
|
|
2477
|
+
const markdown = /^#\s+\S/u.test(trimmed) ? trimmed : `# Plan\n\n${trimmed}`;
|
|
2478
|
+
return Buffer.byteLength(markdown) > PLAN_FILE_MAX_BYTES ? void 0 : markdown;
|
|
2479
|
+
}
|
|
2480
|
+
/** Own exactly one newly created, owner-only directory, never a shared CLI home. */
|
|
2481
|
+
function createOwnedPlanDirectory(fs, cwd) {
|
|
2482
|
+
const parent = fs.realpathSync(cwd ?? process.cwd());
|
|
2483
|
+
const directory = fs.mkdtempSync(join(parent, PLAN_DIRECTORY_PREFIX));
|
|
2484
|
+
if (!isAbsolute(directory) || dirname(directory) !== parent || !directory.startsWith(join(parent, PLAN_DIRECTORY_PREFIX))) throw new Error("PackyCode plan directory ownership could not be established");
|
|
2485
|
+
const identity = fs.lstatSync(directory);
|
|
2486
|
+
if (!identity.isDirectory() || identity.isSymbolicLink()) throw new Error("PackyCode plan directory ownership could not be established");
|
|
2487
|
+
const canonical = directory;
|
|
2488
|
+
let removed = false;
|
|
2489
|
+
const isOwned = () => {
|
|
2490
|
+
try {
|
|
2491
|
+
const current = fs.lstatSync(directory);
|
|
2492
|
+
return current.isDirectory() && !current.isSymbolicLink() && current.dev === identity.dev && current.ino === identity.ino && fs.realpathSync(directory) === canonical;
|
|
2493
|
+
} catch {
|
|
2494
|
+
return false;
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
const owned = {
|
|
2498
|
+
directory: canonical,
|
|
2499
|
+
isOwned,
|
|
2500
|
+
close() {
|
|
2501
|
+
if (removed) return;
|
|
2502
|
+
let current;
|
|
2503
|
+
try {
|
|
2504
|
+
current = fs.lstatSync(directory);
|
|
2505
|
+
} catch (error) {
|
|
2506
|
+
if (error?.code === "ENOENT") {
|
|
2507
|
+
removed = true;
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
throw error;
|
|
2511
|
+
}
|
|
2512
|
+
if (!current.isDirectory() || current.isSymbolicLink() || !isOwned()) throw new Error("PackyCode plan directory ownership changed; refusing to remove it");
|
|
2513
|
+
fs.rmSync(canonical, {
|
|
2514
|
+
recursive: true,
|
|
2515
|
+
force: true
|
|
2516
|
+
});
|
|
2517
|
+
removed = true;
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
if (!isOwned()) {
|
|
2521
|
+
owned.close();
|
|
2522
|
+
throw new Error("PackyCode plan directory ownership could not be established");
|
|
2523
|
+
}
|
|
2524
|
+
return owned;
|
|
2362
2525
|
}
|
|
2363
|
-
/** Whether
|
|
2526
|
+
/** Whether a file stays inside the immutable canonical directory bound to the run. */
|
|
2364
2527
|
function pathInside(root, file, realpath) {
|
|
2528
|
+
if (typeof root !== "string" || !isAbsolute(root) || typeof file !== "string" || !isAbsolute(file)) return false;
|
|
2365
2529
|
let rootReal;
|
|
2366
2530
|
let fileReal;
|
|
2367
2531
|
try {
|
|
@@ -2370,42 +2534,104 @@ function pathInside(root, file, realpath) {
|
|
|
2370
2534
|
} catch {
|
|
2371
2535
|
return false;
|
|
2372
2536
|
}
|
|
2537
|
+
if (rootReal !== resolve(root)) return false;
|
|
2373
2538
|
const prefix = rootReal.endsWith(sep) ? rootReal : `${rootReal}${sep}`;
|
|
2374
|
-
return fileReal
|
|
2539
|
+
return fileReal.startsWith(prefix);
|
|
2540
|
+
}
|
|
2541
|
+
/** A descriptor-based bounded read also rejects a replaced leaf before reading it. */
|
|
2542
|
+
function readBoundedPlanFile(file, expected) {
|
|
2543
|
+
const fd = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
2544
|
+
try {
|
|
2545
|
+
const info = fstatSync(fd);
|
|
2546
|
+
if (!info.isFile() || expected !== void 0 && (info.dev !== expected.dev || info.ino !== expected.ino)) return void 0;
|
|
2547
|
+
if (info.size > PLAN_FILE_MAX_BYTES) throw Object.assign(/* @__PURE__ */ new Error(PLAN_TOO_LARGE_DENY), { code: "CLAUDE_PLAN_TOO_LARGE" });
|
|
2548
|
+
const bytes = Buffer.allocUnsafe(200001);
|
|
2549
|
+
let length = 0;
|
|
2550
|
+
while (length < bytes.length) {
|
|
2551
|
+
const count = readSync(fd, bytes, length, bytes.length - length, null);
|
|
2552
|
+
if (count === 0) break;
|
|
2553
|
+
length += count;
|
|
2554
|
+
}
|
|
2555
|
+
if (length > PLAN_FILE_MAX_BYTES) throw Object.assign(/* @__PURE__ */ new Error(PLAN_TOO_LARGE_DENY), { code: "CLAUDE_PLAN_TOO_LARGE" });
|
|
2556
|
+
return bytes.subarray(0, length).toString("utf8");
|
|
2557
|
+
} finally {
|
|
2558
|
+
closeSync(fd);
|
|
2559
|
+
}
|
|
2375
2560
|
}
|
|
2376
2561
|
/**
|
|
2377
|
-
* The plan a CLI
|
|
2378
|
-
*
|
|
2379
|
-
*
|
|
2380
|
-
*
|
|
2381
|
-
*
|
|
2382
|
-
*
|
|
2383
|
-
*
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2562
|
+
* The plan a CLI ExitPlanMode request submits. Inline text remains valid; a
|
|
2563
|
+
* file must resolve inside this invocation's explicitly bound canonical
|
|
2564
|
+
* planDirectory. Shared config homes and ~/.claude/plans are never searched.
|
|
2565
|
+
* The caller supplies scrub so secrets are removed before the size check.
|
|
2566
|
+
* @param input - the control request's input.
|
|
2567
|
+
* @param options - the bound private plan directory, redactor, and filesystem seams.
|
|
2568
|
+
* @returns bounded plan markdown, or undefined when the request has none yet.
|
|
2569
|
+
*/
|
|
2570
|
+
function readSubmittedPlan(input, { planDirectory, scrub = (value) => value, onRejected = () => void 0, readFile = readBoundedPlanFile, stat = statSync, realpath = realpathSync } = {}) {
|
|
2571
|
+
if (typeof input?.plan === "string") {
|
|
2572
|
+
const text = scrub(input.plan);
|
|
2573
|
+
const inline = planMarkdown(text);
|
|
2574
|
+
if (inline !== void 0) return inline;
|
|
2575
|
+
if (text.trim() !== "") {
|
|
2576
|
+
onRejected("too-large");
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2392
2580
|
const requested = typeof input?.planFilePath === "string" ? input.planFilePath : "";
|
|
2393
|
-
if (requested
|
|
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;
|
|
2581
|
+
if (!pathInside(planDirectory, requested, realpath)) return void 0;
|
|
2396
2582
|
try {
|
|
2397
|
-
file = realpath(requested);
|
|
2583
|
+
const file = realpath(requested);
|
|
2584
|
+
if (!file.toLowerCase().endsWith(".md")) return void 0;
|
|
2398
2585
|
const info = stat(file);
|
|
2399
|
-
if (!info.isFile()
|
|
2400
|
-
|
|
2586
|
+
if (!info.isFile()) return void 0;
|
|
2587
|
+
if (info.size > PLAN_FILE_MAX_BYTES) {
|
|
2588
|
+
onRejected("too-large");
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
const text = readFile(file, info);
|
|
2592
|
+
if (typeof text !== "string") return void 0;
|
|
2593
|
+
const safe = scrub(text);
|
|
2594
|
+
const plan = planMarkdown(safe);
|
|
2595
|
+
if (plan === void 0 && safe.trim() !== "") onRejected("too-large");
|
|
2596
|
+
return plan;
|
|
2597
|
+
} catch (error) {
|
|
2598
|
+
if (error?.code === "CLAUDE_PLAN_TOO_LARGE") onRejected("too-large");
|
|
2401
2599
|
return;
|
|
2402
2600
|
}
|
|
2403
|
-
|
|
2601
|
+
}
|
|
2602
|
+
/**
|
|
2603
|
+
* A file-only completion from the CLI is accepted only from this run's new
|
|
2604
|
+
* private plans directory. There is no shared-directory/mtime fallback. If
|
|
2605
|
+
* more than one eligible file exists, do not guess which session submitted it.
|
|
2606
|
+
* @param options - the bound private plan directory, redactor, and filesystem seams.
|
|
2607
|
+
* @returns plan markdown, or undefined when absent or ambiguous.
|
|
2608
|
+
*/
|
|
2609
|
+
function findPlanWrittenDuringRun({ planDirectory, scrub = (value) => value, onRejected = () => void 0, readdir = readdirSync, readFile = readBoundedPlanFile, stat = statSync, realpath = realpathSync } = {}) {
|
|
2610
|
+
if (typeof planDirectory !== "string" || !isAbsolute(planDirectory)) return void 0;
|
|
2611
|
+
let names;
|
|
2404
2612
|
try {
|
|
2405
|
-
|
|
2613
|
+
names = readdir(planDirectory);
|
|
2406
2614
|
} catch {
|
|
2407
2615
|
return;
|
|
2408
2616
|
}
|
|
2617
|
+
let found;
|
|
2618
|
+
for (const name of names) {
|
|
2619
|
+
if (typeof name !== "string" || !name.toLowerCase().endsWith(".md")) continue;
|
|
2620
|
+
const candidate = join(planDirectory, name);
|
|
2621
|
+
if (dirname(candidate) !== planDirectory) continue;
|
|
2622
|
+
const text = readSubmittedPlan({ planFilePath: candidate }, {
|
|
2623
|
+
planDirectory,
|
|
2624
|
+
scrub,
|
|
2625
|
+
onRejected,
|
|
2626
|
+
readFile,
|
|
2627
|
+
stat,
|
|
2628
|
+
realpath
|
|
2629
|
+
});
|
|
2630
|
+
if (text === void 0) continue;
|
|
2631
|
+
if (found !== void 0) return void 0;
|
|
2632
|
+
found = text;
|
|
2633
|
+
}
|
|
2634
|
+
return found;
|
|
2409
2635
|
}
|
|
2410
2636
|
/**
|
|
2411
2637
|
* Build what the CLI reads from stdin for one run.
|
|
@@ -2534,6 +2760,22 @@ function toolResultText(content) {
|
|
|
2534
2760
|
return content.map((part) => part?.type === "text" && typeof part.text === "string" ? part.text : "").join(" ");
|
|
2535
2761
|
}
|
|
2536
2762
|
/**
|
|
2763
|
+
* Native plan/permission gates sometimes produce no control_request at all.
|
|
2764
|
+
* Recognize their specific messages, never generic is_error, ENOENT, EACCES,
|
|
2765
|
+
* a failed search, or a command's nonzero exit. An associated assistant tool
|
|
2766
|
+
* use is required, so arbitrary user text cannot spend the denial budget.
|
|
2767
|
+
*/
|
|
2768
|
+
function nativePermissionDenial(name, text) {
|
|
2769
|
+
if (typeof name !== "string") return false;
|
|
2770
|
+
if ([
|
|
2771
|
+
"Write",
|
|
2772
|
+
"Edit",
|
|
2773
|
+
"NotebookEdit"
|
|
2774
|
+
].includes(name) && /^Cannot (?:write to|edit|modify) [\s\S]+ while in plan mode\.(?:\s|$)/u.test(text)) return true;
|
|
2775
|
+
if (["Bash", "PowerShell"].includes(name) && text.startsWith("Auto mode could not evaluate this action and is blocking it for safety")) return true;
|
|
2776
|
+
return text.startsWith(PLAN_ACTION_DENY) || text.startsWith(PLAN_EXIT_DENY) || /^Permission to use [^\n]+ (?:has been|was) denied\b/u.test(text) || /^Claude requested permissions to use [^\n]+, but you haven['’]t granted it yet\b/u.test(text) || /^The user doesn['’]t want to proceed with this tool use\b/u.test(text);
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2537
2779
|
* Read one `--output-format stream-json` line.
|
|
2538
2780
|
*
|
|
2539
2781
|
* The CLI prints both partial deltas (`stream_event`) and each finished
|
|
@@ -2656,7 +2898,7 @@ function describeRunKey(credential) {
|
|
|
2656
2898
|
* seams.
|
|
2657
2899
|
* @returns `{ run(options), stream(options) }`.
|
|
2658
2900
|
*/
|
|
2659
|
-
function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_CLAUDE_CONFIG_DIR, resolveCredential, spawnImpl = spawn, timeoutMs = DEFAULT_CLAUDE_TIMEOUT_MS, startupTimeoutMs = DEFAULT_CLAUDE_STARTUP_TIMEOUT_MS, platform = process.platform, createSearchBridge } = {}) {
|
|
2901
|
+
function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_CLAUDE_CONFIG_DIR, resolveCredential, spawnImpl = spawn, timeoutMs = DEFAULT_CLAUDE_TIMEOUT_MS, startupTimeoutMs = DEFAULT_CLAUDE_STARTUP_TIMEOUT_MS, platform = process.platform, createSearchBridge, planFs = PLAN_FS } = {}) {
|
|
2660
2902
|
if (typeof resolveCredential !== "function") throw new Error("the Claude Code CLI runner needs resolveCredential");
|
|
2661
2903
|
const owners = /* @__PURE__ */ new Set();
|
|
2662
2904
|
let disposed = false;
|
|
@@ -2689,8 +2931,8 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2689
2931
|
settled: false,
|
|
2690
2932
|
denies: 0
|
|
2691
2933
|
};
|
|
2692
|
-
const startedAt = Date.now();
|
|
2693
2934
|
let child;
|
|
2935
|
+
let plans;
|
|
2694
2936
|
let bridge;
|
|
2695
2937
|
let bridgeClosing;
|
|
2696
2938
|
let pendingBridge;
|
|
@@ -2743,13 +2985,18 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2743
2985
|
owners.delete(owner);
|
|
2744
2986
|
done();
|
|
2745
2987
|
});
|
|
2988
|
+
plans?.close();
|
|
2746
2989
|
return;
|
|
2747
2990
|
}
|
|
2748
2991
|
try {
|
|
2749
2992
|
await closeBridge();
|
|
2750
2993
|
} finally {
|
|
2751
|
-
|
|
2752
|
-
|
|
2994
|
+
try {
|
|
2995
|
+
plans?.close();
|
|
2996
|
+
} finally {
|
|
2997
|
+
owners.delete(owner);
|
|
2998
|
+
done();
|
|
2999
|
+
}
|
|
2753
3000
|
}
|
|
2754
3001
|
};
|
|
2755
3002
|
const checkAborted = () => {
|
|
@@ -2766,6 +3013,10 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2766
3013
|
}
|
|
2767
3014
|
};
|
|
2768
3015
|
try {
|
|
3016
|
+
if (mode === "plan") {
|
|
3017
|
+
plans = createOwnedPlanDirectory(planFs, cwd);
|
|
3018
|
+
state.planDirectory = plans.directory;
|
|
3019
|
+
}
|
|
2769
3020
|
const credential = await setup(resolveCredential({
|
|
2770
3021
|
model,
|
|
2771
3022
|
signal: runSignal
|
|
@@ -2800,7 +3051,8 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2800
3051
|
stream,
|
|
2801
3052
|
appendSystemPrompt,
|
|
2802
3053
|
searchBridge: bridge,
|
|
2803
|
-
disableNativeSearch: typeof createSearchBridge === "function"
|
|
3054
|
+
disableNativeSearch: typeof createSearchBridge === "function",
|
|
3055
|
+
planDirectory: plans?.directory
|
|
2804
3056
|
});
|
|
2805
3057
|
child = spawnImpl(command.command, [...command.args, ...args], {
|
|
2806
3058
|
cwd,
|
|
@@ -2831,17 +3083,26 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2831
3083
|
child.stderr?.setEncoding?.("utf8");
|
|
2832
3084
|
let controlBuffer = "";
|
|
2833
3085
|
let stdinEnded = false;
|
|
3086
|
+
const onStdinError = () => {
|
|
3087
|
+
if (state.settled || runSignal.aborted || typeof state.plan === "string") return;
|
|
3088
|
+
state.stdinFailed = true;
|
|
3089
|
+
kill();
|
|
3090
|
+
};
|
|
2834
3091
|
const endStdin = () => {
|
|
2835
3092
|
if (stdinEnded) return;
|
|
2836
3093
|
stdinEnded = true;
|
|
2837
3094
|
try {
|
|
2838
3095
|
child.stdin?.end?.();
|
|
2839
|
-
} catch {
|
|
3096
|
+
} catch {
|
|
3097
|
+
onStdinError();
|
|
3098
|
+
}
|
|
2840
3099
|
};
|
|
2841
3100
|
const writeStdin = (frame) => {
|
|
2842
3101
|
try {
|
|
2843
3102
|
child.stdin?.write?.(frame);
|
|
2844
|
-
} catch {
|
|
3103
|
+
} catch {
|
|
3104
|
+
onStdinError();
|
|
3105
|
+
}
|
|
2845
3106
|
};
|
|
2846
3107
|
const controlResponse = (requestId, response) => {
|
|
2847
3108
|
writeStdin(`${JSON.stringify({
|
|
@@ -2858,12 +3119,22 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2858
3119
|
endStdin();
|
|
2859
3120
|
kill();
|
|
2860
3121
|
};
|
|
2861
|
-
const
|
|
2862
|
-
|
|
3122
|
+
const toolUses = /* @__PURE__ */ new Map();
|
|
3123
|
+
const deniedTools = /* @__PURE__ */ new Set();
|
|
3124
|
+
const deniedRequests = /* @__PURE__ */ new Set();
|
|
3125
|
+
const countDenial = (requestId, toolId) => {
|
|
3126
|
+
const duplicate = requestId !== void 0 && deniedRequests.has(requestId) || toolId !== void 0 && deniedTools.has(toolId);
|
|
3127
|
+
if (requestId !== void 0) deniedRequests.add(requestId);
|
|
3128
|
+
if (toolId !== void 0) deniedTools.add(toolId);
|
|
3129
|
+
if (!duplicate) state.denies += 1;
|
|
2863
3130
|
if (state.denies > MAX_PLAN_DENIALS) {
|
|
2864
3131
|
stopForDenials();
|
|
2865
|
-
return;
|
|
3132
|
+
return false;
|
|
2866
3133
|
}
|
|
3134
|
+
return true;
|
|
3135
|
+
};
|
|
3136
|
+
const refuseControl = (requestId, kind, toolId) => {
|
|
3137
|
+
if (!countDenial(requestId, toolId)) return;
|
|
2867
3138
|
if (kind === "dialog") {
|
|
2868
3139
|
writeStdin(`${JSON.stringify({
|
|
2869
3140
|
type: "control_response",
|
|
@@ -2882,10 +3153,12 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2882
3153
|
};
|
|
2883
3154
|
/**
|
|
2884
3155
|
* Plan mode keeps stdin open so a permission prompt can be answered.
|
|
2885
|
-
*
|
|
2886
|
-
*
|
|
3156
|
+
* ExitPlanMode is never allowed: stop with the submitted plan instead.
|
|
3157
|
+
* Native hard denials share the same budget, keyed by tool_use_id so the
|
|
3158
|
+
* tool_result following a control denial cannot charge it a second time.
|
|
2887
3159
|
*/
|
|
2888
3160
|
const handlePlanLine = (line) => {
|
|
3161
|
+
if (state.denyExhausted === true || typeof state.plan === "string") return;
|
|
2889
3162
|
const trimmed = line.trim();
|
|
2890
3163
|
if (!trimmed.startsWith("{")) return;
|
|
2891
3164
|
let message;
|
|
@@ -2895,44 +3168,64 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2895
3168
|
return;
|
|
2896
3169
|
}
|
|
2897
3170
|
if (message?.type === "result") {
|
|
3171
|
+
state.resultError = message.is_error === true;
|
|
2898
3172
|
endStdin();
|
|
2899
3173
|
return;
|
|
2900
3174
|
}
|
|
3175
|
+
if (state.resultError !== void 0) return;
|
|
3176
|
+
if (message?.type === "assistant") {
|
|
3177
|
+
for (const block of Array.isArray(message.message?.content) ? message.message.content : []) {
|
|
3178
|
+
if (block?.type !== "tool_use" || typeof block.id !== "string") continue;
|
|
3179
|
+
toolUses.set(block.id, {
|
|
3180
|
+
name: block.name,
|
|
3181
|
+
input: block.input
|
|
3182
|
+
});
|
|
3183
|
+
if (toolUses.size > 512) toolUses.delete(toolUses.keys().next().value);
|
|
3184
|
+
}
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
if (message?.type === "user") {
|
|
3188
|
+
for (const block of Array.isArray(message.message?.content) ? message.message.content : []) {
|
|
3189
|
+
if (block?.type !== "tool_result" || block.is_error !== true || typeof block.tool_use_id !== "string") continue;
|
|
3190
|
+
if (nativePermissionDenial(toolUses.get(block.tool_use_id)?.name, toolResultText(block.content).trim()) && !countDenial(void 0, block.tool_use_id)) return;
|
|
3191
|
+
}
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
2901
3194
|
if (message?.type !== "control_request") return;
|
|
2902
3195
|
const requestId = message.request_id;
|
|
2903
3196
|
const request = message.request;
|
|
2904
3197
|
if (typeof requestId !== "string" || request === null || typeof request !== "object") {
|
|
2905
|
-
|
|
2906
|
-
if (state.denies > MAX_PLAN_DENIALS) stopForDenials();
|
|
3198
|
+
countDenial(typeof requestId === "string" ? requestId : void 0, void 0);
|
|
2907
3199
|
return;
|
|
2908
3200
|
}
|
|
3201
|
+
const toolId = typeof request.tool_use_id === "string" ? request.tool_use_id : [...toolUses].reverse().find(([, tool]) => tool.name === request.tool_name && JSON.stringify(tool.input) === JSON.stringify(request.input))?.[0];
|
|
2909
3202
|
if (request.subtype !== "can_use_tool") {
|
|
2910
|
-
refuseControl(requestId, "dialog");
|
|
3203
|
+
refuseControl(requestId, "dialog", toolId);
|
|
2911
3204
|
return;
|
|
2912
3205
|
}
|
|
2913
3206
|
if (request.tool_name === "ExitPlanMode") {
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3207
|
+
let tooLarge = false;
|
|
3208
|
+
const plan = typeof request.input?.plan === "string" || plans?.isOwned() ? readSubmittedPlan(request.input, {
|
|
3209
|
+
planDirectory: plans?.directory,
|
|
3210
|
+
scrub,
|
|
3211
|
+
onRejected: (reason) => {
|
|
3212
|
+
tooLarge = reason === "too-large";
|
|
3213
|
+
}
|
|
3214
|
+
}) : void 0;
|
|
2918
3215
|
if (plan !== void 0) {
|
|
2919
|
-
state.plan =
|
|
3216
|
+
state.plan = plan;
|
|
2920
3217
|
endStdin();
|
|
2921
3218
|
kill();
|
|
2922
3219
|
return;
|
|
2923
3220
|
}
|
|
2924
|
-
|
|
2925
|
-
if (state.denies > MAX_PLAN_DENIALS) {
|
|
2926
|
-
stopForDenials();
|
|
2927
|
-
return;
|
|
2928
|
-
}
|
|
3221
|
+
if (!countDenial(requestId, toolId)) return;
|
|
2929
3222
|
controlResponse(requestId, {
|
|
2930
3223
|
behavior: "deny",
|
|
2931
|
-
message: PLAN_EXIT_DENY
|
|
3224
|
+
message: tooLarge ? PLAN_TOO_LARGE_DENY : PLAN_EXIT_DENY
|
|
2932
3225
|
});
|
|
2933
3226
|
return;
|
|
2934
3227
|
}
|
|
2935
|
-
refuseControl(requestId, "tool");
|
|
3228
|
+
refuseControl(requestId, "tool", toolId);
|
|
2936
3229
|
};
|
|
2937
3230
|
child.stdout?.on("data", (chunk) => {
|
|
2938
3231
|
state.sawOutput = true;
|
|
@@ -2962,18 +3255,32 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2962
3255
|
child.on("close", (code, killSignal) => {
|
|
2963
3256
|
if (state.settled) return;
|
|
2964
3257
|
state.settled = true;
|
|
3258
|
+
if (mode === "plan" && controlBuffer.trim() !== "") handlePlanLine(controlBuffer);
|
|
2965
3259
|
if (state.timedOut) return reject(/* @__PURE__ */ new Error(`claude_code timed out after ${Math.round(timeoutMs / 1e3)}s`));
|
|
2966
3260
|
if (state.silent) {
|
|
2967
3261
|
const detail = state.stderr.trim().split("\n").slice(-4).join(" ").slice(0, 400);
|
|
2968
3262
|
return reject(/* @__PURE__ */ new Error(`Claude Code CLI printed nothing within ${Math.round(startupTimeoutMs / 1e3)}s of starting and was stopped${detail === "" ? "" : `: ${detail}`}`));
|
|
2969
3263
|
}
|
|
2970
3264
|
if (signal?.aborted) return reject(/* @__PURE__ */ new Error("claude_code was aborted"));
|
|
3265
|
+
if (state.stdinFailed === true) return reject(/* @__PURE__ */ new Error("claude_code could not write to CLI input"));
|
|
2971
3266
|
if (state.denyExhausted === true) return reject(/* @__PURE__ */ new Error(PLAN_DENY_LIMIT_MESSAGE));
|
|
2972
3267
|
if (typeof state.plan === "string") return resolve({
|
|
2973
3268
|
code,
|
|
2974
3269
|
killSignal
|
|
2975
3270
|
});
|
|
2976
3271
|
if (runSignal.aborted) return reject(/* @__PURE__ */ new Error("claude_code was aborted"));
|
|
3272
|
+
if (mode === "plan" && code === 0 && state.resultError === false && plans?.isOwned()) {
|
|
3273
|
+
let tooLarge = false;
|
|
3274
|
+
const written = findPlanWrittenDuringRun({
|
|
3275
|
+
planDirectory: plans.directory,
|
|
3276
|
+
scrub,
|
|
3277
|
+
onRejected: (reason) => {
|
|
3278
|
+
tooLarge ||= reason === "too-large";
|
|
3279
|
+
}
|
|
3280
|
+
});
|
|
3281
|
+
if (tooLarge) return reject(/* @__PURE__ */ new Error(PLAN_TOO_LARGE_DENY));
|
|
3282
|
+
if (written !== void 0) state.plan = written;
|
|
3283
|
+
}
|
|
2977
3284
|
resolve({
|
|
2978
3285
|
code,
|
|
2979
3286
|
killSignal
|
|
@@ -2987,7 +3294,7 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2987
3294
|
throw error;
|
|
2988
3295
|
});
|
|
2989
3296
|
closed.catch(() => void 0);
|
|
2990
|
-
child.stdin?.on?.("error",
|
|
3297
|
+
child.stdin?.on?.("error", onStdinError);
|
|
2991
3298
|
if (mode === "plan") writeStdin(input);
|
|
2992
3299
|
else child.stdin?.end?.(input);
|
|
2993
3300
|
if (runSignal.aborted) kill();
|
|
@@ -3167,6 +3474,10 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
3167
3474
|
yield* drain();
|
|
3168
3475
|
}
|
|
3169
3476
|
await completion;
|
|
3477
|
+
if (buffer.trim().startsWith("{")) try {
|
|
3478
|
+
const event = readClaudeStreamEvents(JSON.parse(scrub(buffer.trim()))).find((item) => item.type === "result");
|
|
3479
|
+
if (event !== void 0) sawResult = event.result;
|
|
3480
|
+
} catch {}
|
|
3170
3481
|
if (typeof state.plan === "string") {
|
|
3171
3482
|
if (deltaKind !== void 0) {
|
|
3172
3483
|
const tail = deltaRedactor.flush({ boundary: true });
|
|
@@ -3196,10 +3507,6 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
3196
3507
|
};
|
|
3197
3508
|
return;
|
|
3198
3509
|
}
|
|
3199
|
-
if (buffer.trim().startsWith("{")) try {
|
|
3200
|
-
const event = readClaudeStreamEvents(JSON.parse(scrub(buffer.trim()))).find((item) => item.type === "result");
|
|
3201
|
-
if (event !== void 0) sawResult = event.result;
|
|
3202
|
-
} catch {}
|
|
3203
3510
|
if (sawResult === void 0) throw new Error(failureMessage(void 0, void 0, state.stderr));
|
|
3204
3511
|
if (sawResult.is_error === true || exitStatus?.code !== 0) throw new Error(failureMessage(sawResult, exitStatus?.code ?? exitStatus?.killSignal, state.stderr));
|
|
3205
3512
|
const usage = sawResult.usage ?? {};
|
|
@@ -3219,7 +3526,11 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
3219
3526
|
};
|
|
3220
3527
|
} finally {
|
|
3221
3528
|
stopIfAbandoned();
|
|
3222
|
-
|
|
3529
|
+
try {
|
|
3530
|
+
await closed.catch(() => void 0);
|
|
3531
|
+
} finally {
|
|
3532
|
+
await closeBridge();
|
|
3533
|
+
}
|
|
3223
3534
|
}
|
|
3224
3535
|
},
|
|
3225
3536
|
/** Revoke bridges and stop every invocation when the plugin unloads. */
|
|
@@ -4031,13 +4342,15 @@ function claudeModeFor(sandbox, approval) {
|
|
|
4031
4342
|
}
|
|
4032
4343
|
}
|
|
4033
4344
|
/**
|
|
4034
|
-
* Whether
|
|
4345
|
+
* Whether a prepared model request should run the CLI in plan mode.
|
|
4035
4346
|
*
|
|
4036
|
-
*
|
|
4037
|
-
*
|
|
4038
|
-
*
|
|
4039
|
-
*
|
|
4040
|
-
*
|
|
4347
|
+
* This selection is for request preparation only, not an executing tool batch.
|
|
4348
|
+
* `pending` is the selection used for the proposed request assembly and wins
|
|
4349
|
+
* over logged `active`, matching `plan:policy`. An accepted pre-step commits
|
|
4350
|
+
* it; reading after that boundary sees the same selection as logged `active`.
|
|
4351
|
+
* Without the service, the logged projection is the only signal. A missing
|
|
4352
|
+
* session, a missing service, or a throw means plan mode is off — never a
|
|
4353
|
+
* failed turn.
|
|
4041
4354
|
* @param ctx - the plugin context; services are read without an inject requirement.
|
|
4042
4355
|
* @param session - the session whose plan state is read.
|
|
4043
4356
|
* @returns true when the CLI must plan instead of acting.
|
|
@@ -4055,7 +4368,30 @@ function planModeForSession(ctx, session) {
|
|
|
4055
4368
|
return false;
|
|
4056
4369
|
}
|
|
4057
4370
|
}
|
|
4058
|
-
/**
|
|
4371
|
+
/**
|
|
4372
|
+
* Whether a tool in the current batch must plan, using logged active state.
|
|
4373
|
+
*
|
|
4374
|
+
* Approval queues `pending: false`; it does not authorize remaining tools in
|
|
4375
|
+
* that batch to implement. Likewise, pending entry must not change a tool's
|
|
4376
|
+
* current mode before the next accepted pre-step commits it.
|
|
4377
|
+
* @param ctx - the plugin context.
|
|
4378
|
+
* @param session - the executing tool's session.
|
|
4379
|
+
* @param agent - the trusted executing agent, when provided by the tool runtime.
|
|
4380
|
+
*/
|
|
4381
|
+
function planModeForToolExecution(ctx, session, agent) {
|
|
4382
|
+
if (session === void 0 || session === null) return false;
|
|
4383
|
+
try {
|
|
4384
|
+
const caller = agent ?? ctx.get?.("agents")?.currentInitiator?.();
|
|
4385
|
+
if (caller?.session === session) {
|
|
4386
|
+
const state = ctx.get?.("planMode")?.get?.(caller);
|
|
4387
|
+
if (state !== void 0 && state !== null) return state.active === true;
|
|
4388
|
+
}
|
|
4389
|
+
return ctx.get?.("sessionProjections")?.stateOf?.(session, "plan")?.active === true;
|
|
4390
|
+
} catch {
|
|
4391
|
+
return false;
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
/** `pending` is the selection the proposed request was assembled with. */
|
|
4059
4395
|
function planSelected(state) {
|
|
4060
4396
|
if (typeof state?.pending === "boolean") return state.pending;
|
|
4061
4397
|
return state?.active === true;
|
|
@@ -4108,7 +4444,8 @@ const CLAUDE_TOOL_INSTRUCTIONS = "This task was delegated to Claude Code by anot
|
|
|
4108
4444
|
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.";
|
|
4109
4445
|
/**
|
|
4110
4446
|
* @param options - the CLI runner, the most a run may do for a session, and
|
|
4111
|
-
* whether
|
|
4447
|
+
* whether the executing batch's logged state is DSH plan mode (not a
|
|
4448
|
+
* pending selection for the next model request).
|
|
4112
4449
|
* @returns the tool definition to register.
|
|
4113
4450
|
*/
|
|
4114
4451
|
function createClaudeCodeTool({ runner, modeCeiling, planActive }) {
|
|
@@ -4172,7 +4509,7 @@ function createClaudeCodeTool({ runner, modeCeiling, planActive }) {
|
|
|
4172
4509
|
const requested = typeof args.cwd === "string" ? args.cwd.trim() : "";
|
|
4173
4510
|
const cwd = requested === "" ? sessionCwd : isAbsolute(requested) ? requested : sessionCwd === void 0 ? requested : join(sessionCwd, requested);
|
|
4174
4511
|
const session = exec?.agent?.session;
|
|
4175
|
-
const planning = planActive?.(session) === true;
|
|
4512
|
+
const planning = planActive?.(session, exec?.agent) === true;
|
|
4176
4513
|
return await runner.run({
|
|
4177
4514
|
prompt: args.prompt,
|
|
4178
4515
|
mode: planning ? "plan" : capClaudeMode(args.mode, modeCeiling(session)) ?? "edit",
|
|
@@ -6478,9 +6815,9 @@ function apply(ctx, config) {
|
|
|
6478
6815
|
ctx.effect(() => toolsCtx.tools.register(createClaudeCodeTool({
|
|
6479
6816
|
runner: claudeRunner,
|
|
6480
6817
|
modeCeiling: cliModeCeiling,
|
|
6481
|
-
planActive: (session) =>
|
|
6818
|
+
planActive: (session, agent) => planModeForToolExecution(ctx, session, agent)
|
|
6482
6819
|
})), "packycode-api: claude_code tool");
|
|
6483
6820
|
});
|
|
6484
6821
|
}
|
|
6485
6822
|
//#endregion
|
|
6486
|
-
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 };
|
|
6823
|
+
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, planModeForToolExecution, readClaudeStreamEvents, readSearchChoice, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, statusHostOrder, summarizeToolUse };
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-packycode-api",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"packageManager": "pnpm@10.32.1",
|
|
3
|
+
"version": "1.1.4",
|
|
5
4
|
"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
5
|
"type": "module",
|
|
7
6
|
"main": "./lib/index.js",
|
|
@@ -40,11 +39,6 @@
|
|
|
40
39
|
"platform": "web"
|
|
41
40
|
}
|
|
42
41
|
},
|
|
43
|
-
"scripts": {
|
|
44
|
-
"build": "tsdown --config tsdown.config.mjs",
|
|
45
|
-
"test": "node --test tests/*.test.mjs",
|
|
46
|
-
"check": "pnpm run test && pnpm run build && pnpm pack --pack-destination .artifacts"
|
|
47
|
-
},
|
|
48
42
|
"keywords": [
|
|
49
43
|
"dsh-plugin",
|
|
50
44
|
"deepseek-harness",
|
|
@@ -97,5 +91,10 @@
|
|
|
97
91
|
"@earendil-works/pi-ai": "0.85.1",
|
|
98
92
|
"react": "18.3.1",
|
|
99
93
|
"tsdown": "0.22.2"
|
|
94
|
+
},
|
|
95
|
+
"scripts": {
|
|
96
|
+
"build": "tsdown --config tsdown.config.mjs",
|
|
97
|
+
"test": "node --test tests/*.test.mjs",
|
|
98
|
+
"check": "pnpm run test && pnpm run build && pnpm pack --pack-destination .artifacts"
|
|
100
99
|
}
|
|
101
|
-
}
|
|
100
|
+
}
|