dsh-packycode-api 1.1.3 → 1.1.5
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 +19 -0
- package/README.en.md +14 -4
- package/README.md +14 -4
- package/lib/client.js +118 -0
- package/lib/index.js +627 -229
- package/package.json +11 -11
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";
|
|
@@ -544,7 +544,8 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
|
|
|
544
544
|
* model does not support is refused before the request is sent: Claude Opus
|
|
545
545
|
* 5.5 (platform.claude.com effort docs), GPT-6.1 Sol and GPT-6 Astra
|
|
546
546
|
* `reasoning.effort` (developers.openai.com model pages), DeepSeek thinking
|
|
547
|
-
* mode (`thinking` toggle plus `low|high|max` effort).
|
|
547
|
+
* mode (`thinking` toggle plus `low|high|max` effort), and Grok 4.7
|
|
548
|
+
* (docs.x.ai/developers/grok-4-7).
|
|
548
549
|
*
|
|
549
550
|
* @module dsh-packycode-api/catalog
|
|
550
551
|
*/
|
|
@@ -586,6 +587,11 @@ const ALWAYS_THINKING_LEVELS = Object.freeze({
|
|
|
586
587
|
xhigh: "xhigh",
|
|
587
588
|
max: "max"
|
|
588
589
|
});
|
|
590
|
+
/** Grok 4.7 supports low | medium | high | xhigh, with no off or max. */
|
|
591
|
+
const GROK_LEVELS = Object.freeze({
|
|
592
|
+
...ALWAYS_THINKING_LEVELS,
|
|
593
|
+
max: null
|
|
594
|
+
});
|
|
589
595
|
const PACKYCODE_MODELS = Object.freeze([
|
|
590
596
|
Object.freeze({
|
|
591
597
|
id: "deepseek-flash",
|
|
@@ -632,6 +638,20 @@ const PACKYCODE_MODELS = Object.freeze([
|
|
|
632
638
|
maxTokens: 128e3,
|
|
633
639
|
thinkingLevelMap: ALWAYS_THINKING_LEVELS
|
|
634
640
|
}),
|
|
641
|
+
Object.freeze({
|
|
642
|
+
id: "grok-4.7",
|
|
643
|
+
name: "Grok 4.7",
|
|
644
|
+
api: "openai-responses",
|
|
645
|
+
provider: PROVIDER,
|
|
646
|
+
baseUrl: RESPONSES_BASE,
|
|
647
|
+
reasoning: true,
|
|
648
|
+
input: Object.freeze(["text", "image"]),
|
|
649
|
+
cost: NO_COST,
|
|
650
|
+
contextWindow: 5e5,
|
|
651
|
+
maxTokens: 5e5,
|
|
652
|
+
thinkingLevelMap: GROK_LEVELS,
|
|
653
|
+
compat: Object.freeze({ supportsLongCacheRetention: false })
|
|
654
|
+
}),
|
|
635
655
|
Object.freeze({
|
|
636
656
|
id: "claude-opus-5-5",
|
|
637
657
|
name: "Claude Opus 5.5",
|
|
@@ -1275,12 +1295,104 @@ function hostPath(lookup) {
|
|
|
1275
1295
|
return;
|
|
1276
1296
|
}
|
|
1277
1297
|
}
|
|
1298
|
+
/** The only PTC program this adapter emits; its JSON literal also works in Python. */
|
|
1299
|
+
const PLAN_REVIEW_PROGRAM_PREFIX = "return await tools.exit_plan_mode(";
|
|
1300
|
+
/** Parse durable arguments without evaluating model-authored code. */
|
|
1301
|
+
function parsedArguments(value) {
|
|
1302
|
+
try {
|
|
1303
|
+
return typeof value === "string" ? JSON.parse(value) : value;
|
|
1304
|
+
} catch {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Recover only the public plan, never arbitrary tool arguments or PTC code.
|
|
1310
|
+
* The PTC allowlist is deliberately the exact, single-call program we emit.
|
|
1311
|
+
*/
|
|
1312
|
+
function planOfCall(block) {
|
|
1313
|
+
if (block?.type !== "tool-call") return void 0;
|
|
1314
|
+
const args = parsedArguments(block.arguments);
|
|
1315
|
+
let plan;
|
|
1316
|
+
if (block.name === "exit_plan_mode") plan = args?.plan;
|
|
1317
|
+
else if (block.name === "run_code" && typeof args?.code === "string") {
|
|
1318
|
+
const code = args.code;
|
|
1319
|
+
if (!code.startsWith(PLAN_REVIEW_PROGRAM_PREFIX) || !code.endsWith(")")) return void 0;
|
|
1320
|
+
const review = parsedArguments(code.slice(34, -1));
|
|
1321
|
+
if (review === null || typeof review !== "object" || Object.keys(review).length !== 1) return void 0;
|
|
1322
|
+
plan = review.plan;
|
|
1323
|
+
}
|
|
1324
|
+
return typeof plan === "string" && plan.trim() !== "" ? plan : void 0;
|
|
1325
|
+
}
|
|
1326
|
+
/** Keep the latest complete plan and its review from the persisted model transcript. */
|
|
1327
|
+
function planReviewContext(messages) {
|
|
1328
|
+
let latest;
|
|
1329
|
+
for (const message of messages) {
|
|
1330
|
+
if (message?.role !== "assistant") continue;
|
|
1331
|
+
for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
1332
|
+
const plan = planOfCall(block);
|
|
1333
|
+
if (plan !== void 0) latest = {
|
|
1334
|
+
plan,
|
|
1335
|
+
id: block.id
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (latest === void 0) return void 0;
|
|
1340
|
+
let review;
|
|
1341
|
+
for (const message of messages) {
|
|
1342
|
+
if (message?.role !== "user") continue;
|
|
1343
|
+
for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
1344
|
+
if (block?.type !== "tool-result" || latest.id === void 0 || block.toolCallId !== latest.id) continue;
|
|
1345
|
+
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");
|
|
1346
|
+
review = `[计划审阅${block.isError === true ? "未批准/失败" : "结果"}]\n${text}`;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return {
|
|
1350
|
+
id: latest.id,
|
|
1351
|
+
turn: {
|
|
1352
|
+
label: "计划审阅上下文",
|
|
1353
|
+
parts: [{
|
|
1354
|
+
kind: "text",
|
|
1355
|
+
text: `[最近提交的完整计划;提交本身不代表已批准]\n${latest.plan}\n\n${review ?? "[尚无计划审阅结果;不要把提交视为实施许可]"}`
|
|
1356
|
+
}]
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Select the review transport from this request's actual direct-tool catalog.
|
|
1362
|
+
* `run_code` binds `tools` in both supported host language flavors.
|
|
1363
|
+
*/
|
|
1364
|
+
function planReviewCall(options, plan) {
|
|
1365
|
+
if (plan.trim() === "") throw new Error("Claude Code submitted an empty plan; no review or implementation was authorized.");
|
|
1366
|
+
const context = planReviewContext([{
|
|
1367
|
+
role: "assistant",
|
|
1368
|
+
content: [{
|
|
1369
|
+
type: "tool-call",
|
|
1370
|
+
name: "exit_plan_mode",
|
|
1371
|
+
arguments: { plan }
|
|
1372
|
+
}]
|
|
1373
|
+
}]).turn;
|
|
1374
|
+
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.");
|
|
1375
|
+
const tools = Array.isArray(options?.tools) ? options.tools : [];
|
|
1376
|
+
if (tools.some((tool) => tool?.name === "exit_plan_mode")) return {
|
|
1377
|
+
name: "exit_plan_mode",
|
|
1378
|
+
args: JSON.stringify({ plan })
|
|
1379
|
+
};
|
|
1380
|
+
if (tools.some((tool) => tool?.name === "run_code")) return {
|
|
1381
|
+
name: "run_code",
|
|
1382
|
+
args: JSON.stringify({
|
|
1383
|
+
code: `${PLAN_REVIEW_PROGRAM_PREFIX}${JSON.stringify({ plan })})`,
|
|
1384
|
+
description: "Present completed plan for user review"
|
|
1385
|
+
})
|
|
1386
|
+
};
|
|
1387
|
+
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.");
|
|
1388
|
+
}
|
|
1278
1389
|
/**
|
|
1279
1390
|
* Flatten one message's content into ordered parts: text, image, and file.
|
|
1280
1391
|
* Tool calls and results become short text so a turn that started on another
|
|
1281
|
-
* model still reads coherently; reasoning is not carried.
|
|
1392
|
+
* model still reads coherently; reasoning is not carried. The latest review
|
|
1393
|
+
* result is carried in full separately, not through the general 8k tool cap.
|
|
1282
1394
|
*/
|
|
1283
|
-
function collectParts(blocks, parts) {
|
|
1395
|
+
function collectParts(blocks, parts, reviewId) {
|
|
1284
1396
|
for (const block of Array.isArray(blocks) ? blocks : []) {
|
|
1285
1397
|
if (block === null || typeof block !== "object") continue;
|
|
1286
1398
|
switch (block.type) {
|
|
@@ -1314,6 +1426,7 @@ function collectParts(blocks, parts) {
|
|
|
1314
1426
|
});
|
|
1315
1427
|
break;
|
|
1316
1428
|
case "tool-result": {
|
|
1429
|
+
if (reviewId !== void 0 && block.toolCallId === reviewId) break;
|
|
1317
1430
|
if (typeof block.content === "string") {
|
|
1318
1431
|
parts.push({
|
|
1319
1432
|
kind: "text",
|
|
@@ -1355,10 +1468,12 @@ const textLength = (parts) => parts.reduce((sum, part) => sum + (part.kind === "
|
|
|
1355
1468
|
*/
|
|
1356
1469
|
function delegatedTurns(options) {
|
|
1357
1470
|
const messages = Array.isArray(options?.messages) ? options.messages : [];
|
|
1471
|
+
const review = planReviewContext(messages);
|
|
1472
|
+
const reserved = review === void 0 ? 0 : textLength(review.turn.parts) + review.turn.label.length + 3;
|
|
1358
1473
|
const turns = [];
|
|
1359
1474
|
for (const message of messages) {
|
|
1360
1475
|
if (message === null || typeof message !== "object" || message.role === "system") continue;
|
|
1361
|
-
const parts = collectParts(message.content, []);
|
|
1476
|
+
const parts = collectParts(message.content, [], review?.id);
|
|
1362
1477
|
if (parts.length === 0 || textLength(parts) === 0 && !parts.some((part) => part.kind !== "text")) continue;
|
|
1363
1478
|
const label = message.role === "user" ? "用户" : message.role === "assistant" ? "助手" : String(message.role);
|
|
1364
1479
|
turns.push({
|
|
@@ -1366,8 +1481,11 @@ function delegatedTurns(options) {
|
|
|
1366
1481
|
parts
|
|
1367
1482
|
});
|
|
1368
1483
|
}
|
|
1484
|
+
const newest = turns.at(-1);
|
|
1485
|
+
const newestSize = newest === void 0 ? 0 : textLength(newest.parts) + newest.label.length + 3;
|
|
1486
|
+
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
1487
|
const kept = [];
|
|
1370
|
-
let used =
|
|
1488
|
+
let used = reserved;
|
|
1371
1489
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
1372
1490
|
const turn = turns[index];
|
|
1373
1491
|
const size = textLength(turn.parts) + turn.label.length + 3;
|
|
@@ -1383,9 +1501,11 @@ function delegatedTurns(options) {
|
|
|
1383
1501
|
});
|
|
1384
1502
|
break;
|
|
1385
1503
|
}
|
|
1504
|
+
const truncated = kept.length < turns.length || kept[0]?.clipped === true;
|
|
1505
|
+
if (review !== void 0) kept.unshift(review.turn);
|
|
1386
1506
|
return {
|
|
1387
1507
|
turns: kept,
|
|
1388
|
-
truncated
|
|
1508
|
+
truncated
|
|
1389
1509
|
};
|
|
1390
1510
|
}
|
|
1391
1511
|
/** Keep the last `budget` characters of a turn's text, and every part after them. */
|
|
@@ -1898,7 +2018,8 @@ function createClaudeCliAdapter({ inner, runner, modelIds = CLI_BACKED_MODEL_IDS
|
|
|
1898
2018
|
default: break;
|
|
1899
2019
|
}
|
|
1900
2020
|
if (submittedPlan !== void 0) {
|
|
1901
|
-
|
|
2021
|
+
const review = planReviewCall(options, submittedPlan);
|
|
2022
|
+
yield* blocks.toolCall(`plan-${randomUUID()}`, review.name, review.args);
|
|
1902
2023
|
yield {
|
|
1903
2024
|
type: "usage",
|
|
1904
2025
|
usage: turnUsage(lastRequest, finished)
|
|
@@ -2304,10 +2425,11 @@ function resolveSpawnCommand(binary, { platform = process.platform, exists = exi
|
|
|
2304
2425
|
* consumes the streaming protocol, and instructions appended to the CLI's
|
|
2305
2426
|
* own system prompt ({@link DEFAULT_APPEND_SYSTEM_PROMPT} when blank). The
|
|
2306
2427
|
* CLI's system prompt and tool set are never replaced: they are part of
|
|
2307
|
-
* what the official client sends.
|
|
2428
|
+
* what the official client sends. Plan runs supply an absolute private
|
|
2429
|
+
* planDirectory below the child's working directory.
|
|
2308
2430
|
* @returns argv after the binary name.
|
|
2309
2431
|
*/
|
|
2310
|
-
function buildClaudeArgs({ mode = DEFAULT_CLAUDE_MODE, model, stream = false, appendSystemPrompt, searchBridge, disableNativeSearch = false } = {}) {
|
|
2432
|
+
function buildClaudeArgs({ mode = DEFAULT_CLAUDE_MODE, model, stream = false, appendSystemPrompt, searchBridge, disableNativeSearch = false, planDirectory } = {}) {
|
|
2311
2433
|
const spec = CLAUDE_CLI_MODES[mode];
|
|
2312
2434
|
if (spec === void 0) throw new Error(`claude_code mode must be one of ${Object.keys(CLAUDE_CLI_MODES).join(", ")}`);
|
|
2313
2435
|
const args = stream ? [
|
|
@@ -2334,16 +2456,31 @@ function buildClaudeArgs({ mode = DEFAULT_CLAUDE_MODE, model, stream = false, ap
|
|
|
2334
2456
|
args.push("--permission-mode", spec.permissionMode);
|
|
2335
2457
|
if (searchBridge !== void 0) args.push("--allowedTools", searchBridge.toolName);
|
|
2336
2458
|
} else args.push("--allowedTools", ...spec.allowedTools, ...searchBridge === void 0 ? [] : [searchBridge.toolName]);
|
|
2459
|
+
if (mode === "plan") {
|
|
2460
|
+
args.push("--permission-prompt-tool", "stdio");
|
|
2461
|
+
if (planDirectory !== void 0 && (typeof planDirectory !== "string" || !isAbsolute(planDirectory))) throw new Error("claude_code plan directory must be an absolute path");
|
|
2462
|
+
args.push("--settings", JSON.stringify({
|
|
2463
|
+
...planDirectory === void 0 ? {} : { plansDirectory: planDirectory },
|
|
2464
|
+
permissions: { ask: ["ExitPlanMode"] }
|
|
2465
|
+
}));
|
|
2466
|
+
}
|
|
2337
2467
|
if (disableNativeSearch || searchBridge !== void 0) args.push("--disallowedTools", "WebSearch");
|
|
2338
2468
|
if (searchBridge !== void 0) args.push("--mcp-config", searchBridge.configPath);
|
|
2339
2469
|
return args;
|
|
2340
2470
|
}
|
|
2341
|
-
/** How many permission
|
|
2471
|
+
/** How many permission denials one plan-mode run may recover from. */
|
|
2342
2472
|
const MAX_PLAN_DENIALS = 8;
|
|
2343
|
-
/** A plan file larger than this is not
|
|
2473
|
+
/** A plan file or submitted plan larger than this is not returned into the turn. */
|
|
2344
2474
|
const PLAN_FILE_MAX_BYTES = 2e5;
|
|
2345
|
-
const
|
|
2475
|
+
const PLAN_DIRECTORY_PREFIX = ".dsh-packycode-plan-";
|
|
2476
|
+
const PLAN_FS = Object.freeze({
|
|
2477
|
+
mkdtempSync,
|
|
2478
|
+
realpathSync,
|
|
2479
|
+
lstatSync,
|
|
2480
|
+
rmSync
|
|
2481
|
+
});
|
|
2346
2482
|
const PLAN_EXIT_DENY = "Write the plan to the plan file first, starting with a # heading, then call ExitPlanMode again.";
|
|
2483
|
+
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
2484
|
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
2485
|
const PLAN_DIALOG_ERROR = "Plan mode cannot ask the user from the CLI. Decide and continue, or call ExitPlanMode.";
|
|
2349
2486
|
const PLAN_DENY_LIMIT_MESSAGE = "claude_code plan mode stopped after too many denied permission prompts";
|
|
@@ -2357,11 +2494,58 @@ function planMarkdown(value) {
|
|
|
2357
2494
|
if (typeof value !== "string") return void 0;
|
|
2358
2495
|
const trimmed = value.trim();
|
|
2359
2496
|
if (trimmed === "") return void 0;
|
|
2360
|
-
const
|
|
2361
|
-
return
|
|
2497
|
+
const markdown = /^#\s+\S/u.test(trimmed) ? trimmed : `# Plan\n\n${trimmed}`;
|
|
2498
|
+
return Buffer.byteLength(markdown) > PLAN_FILE_MAX_BYTES ? void 0 : markdown;
|
|
2499
|
+
}
|
|
2500
|
+
/** Own exactly one newly created, owner-only directory, never a shared CLI home. */
|
|
2501
|
+
function createOwnedPlanDirectory(fs, cwd) {
|
|
2502
|
+
const parent = fs.realpathSync(cwd ?? process.cwd());
|
|
2503
|
+
const directory = fs.mkdtempSync(join(parent, PLAN_DIRECTORY_PREFIX));
|
|
2504
|
+
if (!isAbsolute(directory) || dirname(directory) !== parent || !directory.startsWith(join(parent, PLAN_DIRECTORY_PREFIX))) throw new Error("PackyCode plan directory ownership could not be established");
|
|
2505
|
+
const identity = fs.lstatSync(directory);
|
|
2506
|
+
if (!identity.isDirectory() || identity.isSymbolicLink()) throw new Error("PackyCode plan directory ownership could not be established");
|
|
2507
|
+
const canonical = directory;
|
|
2508
|
+
let removed = false;
|
|
2509
|
+
const isOwned = () => {
|
|
2510
|
+
try {
|
|
2511
|
+
const current = fs.lstatSync(directory);
|
|
2512
|
+
return current.isDirectory() && !current.isSymbolicLink() && current.dev === identity.dev && current.ino === identity.ino && fs.realpathSync(directory) === canonical;
|
|
2513
|
+
} catch {
|
|
2514
|
+
return false;
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
const owned = {
|
|
2518
|
+
directory: canonical,
|
|
2519
|
+
isOwned,
|
|
2520
|
+
close() {
|
|
2521
|
+
if (removed) return;
|
|
2522
|
+
let current;
|
|
2523
|
+
try {
|
|
2524
|
+
current = fs.lstatSync(directory);
|
|
2525
|
+
} catch (error) {
|
|
2526
|
+
if (error?.code === "ENOENT") {
|
|
2527
|
+
removed = true;
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
throw error;
|
|
2531
|
+
}
|
|
2532
|
+
if (!current.isDirectory() || current.isSymbolicLink() || !isOwned()) throw new Error("PackyCode plan directory ownership changed; refusing to remove it");
|
|
2533
|
+
fs.rmSync(canonical, {
|
|
2534
|
+
recursive: true,
|
|
2535
|
+
force: true
|
|
2536
|
+
});
|
|
2537
|
+
removed = true;
|
|
2538
|
+
}
|
|
2539
|
+
};
|
|
2540
|
+
if (!isOwned()) {
|
|
2541
|
+
owned.close();
|
|
2542
|
+
throw new Error("PackyCode plan directory ownership could not be established");
|
|
2543
|
+
}
|
|
2544
|
+
return owned;
|
|
2362
2545
|
}
|
|
2363
|
-
/** Whether
|
|
2546
|
+
/** Whether a file stays inside the immutable canonical directory bound to the run. */
|
|
2364
2547
|
function pathInside(root, file, realpath) {
|
|
2548
|
+
if (typeof root !== "string" || !isAbsolute(root) || typeof file !== "string" || !isAbsolute(file)) return false;
|
|
2365
2549
|
let rootReal;
|
|
2366
2550
|
let fileReal;
|
|
2367
2551
|
try {
|
|
@@ -2370,87 +2554,104 @@ function pathInside(root, file, realpath) {
|
|
|
2370
2554
|
} catch {
|
|
2371
2555
|
return false;
|
|
2372
2556
|
}
|
|
2557
|
+
if (rootReal !== resolve(root)) return false;
|
|
2373
2558
|
const prefix = rootReal.endsWith(sep) ? rootReal : `${rootReal}${sep}`;
|
|
2374
|
-
return fileReal
|
|
2559
|
+
return fileReal.startsWith(prefix);
|
|
2560
|
+
}
|
|
2561
|
+
/** A descriptor-based bounded read also rejects a replaced leaf before reading it. */
|
|
2562
|
+
function readBoundedPlanFile(file, expected) {
|
|
2563
|
+
const fd = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
2564
|
+
try {
|
|
2565
|
+
const info = fstatSync(fd);
|
|
2566
|
+
if (!info.isFile() || expected !== void 0 && (info.dev !== expected.dev || info.ino !== expected.ino)) return void 0;
|
|
2567
|
+
if (info.size > PLAN_FILE_MAX_BYTES) throw Object.assign(/* @__PURE__ */ new Error(PLAN_TOO_LARGE_DENY), { code: "CLAUDE_PLAN_TOO_LARGE" });
|
|
2568
|
+
const bytes = Buffer.allocUnsafe(200001);
|
|
2569
|
+
let length = 0;
|
|
2570
|
+
while (length < bytes.length) {
|
|
2571
|
+
const count = readSync(fd, bytes, length, bytes.length - length, null);
|
|
2572
|
+
if (count === 0) break;
|
|
2573
|
+
length += count;
|
|
2574
|
+
}
|
|
2575
|
+
if (length > PLAN_FILE_MAX_BYTES) throw Object.assign(/* @__PURE__ */ new Error(PLAN_TOO_LARGE_DENY), { code: "CLAUDE_PLAN_TOO_LARGE" });
|
|
2576
|
+
return bytes.subarray(0, length).toString("utf8");
|
|
2577
|
+
} finally {
|
|
2578
|
+
closeSync(fd);
|
|
2579
|
+
}
|
|
2375
2580
|
}
|
|
2376
2581
|
/**
|
|
2377
|
-
* The plan a CLI
|
|
2378
|
-
*
|
|
2379
|
-
*
|
|
2380
|
-
*
|
|
2381
|
-
*
|
|
2382
|
-
*
|
|
2383
|
-
*
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2582
|
+
* The plan a CLI ExitPlanMode request submits. Inline text remains valid; a
|
|
2583
|
+
* file must resolve inside this invocation's explicitly bound canonical
|
|
2584
|
+
* planDirectory. Shared config homes and ~/.claude/plans are never searched.
|
|
2585
|
+
* The caller supplies scrub so secrets are removed before the size check.
|
|
2586
|
+
* @param input - the control request's input.
|
|
2587
|
+
* @param options - the bound private plan directory, redactor, and filesystem seams.
|
|
2588
|
+
* @returns bounded plan markdown, or undefined when the request has none yet.
|
|
2589
|
+
*/
|
|
2590
|
+
function readSubmittedPlan(input, { planDirectory, scrub = (value) => value, onRejected = () => void 0, readFile = readBoundedPlanFile, stat = statSync, realpath = realpathSync } = {}) {
|
|
2591
|
+
if (typeof input?.plan === "string") {
|
|
2592
|
+
const text = scrub(input.plan);
|
|
2593
|
+
const inline = planMarkdown(text);
|
|
2594
|
+
if (inline !== void 0) return inline;
|
|
2595
|
+
if (text.trim() !== "") {
|
|
2596
|
+
onRejected("too-large");
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2392
2600
|
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;
|
|
2601
|
+
if (!pathInside(planDirectory, requested, realpath)) return void 0;
|
|
2396
2602
|
try {
|
|
2397
|
-
file = realpath(requested);
|
|
2603
|
+
const file = realpath(requested);
|
|
2604
|
+
if (!file.toLowerCase().endsWith(".md")) return void 0;
|
|
2398
2605
|
const info = stat(file);
|
|
2399
|
-
if (!info.isFile()
|
|
2400
|
-
|
|
2606
|
+
if (!info.isFile()) return void 0;
|
|
2607
|
+
if (info.size > PLAN_FILE_MAX_BYTES) {
|
|
2608
|
+
onRejected("too-large");
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
const text = readFile(file, info);
|
|
2612
|
+
if (typeof text !== "string") return void 0;
|
|
2613
|
+
const safe = scrub(text);
|
|
2614
|
+
const plan = planMarkdown(safe);
|
|
2615
|
+
if (plan === void 0 && safe.trim() !== "") onRejected("too-large");
|
|
2616
|
+
return plan;
|
|
2617
|
+
} catch (error) {
|
|
2618
|
+
if (error?.code === "CLAUDE_PLAN_TOO_LARGE") onRejected("too-large");
|
|
2401
2619
|
return;
|
|
2402
2620
|
}
|
|
2403
|
-
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* A file-only completion from the CLI is accepted only from this run's new
|
|
2624
|
+
* private plans directory. There is no shared-directory/mtime fallback. If
|
|
2625
|
+
* more than one eligible file exists, do not guess which session submitted it.
|
|
2626
|
+
* @param options - the bound private plan directory, redactor, and filesystem seams.
|
|
2627
|
+
* @returns plan markdown, or undefined when absent or ambiguous.
|
|
2628
|
+
*/
|
|
2629
|
+
function findPlanWrittenDuringRun({ planDirectory, scrub = (value) => value, onRejected = () => void 0, readdir = readdirSync, readFile = readBoundedPlanFile, stat = statSync, realpath = realpathSync } = {}) {
|
|
2630
|
+
if (typeof planDirectory !== "string" || !isAbsolute(planDirectory)) return void 0;
|
|
2631
|
+
let names;
|
|
2404
2632
|
try {
|
|
2405
|
-
|
|
2633
|
+
names = readdir(planDirectory);
|
|
2406
2634
|
} catch {
|
|
2407
2635
|
return;
|
|
2408
2636
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
-
}
|
|
2637
|
+
let found;
|
|
2638
|
+
for (const name of names) {
|
|
2639
|
+
if (typeof name !== "string" || !name.toLowerCase().endsWith(".md")) continue;
|
|
2640
|
+
const candidate = join(planDirectory, name);
|
|
2641
|
+
if (dirname(candidate) !== planDirectory) continue;
|
|
2642
|
+
const text = readSubmittedPlan({ planFilePath: candidate }, {
|
|
2643
|
+
planDirectory,
|
|
2644
|
+
scrub,
|
|
2645
|
+
onRejected,
|
|
2646
|
+
readFile,
|
|
2647
|
+
stat,
|
|
2648
|
+
realpath
|
|
2649
|
+
});
|
|
2650
|
+
if (text === void 0) continue;
|
|
2651
|
+
if (found !== void 0) return void 0;
|
|
2652
|
+
found = text;
|
|
2452
2653
|
}
|
|
2453
|
-
return
|
|
2654
|
+
return found;
|
|
2454
2655
|
}
|
|
2455
2656
|
/**
|
|
2456
2657
|
* Build what the CLI reads from stdin for one run.
|
|
@@ -2579,6 +2780,22 @@ function toolResultText(content) {
|
|
|
2579
2780
|
return content.map((part) => part?.type === "text" && typeof part.text === "string" ? part.text : "").join(" ");
|
|
2580
2781
|
}
|
|
2581
2782
|
/**
|
|
2783
|
+
* Native plan/permission gates sometimes produce no control_request at all.
|
|
2784
|
+
* Recognize their specific messages, never generic is_error, ENOENT, EACCES,
|
|
2785
|
+
* a failed search, or a command's nonzero exit. An associated assistant tool
|
|
2786
|
+
* use is required, so arbitrary user text cannot spend the denial budget.
|
|
2787
|
+
*/
|
|
2788
|
+
function nativePermissionDenial(name, text) {
|
|
2789
|
+
if (typeof name !== "string") return false;
|
|
2790
|
+
if ([
|
|
2791
|
+
"Write",
|
|
2792
|
+
"Edit",
|
|
2793
|
+
"NotebookEdit"
|
|
2794
|
+
].includes(name) && /^Cannot (?:write to|edit|modify) [\s\S]+ while in plan mode\.(?:\s|$)/u.test(text)) return true;
|
|
2795
|
+
if (["Bash", "PowerShell"].includes(name) && text.startsWith("Auto mode could not evaluate this action and is blocking it for safety")) return true;
|
|
2796
|
+
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);
|
|
2797
|
+
}
|
|
2798
|
+
/**
|
|
2582
2799
|
* Read one `--output-format stream-json` line.
|
|
2583
2800
|
*
|
|
2584
2801
|
* The CLI prints both partial deltas (`stream_event`) and each finished
|
|
@@ -2701,7 +2918,7 @@ function describeRunKey(credential) {
|
|
|
2701
2918
|
* seams.
|
|
2702
2919
|
* @returns `{ run(options), stream(options) }`.
|
|
2703
2920
|
*/
|
|
2704
|
-
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 } = {}) {
|
|
2921
|
+
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 } = {}) {
|
|
2705
2922
|
if (typeof resolveCredential !== "function") throw new Error("the Claude Code CLI runner needs resolveCredential");
|
|
2706
2923
|
const owners = /* @__PURE__ */ new Set();
|
|
2707
2924
|
let disposed = false;
|
|
@@ -2725,7 +2942,6 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2725
2942
|
if (disposed || signal?.aborted) throw new Error("claude_code was aborted");
|
|
2726
2943
|
const cancellation = new AbortController();
|
|
2727
2944
|
const runSignal = signal === void 0 ? cancellation.signal : AbortSignal.any([signal, cancellation.signal]);
|
|
2728
|
-
const startedAt = Date.now();
|
|
2729
2945
|
const state = {
|
|
2730
2946
|
stdout: "",
|
|
2731
2947
|
stderr: "",
|
|
@@ -2733,10 +2949,10 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2733
2949
|
silent: false,
|
|
2734
2950
|
sawOutput: false,
|
|
2735
2951
|
settled: false,
|
|
2736
|
-
denies: 0
|
|
2737
|
-
startedAt
|
|
2952
|
+
denies: 0
|
|
2738
2953
|
};
|
|
2739
2954
|
let child;
|
|
2955
|
+
let plans;
|
|
2740
2956
|
let bridge;
|
|
2741
2957
|
let bridgeClosing;
|
|
2742
2958
|
let pendingBridge;
|
|
@@ -2789,13 +3005,18 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2789
3005
|
owners.delete(owner);
|
|
2790
3006
|
done();
|
|
2791
3007
|
});
|
|
3008
|
+
plans?.close();
|
|
2792
3009
|
return;
|
|
2793
3010
|
}
|
|
2794
3011
|
try {
|
|
2795
3012
|
await closeBridge();
|
|
2796
3013
|
} finally {
|
|
2797
|
-
|
|
2798
|
-
|
|
3014
|
+
try {
|
|
3015
|
+
plans?.close();
|
|
3016
|
+
} finally {
|
|
3017
|
+
owners.delete(owner);
|
|
3018
|
+
done();
|
|
3019
|
+
}
|
|
2799
3020
|
}
|
|
2800
3021
|
};
|
|
2801
3022
|
const checkAborted = () => {
|
|
@@ -2812,6 +3033,10 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2812
3033
|
}
|
|
2813
3034
|
};
|
|
2814
3035
|
try {
|
|
3036
|
+
if (mode === "plan") {
|
|
3037
|
+
plans = createOwnedPlanDirectory(planFs, cwd);
|
|
3038
|
+
state.planDirectory = plans.directory;
|
|
3039
|
+
}
|
|
2815
3040
|
const credential = await setup(resolveCredential({
|
|
2816
3041
|
model,
|
|
2817
3042
|
signal: runSignal
|
|
@@ -2846,7 +3071,8 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2846
3071
|
stream,
|
|
2847
3072
|
appendSystemPrompt,
|
|
2848
3073
|
searchBridge: bridge,
|
|
2849
|
-
disableNativeSearch: typeof createSearchBridge === "function"
|
|
3074
|
+
disableNativeSearch: typeof createSearchBridge === "function",
|
|
3075
|
+
planDirectory: plans?.directory
|
|
2850
3076
|
});
|
|
2851
3077
|
child = spawnImpl(command.command, [...command.args, ...args], {
|
|
2852
3078
|
cwd,
|
|
@@ -2877,17 +3103,26 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2877
3103
|
child.stderr?.setEncoding?.("utf8");
|
|
2878
3104
|
let controlBuffer = "";
|
|
2879
3105
|
let stdinEnded = false;
|
|
3106
|
+
const onStdinError = () => {
|
|
3107
|
+
if (state.settled || runSignal.aborted || typeof state.plan === "string") return;
|
|
3108
|
+
state.stdinFailed = true;
|
|
3109
|
+
kill();
|
|
3110
|
+
};
|
|
2880
3111
|
const endStdin = () => {
|
|
2881
3112
|
if (stdinEnded) return;
|
|
2882
3113
|
stdinEnded = true;
|
|
2883
3114
|
try {
|
|
2884
3115
|
child.stdin?.end?.();
|
|
2885
|
-
} catch {
|
|
3116
|
+
} catch {
|
|
3117
|
+
onStdinError();
|
|
3118
|
+
}
|
|
2886
3119
|
};
|
|
2887
3120
|
const writeStdin = (frame) => {
|
|
2888
3121
|
try {
|
|
2889
3122
|
child.stdin?.write?.(frame);
|
|
2890
|
-
} catch {
|
|
3123
|
+
} catch {
|
|
3124
|
+
onStdinError();
|
|
3125
|
+
}
|
|
2891
3126
|
};
|
|
2892
3127
|
const controlResponse = (requestId, response) => {
|
|
2893
3128
|
writeStdin(`${JSON.stringify({
|
|
@@ -2904,12 +3139,22 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2904
3139
|
endStdin();
|
|
2905
3140
|
kill();
|
|
2906
3141
|
};
|
|
2907
|
-
const
|
|
2908
|
-
|
|
3142
|
+
const toolUses = /* @__PURE__ */ new Map();
|
|
3143
|
+
const deniedTools = /* @__PURE__ */ new Set();
|
|
3144
|
+
const deniedRequests = /* @__PURE__ */ new Set();
|
|
3145
|
+
const countDenial = (requestId, toolId) => {
|
|
3146
|
+
const duplicate = requestId !== void 0 && deniedRequests.has(requestId) || toolId !== void 0 && deniedTools.has(toolId);
|
|
3147
|
+
if (requestId !== void 0) deniedRequests.add(requestId);
|
|
3148
|
+
if (toolId !== void 0) deniedTools.add(toolId);
|
|
3149
|
+
if (!duplicate) state.denies += 1;
|
|
2909
3150
|
if (state.denies > MAX_PLAN_DENIALS) {
|
|
2910
3151
|
stopForDenials();
|
|
2911
|
-
return;
|
|
3152
|
+
return false;
|
|
2912
3153
|
}
|
|
3154
|
+
return true;
|
|
3155
|
+
};
|
|
3156
|
+
const refuseControl = (requestId, kind, toolId) => {
|
|
3157
|
+
if (!countDenial(requestId, toolId)) return;
|
|
2913
3158
|
if (kind === "dialog") {
|
|
2914
3159
|
writeStdin(`${JSON.stringify({
|
|
2915
3160
|
type: "control_response",
|
|
@@ -2928,10 +3173,12 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2928
3173
|
};
|
|
2929
3174
|
/**
|
|
2930
3175
|
* Plan mode keeps stdin open so a permission prompt can be answered.
|
|
2931
|
-
*
|
|
2932
|
-
*
|
|
3176
|
+
* ExitPlanMode is never allowed: stop with the submitted plan instead.
|
|
3177
|
+
* Native hard denials share the same budget, keyed by tool_use_id so the
|
|
3178
|
+
* tool_result following a control denial cannot charge it a second time.
|
|
2933
3179
|
*/
|
|
2934
3180
|
const handlePlanLine = (line) => {
|
|
3181
|
+
if (state.denyExhausted === true || typeof state.plan === "string") return;
|
|
2935
3182
|
const trimmed = line.trim();
|
|
2936
3183
|
if (!trimmed.startsWith("{")) return;
|
|
2937
3184
|
let message;
|
|
@@ -2941,44 +3188,64 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
2941
3188
|
return;
|
|
2942
3189
|
}
|
|
2943
3190
|
if (message?.type === "result") {
|
|
3191
|
+
state.resultError = message.is_error === true;
|
|
2944
3192
|
endStdin();
|
|
2945
3193
|
return;
|
|
2946
3194
|
}
|
|
3195
|
+
if (state.resultError !== void 0) return;
|
|
3196
|
+
if (message?.type === "assistant") {
|
|
3197
|
+
for (const block of Array.isArray(message.message?.content) ? message.message.content : []) {
|
|
3198
|
+
if (block?.type !== "tool_use" || typeof block.id !== "string") continue;
|
|
3199
|
+
toolUses.set(block.id, {
|
|
3200
|
+
name: block.name,
|
|
3201
|
+
input: block.input
|
|
3202
|
+
});
|
|
3203
|
+
if (toolUses.size > 512) toolUses.delete(toolUses.keys().next().value);
|
|
3204
|
+
}
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
if (message?.type === "user") {
|
|
3208
|
+
for (const block of Array.isArray(message.message?.content) ? message.message.content : []) {
|
|
3209
|
+
if (block?.type !== "tool_result" || block.is_error !== true || typeof block.tool_use_id !== "string") continue;
|
|
3210
|
+
if (nativePermissionDenial(toolUses.get(block.tool_use_id)?.name, toolResultText(block.content).trim()) && !countDenial(void 0, block.tool_use_id)) return;
|
|
3211
|
+
}
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
2947
3214
|
if (message?.type !== "control_request") return;
|
|
2948
3215
|
const requestId = message.request_id;
|
|
2949
3216
|
const request = message.request;
|
|
2950
3217
|
if (typeof requestId !== "string" || request === null || typeof request !== "object") {
|
|
2951
|
-
|
|
2952
|
-
if (state.denies > MAX_PLAN_DENIALS) stopForDenials();
|
|
3218
|
+
countDenial(typeof requestId === "string" ? requestId : void 0, void 0);
|
|
2953
3219
|
return;
|
|
2954
3220
|
}
|
|
3221
|
+
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];
|
|
2955
3222
|
if (request.subtype !== "can_use_tool") {
|
|
2956
|
-
refuseControl(requestId, "dialog");
|
|
3223
|
+
refuseControl(requestId, "dialog", toolId);
|
|
2957
3224
|
return;
|
|
2958
3225
|
}
|
|
2959
3226
|
if (request.tool_name === "ExitPlanMode") {
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3227
|
+
let tooLarge = false;
|
|
3228
|
+
const plan = typeof request.input?.plan === "string" || plans?.isOwned() ? readSubmittedPlan(request.input, {
|
|
3229
|
+
planDirectory: plans?.directory,
|
|
3230
|
+
scrub,
|
|
3231
|
+
onRejected: (reason) => {
|
|
3232
|
+
tooLarge = reason === "too-large";
|
|
3233
|
+
}
|
|
3234
|
+
}) : void 0;
|
|
2964
3235
|
if (plan !== void 0) {
|
|
2965
|
-
state.plan =
|
|
3236
|
+
state.plan = plan;
|
|
2966
3237
|
endStdin();
|
|
2967
3238
|
kill();
|
|
2968
3239
|
return;
|
|
2969
3240
|
}
|
|
2970
|
-
|
|
2971
|
-
if (state.denies > MAX_PLAN_DENIALS) {
|
|
2972
|
-
stopForDenials();
|
|
2973
|
-
return;
|
|
2974
|
-
}
|
|
3241
|
+
if (!countDenial(requestId, toolId)) return;
|
|
2975
3242
|
controlResponse(requestId, {
|
|
2976
3243
|
behavior: "deny",
|
|
2977
|
-
message: PLAN_EXIT_DENY
|
|
3244
|
+
message: tooLarge ? PLAN_TOO_LARGE_DENY : PLAN_EXIT_DENY
|
|
2978
3245
|
});
|
|
2979
3246
|
return;
|
|
2980
3247
|
}
|
|
2981
|
-
refuseControl(requestId, "tool");
|
|
3248
|
+
refuseControl(requestId, "tool", toolId);
|
|
2982
3249
|
};
|
|
2983
3250
|
child.stdout?.on("data", (chunk) => {
|
|
2984
3251
|
state.sawOutput = true;
|
|
@@ -3008,18 +3275,32 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
3008
3275
|
child.on("close", (code, killSignal) => {
|
|
3009
3276
|
if (state.settled) return;
|
|
3010
3277
|
state.settled = true;
|
|
3278
|
+
if (mode === "plan" && controlBuffer.trim() !== "") handlePlanLine(controlBuffer);
|
|
3011
3279
|
if (state.timedOut) return reject(/* @__PURE__ */ new Error(`claude_code timed out after ${Math.round(timeoutMs / 1e3)}s`));
|
|
3012
3280
|
if (state.silent) {
|
|
3013
3281
|
const detail = state.stderr.trim().split("\n").slice(-4).join(" ").slice(0, 400);
|
|
3014
3282
|
return reject(/* @__PURE__ */ new Error(`Claude Code CLI printed nothing within ${Math.round(startupTimeoutMs / 1e3)}s of starting and was stopped${detail === "" ? "" : `: ${detail}`}`));
|
|
3015
3283
|
}
|
|
3016
3284
|
if (signal?.aborted) return reject(/* @__PURE__ */ new Error("claude_code was aborted"));
|
|
3285
|
+
if (state.stdinFailed === true) return reject(/* @__PURE__ */ new Error("claude_code could not write to CLI input"));
|
|
3017
3286
|
if (state.denyExhausted === true) return reject(/* @__PURE__ */ new Error(PLAN_DENY_LIMIT_MESSAGE));
|
|
3018
3287
|
if (typeof state.plan === "string") return resolve({
|
|
3019
3288
|
code,
|
|
3020
3289
|
killSignal
|
|
3021
3290
|
});
|
|
3022
3291
|
if (runSignal.aborted) return reject(/* @__PURE__ */ new Error("claude_code was aborted"));
|
|
3292
|
+
if (mode === "plan" && code === 0 && state.resultError === false && plans?.isOwned()) {
|
|
3293
|
+
let tooLarge = false;
|
|
3294
|
+
const written = findPlanWrittenDuringRun({
|
|
3295
|
+
planDirectory: plans.directory,
|
|
3296
|
+
scrub,
|
|
3297
|
+
onRejected: (reason) => {
|
|
3298
|
+
tooLarge ||= reason === "too-large";
|
|
3299
|
+
}
|
|
3300
|
+
});
|
|
3301
|
+
if (tooLarge) return reject(/* @__PURE__ */ new Error(PLAN_TOO_LARGE_DENY));
|
|
3302
|
+
if (written !== void 0) state.plan = written;
|
|
3303
|
+
}
|
|
3023
3304
|
resolve({
|
|
3024
3305
|
code,
|
|
3025
3306
|
killSignal
|
|
@@ -3033,7 +3314,7 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
3033
3314
|
throw error;
|
|
3034
3315
|
});
|
|
3035
3316
|
closed.catch(() => void 0);
|
|
3036
|
-
child.stdin?.on?.("error",
|
|
3317
|
+
child.stdin?.on?.("error", onStdinError);
|
|
3037
3318
|
if (mode === "plan") writeStdin(input);
|
|
3038
3319
|
else child.stdin?.end?.(input);
|
|
3039
3320
|
if (runSignal.aborted) kill();
|
|
@@ -3217,13 +3498,6 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
3217
3498
|
const event = readClaudeStreamEvents(JSON.parse(scrub(buffer.trim()))).find((item) => item.type === "result");
|
|
3218
3499
|
if (event !== void 0) sawResult = event.result;
|
|
3219
3500
|
} 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
3501
|
if (typeof state.plan === "string") {
|
|
3228
3502
|
if (deltaKind !== void 0) {
|
|
3229
3503
|
const tail = deltaRedactor.flush({ boundary: true });
|
|
@@ -3272,7 +3546,11 @@ function createClaudeRunner({ binary = findClaudeBinary(), configDir = DEFAULT_C
|
|
|
3272
3546
|
};
|
|
3273
3547
|
} finally {
|
|
3274
3548
|
stopIfAbandoned();
|
|
3275
|
-
|
|
3549
|
+
try {
|
|
3550
|
+
await closed.catch(() => void 0);
|
|
3551
|
+
} finally {
|
|
3552
|
+
await closeBridge();
|
|
3553
|
+
}
|
|
3276
3554
|
}
|
|
3277
3555
|
},
|
|
3278
3556
|
/** Revoke bridges and stop every invocation when the plugin unloads. */
|
|
@@ -4084,13 +4362,15 @@ function claudeModeFor(sandbox, approval) {
|
|
|
4084
4362
|
}
|
|
4085
4363
|
}
|
|
4086
4364
|
/**
|
|
4087
|
-
* Whether
|
|
4365
|
+
* Whether a prepared model request should run the CLI in plan mode.
|
|
4088
4366
|
*
|
|
4089
|
-
*
|
|
4090
|
-
*
|
|
4091
|
-
*
|
|
4092
|
-
*
|
|
4093
|
-
*
|
|
4367
|
+
* This selection is for request preparation only, not an executing tool batch.
|
|
4368
|
+
* `pending` is the selection used for the proposed request assembly and wins
|
|
4369
|
+
* over logged `active`, matching `plan:policy`. An accepted pre-step commits
|
|
4370
|
+
* it; reading after that boundary sees the same selection as logged `active`.
|
|
4371
|
+
* Without the service, the logged projection is the only signal. A missing
|
|
4372
|
+
* session, a missing service, or a throw means plan mode is off — never a
|
|
4373
|
+
* failed turn.
|
|
4094
4374
|
* @param ctx - the plugin context; services are read without an inject requirement.
|
|
4095
4375
|
* @param session - the session whose plan state is read.
|
|
4096
4376
|
* @returns true when the CLI must plan instead of acting.
|
|
@@ -4108,7 +4388,30 @@ function planModeForSession(ctx, session) {
|
|
|
4108
4388
|
return false;
|
|
4109
4389
|
}
|
|
4110
4390
|
}
|
|
4111
|
-
/**
|
|
4391
|
+
/**
|
|
4392
|
+
* Whether a tool in the current batch must plan, using logged active state.
|
|
4393
|
+
*
|
|
4394
|
+
* Approval queues `pending: false`; it does not authorize remaining tools in
|
|
4395
|
+
* that batch to implement. Likewise, pending entry must not change a tool's
|
|
4396
|
+
* current mode before the next accepted pre-step commits it.
|
|
4397
|
+
* @param ctx - the plugin context.
|
|
4398
|
+
* @param session - the executing tool's session.
|
|
4399
|
+
* @param agent - the trusted executing agent, when provided by the tool runtime.
|
|
4400
|
+
*/
|
|
4401
|
+
function planModeForToolExecution(ctx, session, agent) {
|
|
4402
|
+
if (session === void 0 || session === null) return false;
|
|
4403
|
+
try {
|
|
4404
|
+
const caller = agent ?? ctx.get?.("agents")?.currentInitiator?.();
|
|
4405
|
+
if (caller?.session === session) {
|
|
4406
|
+
const state = ctx.get?.("planMode")?.get?.(caller);
|
|
4407
|
+
if (state !== void 0 && state !== null) return state.active === true;
|
|
4408
|
+
}
|
|
4409
|
+
return ctx.get?.("sessionProjections")?.stateOf?.(session, "plan")?.active === true;
|
|
4410
|
+
} catch {
|
|
4411
|
+
return false;
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
/** `pending` is the selection the proposed request was assembled with. */
|
|
4112
4415
|
function planSelected(state) {
|
|
4113
4416
|
if (typeof state?.pending === "boolean") return state.pending;
|
|
4114
4417
|
return state?.active === true;
|
|
@@ -4161,7 +4464,8 @@ const CLAUDE_TOOL_INSTRUCTIONS = "This task was delegated to Claude Code by anot
|
|
|
4161
4464
|
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.";
|
|
4162
4465
|
/**
|
|
4163
4466
|
* @param options - the CLI runner, the most a run may do for a session, and
|
|
4164
|
-
* whether
|
|
4467
|
+
* whether the executing batch's logged state is DSH plan mode (not a
|
|
4468
|
+
* pending selection for the next model request).
|
|
4165
4469
|
* @returns the tool definition to register.
|
|
4166
4470
|
*/
|
|
4167
4471
|
function createClaudeCodeTool({ runner, modeCeiling, planActive }) {
|
|
@@ -4225,7 +4529,7 @@ function createClaudeCodeTool({ runner, modeCeiling, planActive }) {
|
|
|
4225
4529
|
const requested = typeof args.cwd === "string" ? args.cwd.trim() : "";
|
|
4226
4530
|
const cwd = requested === "" ? sessionCwd : isAbsolute(requested) ? requested : sessionCwd === void 0 ? requested : join(sessionCwd, requested);
|
|
4227
4531
|
const session = exec?.agent?.session;
|
|
4228
|
-
const planning = planActive?.(session) === true;
|
|
4532
|
+
const planning = planActive?.(session, exec?.agent) === true;
|
|
4229
4533
|
return await runner.run({
|
|
4230
4534
|
prompt: args.prompt,
|
|
4231
4535
|
mode: planning ? "plan" : capClaudeMode(args.mode, modeCeiling(session)) ?? "edit",
|
|
@@ -4612,6 +4916,134 @@ var PackyCodeKeyRing = class {
|
|
|
4612
4916
|
}
|
|
4613
4917
|
};
|
|
4614
4918
|
//#endregion
|
|
4919
|
+
//#region src/preferences.js
|
|
4920
|
+
/**
|
|
4921
|
+
* Where the plugin's preferences are read from and written to, on both
|
|
4922
|
+
* settings generations DSH has shipped.
|
|
4923
|
+
*
|
|
4924
|
+
* - DSH 0.1.5 installs a settings *section*: the section hands back a live
|
|
4925
|
+
* source and writes go through `settings.update(namespace, patch)`.
|
|
4926
|
+
* - DSH 0.2 has no sections: a preference is a *volatile* Config field of the
|
|
4927
|
+
* plugin's own row, read through a live reference, written through
|
|
4928
|
+
* `settings.update(rowId, patch)`, and announced by `loader/volatile-update`.
|
|
4929
|
+
* Only volatile fields are writable there, so the host overrides stay
|
|
4930
|
+
* read-only while web search and model visibility are editable.
|
|
4931
|
+
*
|
|
4932
|
+
* @module dsh-packycode-api/preferences
|
|
4933
|
+
*/
|
|
4934
|
+
/**
|
|
4935
|
+
* Mark a preference editable without restarting the plugin. DSH 0.2 stores
|
|
4936
|
+
* plugin preferences as volatile Config fields and writes only those. The
|
|
4937
|
+
* peer dependency requires the schema version that implements `volatile()`.
|
|
4938
|
+
*/
|
|
4939
|
+
const live = (schema) => schema.volatile();
|
|
4940
|
+
/** Read a Config field that may arrive as a volatile reference. */
|
|
4941
|
+
const fieldValue = (value) => value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
|
|
4942
|
+
/** A preference target nothing can write to yet. */
|
|
4943
|
+
function unwritable(message) {
|
|
4944
|
+
return {
|
|
4945
|
+
writable: false,
|
|
4946
|
+
set: async () => {
|
|
4947
|
+
throw new Error(message);
|
|
4948
|
+
}
|
|
4949
|
+
};
|
|
4950
|
+
}
|
|
4951
|
+
/**
|
|
4952
|
+
* Connect the preferences to the settings service the host provides.
|
|
4953
|
+
* @param settingsCtx - the scope the `settings` inject handed over.
|
|
4954
|
+
* @param options - the plugin context and Config, the section namespace, the
|
|
4955
|
+
* current source and how to replace it, what to do when the section changes,
|
|
4956
|
+
* and the preference targets this fills in (host overrides, search, models).
|
|
4957
|
+
*/
|
|
4958
|
+
function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference, modelPreference }) {
|
|
4959
|
+
const settings = settingsCtx.settings;
|
|
4960
|
+
if (typeof settings?.installSection === "function") {
|
|
4961
|
+
try {
|
|
4962
|
+
settings.installSection(ctx, namespace, Config, source(), {
|
|
4963
|
+
setSource: (current) => setSource(typeof current === "function" ? () => current() ?? {} : () => current ?? {}),
|
|
4964
|
+
onChange
|
|
4965
|
+
});
|
|
4966
|
+
if (typeof settings.update === "function") {
|
|
4967
|
+
const write = (patch) => settings.update(namespace, patch);
|
|
4968
|
+
const target = {
|
|
4969
|
+
writable: settings.writable !== false,
|
|
4970
|
+
set: write
|
|
4971
|
+
};
|
|
4972
|
+
Object.assign(hostPreference, target);
|
|
4973
|
+
Object.assign(searchPreference, target);
|
|
4974
|
+
Object.assign(modelPreference, target);
|
|
4975
|
+
}
|
|
4976
|
+
} catch (error) {
|
|
4977
|
+
ctx.logger?.warn?.("packycode-api: host overrides unavailable, defaults stand: %s", error.message);
|
|
4978
|
+
}
|
|
4979
|
+
return;
|
|
4980
|
+
}
|
|
4981
|
+
if (typeof settings?.update !== "function") return;
|
|
4982
|
+
const rowId = ctx.fiber?.entry?.options?.id ?? namespace;
|
|
4983
|
+
const target = {
|
|
4984
|
+
writable: settings.writable !== false,
|
|
4985
|
+
set: (patch) => settings.update(rowId, patch)
|
|
4986
|
+
};
|
|
4987
|
+
Object.assign(searchPreference, target);
|
|
4988
|
+
Object.assign(modelPreference, target);
|
|
4989
|
+
if (typeof settings.configure === "function") {
|
|
4990
|
+
const scope = typeof settingsCtx.effect === "function" ? settingsCtx : ctx;
|
|
4991
|
+
try {
|
|
4992
|
+
scope.effect(() => settings.configure({ auto: false }, ctx.fiber), "packycode-api: settings page policy");
|
|
4993
|
+
} catch (error) {
|
|
4994
|
+
ctx.logger?.warn?.("packycode-api: could not hide the generated settings page: %s", error.message);
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
//#endregion
|
|
4999
|
+
//#region src/model-selection.js
|
|
5000
|
+
/** Profile preferences for the models advertised in PackyCode's picker. */
|
|
5001
|
+
/** Store exclusions so new catalog models are selected by default as well. */
|
|
5002
|
+
function readHiddenModels(preference) {
|
|
5003
|
+
const value = fieldValue(preference?.hiddenModels);
|
|
5004
|
+
if (!Array.isArray(value)) return [];
|
|
5005
|
+
const hidden = new Set(value);
|
|
5006
|
+
return PACKYCODE_MODEL_IDS.filter((id) => hidden.has(id));
|
|
5007
|
+
}
|
|
5008
|
+
function createModelSelectionController({ readPreference, writePreference, isWritable, onChange }) {
|
|
5009
|
+
const hidden = () => readHiddenModels(readPreference());
|
|
5010
|
+
let advertised = hidden().join(",");
|
|
5011
|
+
const reconcile = () => {
|
|
5012
|
+
const next = hidden().join(",");
|
|
5013
|
+
if (next === advertised) return;
|
|
5014
|
+
onChange?.();
|
|
5015
|
+
advertised = next;
|
|
5016
|
+
};
|
|
5017
|
+
const status = () => {
|
|
5018
|
+
const excluded = new Set(hidden());
|
|
5019
|
+
return {
|
|
5020
|
+
writable: isWritable() === true,
|
|
5021
|
+
models: PACKYCODE_MODELS.map(({ id, name }) => ({
|
|
5022
|
+
id,
|
|
5023
|
+
name,
|
|
5024
|
+
selected: !excluded.has(id)
|
|
5025
|
+
}))
|
|
5026
|
+
};
|
|
5027
|
+
};
|
|
5028
|
+
return Object.freeze({
|
|
5029
|
+
status,
|
|
5030
|
+
reconcile,
|
|
5031
|
+
isVisible: (id) => !hidden().includes(id),
|
|
5032
|
+
async update({ selectedIds } = {}) {
|
|
5033
|
+
if (!Array.isArray(selectedIds) || selectedIds.some((id) => !PACKYCODE_MODEL_IDS.includes(id))) throw new Error("That PackyCode model selection is not supported");
|
|
5034
|
+
if (isWritable() !== true) throw new Error("PackyCode settings are not writable in this profile");
|
|
5035
|
+
const selected = new Set(selectedIds);
|
|
5036
|
+
try {
|
|
5037
|
+
await writePreference({ hiddenModels: PACKYCODE_MODEL_IDS.filter((id) => !selected.has(id)) });
|
|
5038
|
+
} catch {
|
|
5039
|
+
throw new Error("PackyCode could not save the model selection");
|
|
5040
|
+
}
|
|
5041
|
+
reconcile();
|
|
5042
|
+
return status();
|
|
5043
|
+
}
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
//#endregion
|
|
4615
5047
|
//#region src/plugin-version.js
|
|
4616
5048
|
/**
|
|
4617
5049
|
* Version self-knowledge for the settings panel.
|
|
@@ -4934,85 +5366,6 @@ function createPackyCodePluginManager({ fetchImpl = ambientFetch, runCommand = s
|
|
|
4934
5366
|
});
|
|
4935
5367
|
}
|
|
4936
5368
|
//#endregion
|
|
4937
|
-
//#region src/preferences.js
|
|
4938
|
-
/**
|
|
4939
|
-
* Where the plugin's preferences are read from and written to, on both
|
|
4940
|
-
* settings generations DSH has shipped.
|
|
4941
|
-
*
|
|
4942
|
-
* - DSH 0.1.5 installs a settings *section*: the section hands back a live
|
|
4943
|
-
* source and writes go through `settings.update(namespace, patch)`.
|
|
4944
|
-
* - DSH 0.2 has no sections: a preference is a *volatile* Config field of the
|
|
4945
|
-
* plugin's own row, read through a live reference, written through
|
|
4946
|
-
* `settings.update(rowId, patch)`, and announced by `loader/volatile-update`.
|
|
4947
|
-
* Only volatile fields are writable there, so the host overrides stay
|
|
4948
|
-
* read-only while the web search preference is editable.
|
|
4949
|
-
*
|
|
4950
|
-
* @module dsh-packycode-api/preferences
|
|
4951
|
-
*/
|
|
4952
|
-
/**
|
|
4953
|
-
* Mark a preference editable without restarting the plugin. DSH 0.2 stores
|
|
4954
|
-
* plugin preferences as volatile Config fields and writes only those; an older
|
|
4955
|
-
* schema library has no `volatile()` and keeps a plain field.
|
|
4956
|
-
*/
|
|
4957
|
-
const live = (schema) => typeof schema.volatile === "function" ? schema.volatile() : schema;
|
|
4958
|
-
/** Read a Config field that may arrive as a volatile reference. */
|
|
4959
|
-
const fieldValue = (value) => value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
|
|
4960
|
-
/** A preference target nothing can write to yet. */
|
|
4961
|
-
function unwritable(message) {
|
|
4962
|
-
return {
|
|
4963
|
-
writable: false,
|
|
4964
|
-
set: async () => {
|
|
4965
|
-
throw new Error(message);
|
|
4966
|
-
}
|
|
4967
|
-
};
|
|
4968
|
-
}
|
|
4969
|
-
/**
|
|
4970
|
-
* Connect the preferences to the settings service the host provides.
|
|
4971
|
-
* @param settingsCtx - the scope the `settings` inject handed over.
|
|
4972
|
-
* @param options - the plugin context and Config, the section namespace, the
|
|
4973
|
-
* current source and how to replace it, what to do when the section changes,
|
|
4974
|
-
* and the two preference targets this fills in (host overrides, search).
|
|
4975
|
-
*/
|
|
4976
|
-
function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference }) {
|
|
4977
|
-
const settings = settingsCtx.settings;
|
|
4978
|
-
if (typeof settings?.installSection === "function") {
|
|
4979
|
-
try {
|
|
4980
|
-
settings.installSection(ctx, namespace, Config, source(), {
|
|
4981
|
-
setSource: (current) => setSource(typeof current === "function" ? () => current() ?? {} : () => current ?? {}),
|
|
4982
|
-
onChange
|
|
4983
|
-
});
|
|
4984
|
-
if (typeof settings.update === "function") {
|
|
4985
|
-
const write = (patch) => settings.update(namespace, patch);
|
|
4986
|
-
Object.assign(hostPreference, {
|
|
4987
|
-
writable: true,
|
|
4988
|
-
set: write
|
|
4989
|
-
});
|
|
4990
|
-
Object.assign(searchPreference, {
|
|
4991
|
-
writable: true,
|
|
4992
|
-
set: write
|
|
4993
|
-
});
|
|
4994
|
-
}
|
|
4995
|
-
} catch (error) {
|
|
4996
|
-
ctx.logger?.warn?.("packycode-api: host overrides unavailable, defaults stand: %s", error.message);
|
|
4997
|
-
}
|
|
4998
|
-
return;
|
|
4999
|
-
}
|
|
5000
|
-
if (typeof settings?.update !== "function") return;
|
|
5001
|
-
const rowId = ctx.fiber?.entry?.options?.id ?? namespace;
|
|
5002
|
-
Object.assign(searchPreference, {
|
|
5003
|
-
writable: settings.writable !== false,
|
|
5004
|
-
set: (patch) => settings.update(rowId, patch)
|
|
5005
|
-
});
|
|
5006
|
-
if (typeof settings.configure === "function") {
|
|
5007
|
-
const scope = typeof settingsCtx.effect === "function" ? settingsCtx : ctx;
|
|
5008
|
-
try {
|
|
5009
|
-
scope.effect(() => settings.configure({ auto: false }, ctx.fiber), "packycode-api: settings page policy");
|
|
5010
|
-
} catch (error) {
|
|
5011
|
-
ctx.logger?.warn?.("packycode-api: could not hide the generated settings page: %s", error.message);
|
|
5012
|
-
}
|
|
5013
|
-
}
|
|
5014
|
-
}
|
|
5015
|
-
//#endregion
|
|
5016
5369
|
//#region src/provider.js
|
|
5017
5370
|
/**
|
|
5018
5371
|
* The PackyCode pi-ai provider, built in code so the user configures no route.
|
|
@@ -5042,7 +5395,7 @@ function keyRingAuth() {
|
|
|
5042
5395
|
}
|
|
5043
5396
|
/** Build the route's pi-ai provider (unguarded; the plugin wraps its streams). */
|
|
5044
5397
|
function createPackyCodeProvider() {
|
|
5045
|
-
|
|
5398
|
+
const provider = createProvider({
|
|
5046
5399
|
id: PROVIDER,
|
|
5047
5400
|
name: DISPLAY_NAME,
|
|
5048
5401
|
baseUrl: DEFAULT_API_HOST,
|
|
@@ -5054,6 +5407,15 @@ function createPackyCodeProvider() {
|
|
|
5054
5407
|
"anthropic-messages": anthropicMessagesApi()
|
|
5055
5408
|
}
|
|
5056
5409
|
});
|
|
5410
|
+
const requestOptions = (model, options) => model.api === "openai-responses" && !model.id.startsWith("gpt-") ? {
|
|
5411
|
+
...options,
|
|
5412
|
+
cacheRetention: "none"
|
|
5413
|
+
} : options;
|
|
5414
|
+
return Object.freeze({
|
|
5415
|
+
...provider,
|
|
5416
|
+
stream: (model, context, options) => provider.stream(model, context, requestOptions(model, options)),
|
|
5417
|
+
streamSimple: (model, context, options) => provider.streamSimple(model, context, requestOptions(model, options))
|
|
5418
|
+
});
|
|
5057
5419
|
}
|
|
5058
5420
|
/**
|
|
5059
5421
|
* Observe a pi-ai event stream and hand a key-level failure to the plugin.
|
|
@@ -5134,7 +5496,7 @@ function guardKeyRotation(stream, { onKeyFailure, key } = {}) {
|
|
|
5134
5496
|
//#region src/route.js
|
|
5135
5497
|
/**
|
|
5136
5498
|
* The HTTP model route: the pi-ai provider profile and the adapter that
|
|
5137
|
-
* serves the catalog's HTTP models (DeepSeek and
|
|
5499
|
+
* serves the catalog's HTTP models (DeepSeek, GPT and Grok) with the ring's keys.
|
|
5138
5500
|
*
|
|
5139
5501
|
* @module dsh-packycode-api/route
|
|
5140
5502
|
*/
|
|
@@ -5224,7 +5586,7 @@ function createPackyCodeProfile(provider) {
|
|
|
5224
5586
|
* supplies none, and the attachment store image input is read from.
|
|
5225
5587
|
* @returns a `PiAiAdapter` serving the `packycode` route.
|
|
5226
5588
|
*/
|
|
5227
|
-
function createRouteAdapter({ ring, keys, fallbackSessionId, resolveAttachments }) {
|
|
5589
|
+
function createRouteAdapter({ ring, keys, fallbackSessionId, resolveAttachments, isModelVisible = () => true }) {
|
|
5228
5590
|
const raw = createPackyCodeProvider();
|
|
5229
5591
|
/**
|
|
5230
5592
|
* The gateway's Codex-group check refuses a Responses request that carries
|
|
@@ -5257,7 +5619,11 @@ function createRouteAdapter({ ring, keys, fallbackSessionId, resolveAttachments
|
|
|
5257
5619
|
streamSimple: guarded("streamSimple")
|
|
5258
5620
|
});
|
|
5259
5621
|
const profiles = /* @__PURE__ */ new Map([[PROVIDER, createPackyCodeProfile(provider)]]);
|
|
5260
|
-
return new PiAiAdapter
|
|
5622
|
+
return new class extends PiAiAdapter {
|
|
5623
|
+
async listModels(provider) {
|
|
5624
|
+
return (await super.listModels(provider)).filter((model) => isModelVisible(model.id));
|
|
5625
|
+
}
|
|
5626
|
+
}({
|
|
5261
5627
|
profiles: () => profiles,
|
|
5262
5628
|
resolveApiKey: async () => {
|
|
5263
5629
|
const active = await keys.active();
|
|
@@ -5282,7 +5648,7 @@ function createRouteAdapter({ ring, keys, fallbackSessionId, resolveAttachments
|
|
|
5282
5648
|
* Every reply carries only what this plugin owns: key labels and state, the
|
|
5283
5649
|
* balance and group projections the host built, host overrides, whether this
|
|
5284
5650
|
* device has the Claude Code CLI, the plugin version, and the web search
|
|
5285
|
-
* preference. Key material never crosses this boundary.
|
|
5651
|
+
* preference and model visibility. Key material never crosses this boundary.
|
|
5286
5652
|
*
|
|
5287
5653
|
* @module dsh-packycode-api/rpc
|
|
5288
5654
|
*/
|
|
@@ -5292,7 +5658,11 @@ const ok = (value) => ({
|
|
|
5292
5658
|
});
|
|
5293
5659
|
const failed = (message) => ({
|
|
5294
5660
|
ok: false,
|
|
5295
|
-
error: {
|
|
5661
|
+
error: {
|
|
5662
|
+
code: "packycode/request-failed",
|
|
5663
|
+
message,
|
|
5664
|
+
details: {}
|
|
5665
|
+
}
|
|
5296
5666
|
});
|
|
5297
5667
|
/** Only sentences this plugin authored may reach the browser. */
|
|
5298
5668
|
const SAFE_MESSAGE = /^(?:PackyCode|That |Unknown |PackyCode's |add |the )[\s\S]{0,180}$/u;
|
|
@@ -5304,7 +5674,7 @@ const keyId = (payload) => String(payload?.id ?? "");
|
|
|
5304
5674
|
* web search controller (without one, its endpoints refuse).
|
|
5305
5675
|
* @returns a handler for `connection.rpc.handle(CHANNEL, handler)`.
|
|
5306
5676
|
*/
|
|
5307
|
-
function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCli, pluginManager, search }) {
|
|
5677
|
+
function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCli, pluginManager, search, modelSelection }) {
|
|
5308
5678
|
const need = (service, message) => {
|
|
5309
5679
|
if (service === void 0) throw new Error(message);
|
|
5310
5680
|
return service;
|
|
@@ -5313,6 +5683,7 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
|
|
|
5313
5683
|
const manager = () => need(pluginManager, "PackyCode plugin version is unavailable");
|
|
5314
5684
|
const webSearch = () => need(search, "PackyCode web search is unavailable in this runtime");
|
|
5315
5685
|
const groupReader = () => need(groups, "PackyCode key groups are unavailable in this runtime");
|
|
5686
|
+
const models = () => need(modelSelection, "PackyCode model selection is unavailable in this runtime");
|
|
5316
5687
|
const status = async () => ({
|
|
5317
5688
|
...await ring.list(),
|
|
5318
5689
|
hosts: hosts(),
|
|
@@ -5377,7 +5748,9 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
|
|
|
5377
5748
|
"search/update": (payload) => webSearch().update({
|
|
5378
5749
|
searchProvider: payload?.searchProvider,
|
|
5379
5750
|
searchBackend: payload?.searchBackend
|
|
5380
|
-
})
|
|
5751
|
+
}),
|
|
5752
|
+
"models/status": () => models().status(),
|
|
5753
|
+
"models/update": (payload) => models().update({ selectedIds: payload?.selectedIds })
|
|
5381
5754
|
};
|
|
5382
5755
|
return async (endpoint, payload, signal) => {
|
|
5383
5756
|
try {
|
|
@@ -6122,9 +6495,11 @@ const FOREIGN_ROUTERS = Object.freeze({
|
|
|
6122
6495
|
});
|
|
6123
6496
|
/** Normalize a stored preference; anything unknown reads as the default. */
|
|
6124
6497
|
function readSearchChoice(value) {
|
|
6498
|
+
const provider = fieldValue(value?.searchProvider);
|
|
6499
|
+
const selectedBackend = fieldValue(value?.searchBackend);
|
|
6125
6500
|
return {
|
|
6126
|
-
mode: SEARCH_MODES.includes(
|
|
6127
|
-
backend: SEARCH_BACKENDS.includes(
|
|
6501
|
+
mode: SEARCH_MODES.includes(provider) ? provider : SEARCH_MODE_DEFAULT,
|
|
6502
|
+
backend: SEARCH_BACKENDS.includes(selectedBackend) ? selectedBackend : DEFAULT_SEARCH_BACKEND
|
|
6128
6503
|
};
|
|
6129
6504
|
}
|
|
6130
6505
|
/** Only sentences this plugin wrote reach the browser. */
|
|
@@ -6308,7 +6683,9 @@ const Config = z.object({
|
|
|
6308
6683
|
*/
|
|
6309
6684
|
searchProvider: live(z.union([...SEARCH_MODES]).default(SEARCH_MODE_DEFAULT)),
|
|
6310
6685
|
/** Which PackyCode model runs a search: `deepseek` or `gpt`. */
|
|
6311
|
-
searchBackend: live(z.union([...SEARCH_BACKENDS]).default(DEFAULT_SEARCH_BACKEND))
|
|
6686
|
+
searchBackend: live(z.union([...SEARCH_BACKENDS]).default(DEFAULT_SEARCH_BACKEND)),
|
|
6687
|
+
/** Only the picker is filtered; an existing session can keep its model. */
|
|
6688
|
+
hiddenModels: live(z.array(z.string()).default([]))
|
|
6312
6689
|
});
|
|
6313
6690
|
function apply(ctx, config) {
|
|
6314
6691
|
const ring = new PackyCodeKeyRing(ctx.credentials);
|
|
@@ -6316,7 +6693,8 @@ function apply(ctx, config) {
|
|
|
6316
6693
|
apiHost: normalizeHost(config?.apiHost, DEFAULT_API_HOST),
|
|
6317
6694
|
statusHost: normalizeHost(config?.statusHost, DEFAULT_STATUS_HOST),
|
|
6318
6695
|
searchProvider: fieldValue(config?.searchProvider),
|
|
6319
|
-
searchBackend: fieldValue(config?.searchBackend)
|
|
6696
|
+
searchBackend: fieldValue(config?.searchBackend),
|
|
6697
|
+
hiddenModels: fieldValue(config?.hiddenModels)
|
|
6320
6698
|
});
|
|
6321
6699
|
const hosts = () => {
|
|
6322
6700
|
const current = source() ?? {};
|
|
@@ -6362,6 +6740,17 @@ function apply(ctx, config) {
|
|
|
6362
6740
|
};
|
|
6363
6741
|
const fallbackSessionId = `${PLUGIN_ID}-${randomUUID()}`;
|
|
6364
6742
|
const attachments = () => ctx.get?.("attachments") ?? ctx.attachments;
|
|
6743
|
+
const modelPreference = {
|
|
6744
|
+
writable: false,
|
|
6745
|
+
set: void 0
|
|
6746
|
+
};
|
|
6747
|
+
let refreshModels = () => {};
|
|
6748
|
+
const modelSelection = createModelSelectionController({
|
|
6749
|
+
readPreference: () => source() ?? {},
|
|
6750
|
+
writePreference: (patch) => modelPreference.set(patch),
|
|
6751
|
+
isWritable: () => modelPreference.writable === true && typeof modelPreference.set === "function",
|
|
6752
|
+
onChange: () => refreshModels()
|
|
6753
|
+
});
|
|
6365
6754
|
/**
|
|
6366
6755
|
* The Claude Code CLI runner. PackyCode serves its Claude models only to the
|
|
6367
6756
|
* official CLI, so CLI-backed models are run by that CLI itself — with this
|
|
@@ -6420,12 +6809,13 @@ function apply(ctx, config) {
|
|
|
6420
6809
|
const configured = config?.claudeCliMode;
|
|
6421
6810
|
return configured === void 0 || configured === "auto" ? claudeModeForSession(ctx, session) : configured;
|
|
6422
6811
|
};
|
|
6423
|
-
ctx.llm.registerAdapter([PROVIDER], createClaudeCliAdapter({
|
|
6812
|
+
const registration = ctx.llm.registerAdapter([PROVIDER], createClaudeCliAdapter({
|
|
6424
6813
|
inner: createRouteAdapter({
|
|
6425
6814
|
ring,
|
|
6426
6815
|
keys,
|
|
6427
6816
|
fallbackSessionId,
|
|
6428
|
-
resolveAttachments: attachments
|
|
6817
|
+
resolveAttachments: attachments,
|
|
6818
|
+
isModelVisible: modelSelection.isVisible
|
|
6429
6819
|
}),
|
|
6430
6820
|
runner: claudeRunner,
|
|
6431
6821
|
resolveMode: () => {
|
|
@@ -6436,6 +6826,10 @@ function apply(ctx, config) {
|
|
|
6436
6826
|
resolveAttachments: attachments,
|
|
6437
6827
|
explainKey: (runKey) => keys.describe(runKey)
|
|
6438
6828
|
}));
|
|
6829
|
+
refreshModels = () => {
|
|
6830
|
+
if (typeof registration?.replace === "function") registration.replace([PROVIDER]);
|
|
6831
|
+
else ctx.emit?.("llm/adapters-updated");
|
|
6832
|
+
};
|
|
6439
6833
|
ctx.llm.registerConfigurableProviders([{
|
|
6440
6834
|
provider: PROVIDER,
|
|
6441
6835
|
displayName: DISPLAY_NAME,
|
|
@@ -6487,12 +6881,15 @@ function apply(ctx, config) {
|
|
|
6487
6881
|
onChange: () => {
|
|
6488
6882
|
balance.invalidate();
|
|
6489
6883
|
groups.invalidate();
|
|
6884
|
+
modelSelection.reconcile();
|
|
6490
6885
|
search.reconcile();
|
|
6491
6886
|
},
|
|
6492
6887
|
hostPreference,
|
|
6493
|
-
searchPreference
|
|
6888
|
+
searchPreference,
|
|
6889
|
+
modelPreference
|
|
6494
6890
|
}));
|
|
6495
6891
|
ctx.on?.("loader/volatile-update", () => {
|
|
6892
|
+
modelSelection.reconcile();
|
|
6496
6893
|
search.reconcile();
|
|
6497
6894
|
});
|
|
6498
6895
|
/**
|
|
@@ -6514,7 +6911,8 @@ function apply(ctx, config) {
|
|
|
6514
6911
|
preference: hostPreference,
|
|
6515
6912
|
claudeCli,
|
|
6516
6913
|
pluginManager: createPackyCodePluginManager(),
|
|
6517
|
-
search
|
|
6914
|
+
search,
|
|
6915
|
+
modelSelection
|
|
6518
6916
|
});
|
|
6519
6917
|
/**
|
|
6520
6918
|
* Connection registers a channel through `owner.webServer`, read on its own
|
|
@@ -6531,9 +6929,9 @@ function apply(ctx, config) {
|
|
|
6531
6929
|
ctx.effect(() => toolsCtx.tools.register(createClaudeCodeTool({
|
|
6532
6930
|
runner: claudeRunner,
|
|
6533
6931
|
modeCeiling: cliModeCeiling,
|
|
6534
|
-
planActive: (session) =>
|
|
6932
|
+
planActive: (session, agent) => planModeForToolExecution(ctx, session, agent)
|
|
6535
6933
|
})), "packycode-api: claude_code tool");
|
|
6536
6934
|
});
|
|
6537
6935
|
}
|
|
6538
6936
|
//#endregion
|
|
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 };
|
|
6937
|
+
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, createModelSelectionController, 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, readHiddenModels, readSearchChoice, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, statusHostOrder, summarizeToolUse };
|