@cydm/pie 1.0.48 → 1.0.50
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/README.md +1 -1
- package/dist/builtin/extensions/ask-user/index.js +2 -9
- package/dist/builtin/extensions/ask-user/package.json +0 -7
- package/dist/builtin/extensions/changelog/index.js +15 -20
- package/dist/builtin/extensions/document-attachments/index.js +0 -1
- package/dist/builtin/extensions/document-attachments/package.json +1 -1
- package/dist/builtin/extensions/init/index.js +0 -13
- package/dist/builtin/extensions/plan-mode/index.js +29 -17
- package/dist/builtin/extensions/subagent/index.js +15 -31
- package/dist/builtin/extensions/todo/index.js +4 -8
- package/dist/builtin/skills/skill-creator/SKILL.md +15 -43
- package/dist/builtin/skills/skill-creator/eval-viewer/generate_review.mjs +1 -1
- package/dist/builtin/skills/skill-creator/eval-viewer/viewer.html +2 -2
- package/dist/builtin/skills/skill-creator/references/schemas.md +1 -1
- package/dist/builtin/skills/skill-creator/scripts/generate_report.mjs +1 -1
- package/dist/builtin/skills/skill-creator/scripts/improve_description.mjs +3 -3
- package/dist/builtin/skills/skill-creator/scripts/pie_runner.mjs +32 -4
- package/dist/builtin/skills/skill-creator/scripts/run_loop.mjs +1 -1
- package/dist/chunks/{chunk-IJMP3JTX.js → chunk-DZS34ACX.js} +230 -46
- package/dist/chunks/{chunk-U4B6RJ6Z.js → chunk-XBWFDIAU.js} +1 -1
- package/dist/cli.js +127 -80
- package/package.json +3 -3
- package/dist/builtin/extensions/deploy/index.js +0 -11
- package/dist/builtin/extensions/deploy/package.json +0 -11
- package/dist/builtin/extensions/files/index.js +0 -10
- package/dist/builtin/extensions/files/package.json +0 -14
- package/dist/builtin/extensions/kimi-attachments/index.js +0 -47
- package/dist/builtin/extensions/kimi-attachments/package.json +0 -11
- package/dist/builtin/skills/skill-creator/scripts/claude_cli.mjs +0 -115
|
@@ -545,7 +545,7 @@
|
|
|
545
545
|
<div class="header">
|
|
546
546
|
<div>
|
|
547
547
|
<h1>Eval Review: <span id="skill-name"></span></h1>
|
|
548
|
-
<div class="instructions">Review each output and leave feedback below. Navigate with arrow keys or buttons. When done, copy feedback and paste into
|
|
548
|
+
<div class="instructions">Review each output and leave feedback below. Navigate with arrow keys or buttons. When done, copy feedback and paste it back into your Pie session.</div>
|
|
549
549
|
</div>
|
|
550
550
|
<div class="progress" id="progress"></div>
|
|
551
551
|
</div>
|
|
@@ -634,7 +634,7 @@
|
|
|
634
634
|
<div class="done-overlay" id="done-overlay">
|
|
635
635
|
<div class="done-card">
|
|
636
636
|
<h2>Review Complete</h2>
|
|
637
|
-
<p>Your feedback has been saved. Go back to your
|
|
637
|
+
<p>Your feedback has been saved. Go back to your Pie session and say you're done reviewing.</p>
|
|
638
638
|
<div class="btn-row">
|
|
639
639
|
<button onclick="closeDoneDialog()">OK</button>
|
|
640
640
|
</div>
|
|
@@ -225,7 +225,7 @@ Output from Benchmark mode. Located at `benchmarks/<timestamp>/benchmark.json`.
|
|
|
225
225
|
"metadata": {
|
|
226
226
|
"skill_name": "pdf",
|
|
227
227
|
"skill_path": "/path/to/pdf",
|
|
228
|
-
"executor_model": "
|
|
228
|
+
"executor_model": "pie-agent",
|
|
229
229
|
"analyzer_model": "most-capable-model",
|
|
230
230
|
"timestamp": "2026-01-15T10:30:00Z",
|
|
231
231
|
"evals_run": [1, 2, 3],
|
|
@@ -75,7 +75,7 @@ ${refreshTag} <title>${titlePrefix}Skill Description Optimization</title>
|
|
|
75
75
|
<body>
|
|
76
76
|
<h1>${titlePrefix}Skill Description Optimization</h1>
|
|
77
77
|
<div class="explainer">
|
|
78
|
-
<strong>Optimizing your skill's description.</strong> This page updates automatically as
|
|
78
|
+
<strong>Optimizing your skill's description.</strong> This page updates automatically as the model tests different versions of your skill's description. Each row is an iteration — a new description attempt. The columns show test queries: green checkmarks mean the skill triggered correctly (or correctly didn't trigger), red crosses mean it got it wrong.
|
|
79
79
|
</div>
|
|
80
80
|
<div class="summary">
|
|
81
81
|
<p><strong>Original:</strong> ${escapeHtml(data?.original_description ?? "N/A")}</p>
|
|
@@ -25,9 +25,9 @@ export async function improveDescription({
|
|
|
25
25
|
? `Train: ${trainScore}, Test: ${testResults.summary.passed}/${testResults.summary.total}`
|
|
26
26
|
: `Train: ${trainScore}`;
|
|
27
27
|
|
|
28
|
-
let prompt = `You are optimizing a skill description for a
|
|
28
|
+
let prompt = `You are optimizing a skill description for a Pie skill called "${skillName}". A "skill" is sort of like a prompt, but with progressive disclosure -- there's a title and description that the agent sees when deciding whether to use the skill, and then if it does use the skill, it reads the .md file which has lots more details and potentially links to other resources in the skill folder like helper files and scripts and additional documentation or examples.
|
|
29
29
|
|
|
30
|
-
The description appears in
|
|
30
|
+
The description appears in the agent's "available_skills" list. When a user sends a query, the agent decides whether to invoke the skill based solely on the title and on this description. Your goal is to write a description that triggers for relevant queries, and doesn't trigger for irrelevant ones.
|
|
31
31
|
|
|
32
32
|
Here's the current description:
|
|
33
33
|
<current_description>
|
|
@@ -93,7 +93,7 @@ Concretely, your description should not be more than about 100-200 words, even i
|
|
|
93
93
|
Here are some tips that we've found to work well in writing these descriptions:
|
|
94
94
|
- The skill should be phrased in the imperative -- "Use this skill for" rather than "this skill does"
|
|
95
95
|
- The skill description should focus on the user's intent, what they are trying to achieve, vs. the implementation details of how the skill works.
|
|
96
|
-
- The description competes with other skills for
|
|
96
|
+
- The description competes with other skills for the model's attention — make it distinctive and immediately recognizable.
|
|
97
97
|
- If you're getting lots of failures after repeated attempts, change things up. Try different sentence structures or wordings.
|
|
98
98
|
|
|
99
99
|
I'd encourage you to be creative and mix up the style in different iterations since you'll have multiple opportunities to try different approaches and we'll just grab the highest-scoring one at the end.
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
8
9
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const REPO_ROOT = path.resolve(SCRIPT_DIR, "../../../../../../");
|
|
10
10
|
|
|
11
11
|
export function findProjectRoot(start = process.cwd()) {
|
|
12
12
|
return path.resolve(start);
|
|
@@ -29,13 +29,41 @@ export function openInBrowser(target) {
|
|
|
29
29
|
spawn("xdg-open", [target], { detached: true, stdio: "ignore" }).unref();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
let cachedCliPath;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Locate the Pie CLI entry point across layouts:
|
|
36
|
+
* - installed package: <pkg>/dist/builtin/skills/skill-creator/scripts -> <pkg>/dist/cli.js
|
|
37
|
+
* - dev repository: <repo>/products/cli/builtin/skills/skill-creator/scripts -> <repo>/products/cli/dist/cli.js
|
|
38
|
+
* - explicit override via PIE_CLI_PATH
|
|
39
|
+
*
|
|
40
|
+
* Candidate order is layout-aware: an installed build always prefers the
|
|
41
|
+
* package root and a dev checkout always prefers products/cli, so an
|
|
42
|
+
* unrelated sibling artifact cannot win over the correct entry.
|
|
43
|
+
*/
|
|
44
|
+
export function getPieCliPath() {
|
|
45
|
+
if (cachedCliPath) return cachedCliPath;
|
|
46
|
+
const installed = path.resolve(SCRIPT_DIR, "../../../../../dist/cli.js"); // installed package root
|
|
47
|
+
const dev = path.resolve(SCRIPT_DIR, "../../../../dist/cli.js"); // dev repo: products/cli
|
|
48
|
+
const inInstalledLayout = SCRIPT_DIR.includes(`${path.sep}dist${path.sep}builtin${path.sep}`);
|
|
49
|
+
const candidates = [
|
|
50
|
+
process.env.PIE_CLI_PATH,
|
|
51
|
+
...(inInstalledLayout ? [installed, dev] : [dev, installed]),
|
|
52
|
+
].filter((candidate) => typeof candidate === "string" && candidate.length > 0);
|
|
53
|
+
for (const candidate of candidates) {
|
|
54
|
+
if (fs.existsSync(candidate)) {
|
|
55
|
+
cachedCliPath = candidate;
|
|
56
|
+
return cachedCliPath;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Pie CLI entry not found. Tried: ${candidates.join(", ")}. Set PIE_CLI_PATH to the cli.js entry point.`,
|
|
61
|
+
);
|
|
34
62
|
}
|
|
35
63
|
|
|
36
64
|
export async function runPieJsonPrompt(prompt, { cwd = process.cwd(), timeout = 300_000, sessionId = null } = {}) {
|
|
37
65
|
return new Promise((resolve, reject) => {
|
|
38
|
-
const args = [getPieCliPath(
|
|
66
|
+
const args = [getPieCliPath(), prompt, "--json-output"];
|
|
39
67
|
if (sessionId) {
|
|
40
68
|
args.push("--session-id", sessionId);
|
|
41
69
|
}
|
|
@@ -6,7 +6,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { generateHtml } from "./generate_report.mjs";
|
|
8
8
|
import { improveDescription } from "./improve_description.mjs";
|
|
9
|
-
import { findProjectRoot, openInBrowser } from "./
|
|
9
|
+
import { findProjectRoot, openInBrowser } from "./pie_runner.mjs";
|
|
10
10
|
import { runEval } from "./run_eval.mjs";
|
|
11
11
|
import { parseSkillFile, resolveSkillPath } from "./skill_metadata.mjs";
|
|
12
12
|
|
|
@@ -848,6 +848,10 @@ var BUILTIN_TOOL_CAPABILITY_METADATA = {
|
|
|
848
848
|
spawn_subagents_parallel: { riskClass: "read_only", concurrencySafe: false, permissionScope: "host", readsHostResource: true, availableInPlanMode: true, maxOutputChars: 1e5 },
|
|
849
849
|
ask_user_multi: { riskClass: "user_interaction", concurrencySafe: false, permissionScope: "user", asksUser: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
|
|
850
850
|
manage_todo_list: { riskClass: "session_mutation", concurrencySafe: false, permissionScope: "session", mutatesSession: true, availableInPlanMode: true },
|
|
851
|
+
// plan_scratch ships as the plan-mode extension's session-local draft tool;
|
|
852
|
+
// registration feeds /permissions display and the legacy plan-mode name list,
|
|
853
|
+
// while runtime decisions still honor the tool's inline risk metadata first.
|
|
854
|
+
plan_scratch: { riskClass: "session_mutation", concurrencySafe: false, permissionScope: "session", mutatesSession: true, availableInPlanMode: true },
|
|
851
855
|
unity_project_inspect: { riskClass: "read_only", concurrencySafe: true, permissionScope: "unity", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
|
|
852
856
|
unity_scene_query: { riskClass: "read_only", concurrencySafe: true, permissionScope: "unity", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
|
|
853
857
|
unity_scene_object_inspect: { riskClass: "read_only", concurrencySafe: true, permissionScope: "unity", readsHostResource: true, availableInPlanMode: true, allowedForSubagentByDefault: true },
|
|
@@ -1409,8 +1413,9 @@ function buildSubagentPlan(deps, task, name, priority, timeoutSeconds) {
|
|
|
1409
1413
|
const complexity = inferComplexity(task, priority);
|
|
1410
1414
|
const modelClass = selectModelClass(complexity, priority);
|
|
1411
1415
|
const modelResolution = deps.resolveModelClass?.(modelClass);
|
|
1412
|
-
const
|
|
1413
|
-
const
|
|
1416
|
+
const resolvedPair = modelResolution?.model && modelResolution?.apiKey ? { model: modelResolution.model, apiKey: modelResolution.apiKey } : void 0;
|
|
1417
|
+
const model = resolvedPair?.model ?? deps.model;
|
|
1418
|
+
const apiKey = resolvedPair?.apiKey ?? deps.apiKey;
|
|
1414
1419
|
const partialPlan = {
|
|
1415
1420
|
task,
|
|
1416
1421
|
name,
|
|
@@ -1508,6 +1513,9 @@ async function runSubagentWithProgress(deps, task, reportProgress, signal) {
|
|
|
1508
1513
|
if (!apiKey) {
|
|
1509
1514
|
throw new Error("No API key available for subagent execution.");
|
|
1510
1515
|
}
|
|
1516
|
+
if (!model) {
|
|
1517
|
+
throw new Error("No model configured for subagent execution.");
|
|
1518
|
+
}
|
|
1511
1519
|
if (signal?.aborted) {
|
|
1512
1520
|
throw new Error("Subagent aborted by parent");
|
|
1513
1521
|
}
|
|
@@ -1845,6 +1853,12 @@ var ManageTodoListParamsSchema = Type.Object(
|
|
|
1845
1853
|
},
|
|
1846
1854
|
{ additionalProperties: false }
|
|
1847
1855
|
);
|
|
1856
|
+
var MANAGE_TODO_LIST_TOOL_DESCRIPTION = `Manage a session-local todo list. Use when the user explicitly asks to use the todo tool, create/manage todos, track tasks, follow a todo list, show tasks, or mark a task done.
|
|
1857
|
+
NEVER use for: internal planning, tracking your own progress, remembering subtasks, or creating checklists for yourself.
|
|
1858
|
+
Use action=create with items to create a list; provide todo titles and optional descriptions only. The tool assigns ids and starts item 1 automatically.
|
|
1859
|
+
Use action=complete_current after finishing the current item; the tool advances to the next item or clears the list after the final item. Do not call complete_current after a required install, build, test, or verification command just failed; fix/retry first or clear with a reason.
|
|
1860
|
+
When a todo list exists, keep executing from the current in-progress item until it is cleared. If the work cannot continue, explain the failure and use action=clear with a reason.
|
|
1861
|
+
Actions: create, read, complete_current, clear.`;
|
|
1848
1862
|
function cloneTodos(todos) {
|
|
1849
1863
|
return todos.map((todo) => ({ ...todo }));
|
|
1850
1864
|
}
|
|
@@ -7869,6 +7883,193 @@ function fuzzyFindText(content, oldText) {
|
|
|
7869
7883
|
function stripBom(content) {
|
|
7870
7884
|
return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
|
|
7871
7885
|
}
|
|
7886
|
+
function findEditMatch(content, oldText, newText) {
|
|
7887
|
+
const direct = fuzzyFindText(content, oldText);
|
|
7888
|
+
if (direct.found) {
|
|
7889
|
+
return { found: true, occurrences: direct.occurrences, matches: direct.matches, matchKind: direct.matchKind };
|
|
7890
|
+
}
|
|
7891
|
+
return findWhitespaceRelaxedMatch(content, oldText, newText);
|
|
7892
|
+
}
|
|
7893
|
+
function leadingWhitespace(line) {
|
|
7894
|
+
const match2 = line.match(/^[ \t]*/);
|
|
7895
|
+
return match2 ? match2[0] : "";
|
|
7896
|
+
}
|
|
7897
|
+
function findRelaxedWindows(fileLines, qLines) {
|
|
7898
|
+
const firstNonBlank = qLines.findIndex((line) => line.trim() !== "");
|
|
7899
|
+
if (firstNonBlank === -1) return [];
|
|
7900
|
+
const anchor = qLines[firstNonBlank].trim();
|
|
7901
|
+
if (!anchor) return [];
|
|
7902
|
+
const windows = [];
|
|
7903
|
+
for (let anchorFileLine = 0; anchorFileLine < fileLines.length; anchorFileLine++) {
|
|
7904
|
+
if (fileLines[anchorFileLine].trim() !== anchor) continue;
|
|
7905
|
+
const startLine = anchorFileLine - firstNonBlank;
|
|
7906
|
+
if (startLine < 0) continue;
|
|
7907
|
+
let fileLine = startLine;
|
|
7908
|
+
let ok = true;
|
|
7909
|
+
let lastPaired = -1;
|
|
7910
|
+
const qLineToFileLine = /* @__PURE__ */ new Map();
|
|
7911
|
+
for (let qLine = 0; qLine < qLines.length; qLine++) {
|
|
7912
|
+
if (fileLine >= fileLines.length) {
|
|
7913
|
+
ok = false;
|
|
7914
|
+
break;
|
|
7915
|
+
}
|
|
7916
|
+
const q = qLines[qLine];
|
|
7917
|
+
const f = fileLines[fileLine];
|
|
7918
|
+
if (q.trim() === "") {
|
|
7919
|
+
if (f.trim() !== "") {
|
|
7920
|
+
ok = false;
|
|
7921
|
+
break;
|
|
7922
|
+
}
|
|
7923
|
+
} else {
|
|
7924
|
+
if (f.trim() !== q.trim()) {
|
|
7925
|
+
ok = false;
|
|
7926
|
+
break;
|
|
7927
|
+
}
|
|
7928
|
+
qLineToFileLine.set(qLine, fileLine);
|
|
7929
|
+
lastPaired = fileLine;
|
|
7930
|
+
}
|
|
7931
|
+
fileLine++;
|
|
7932
|
+
}
|
|
7933
|
+
if (ok && lastPaired !== -1) windows.push({ startLine, endLine: lastPaired, qLineToFileLine });
|
|
7934
|
+
}
|
|
7935
|
+
return windows;
|
|
7936
|
+
}
|
|
7937
|
+
function lineSpanToCharSpan(fileLines, startLine, endLine) {
|
|
7938
|
+
let index = 0;
|
|
7939
|
+
for (let i = 0; i < startLine; i++) index += fileLines[i].length + 1;
|
|
7940
|
+
let matchLength = 0;
|
|
7941
|
+
for (let i = startLine; i <= endLine; i++) matchLength += fileLines[i].length + (i < endLine ? 1 : 0);
|
|
7942
|
+
return { index, matchLength };
|
|
7943
|
+
}
|
|
7944
|
+
function reindentNewText(newText, qLines, fileLines, window) {
|
|
7945
|
+
const newLines = newText.split("\n");
|
|
7946
|
+
const qIndent = (qLine) => {
|
|
7947
|
+
const fileLine = window.qLineToFileLine.get(qLine);
|
|
7948
|
+
return fileLine === void 0 ? "" : leadingWhitespace(fileLines[fileLine]);
|
|
7949
|
+
};
|
|
7950
|
+
const qIdx = [];
|
|
7951
|
+
for (let i = 0; i < qLines.length; i++) if (qLines[i].trim() !== "") qIdx.push(i);
|
|
7952
|
+
const nIdx = [];
|
|
7953
|
+
const nPosOfLine = /* @__PURE__ */ new Map();
|
|
7954
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
7955
|
+
if (newLines[i].trim() === "") continue;
|
|
7956
|
+
nPosOfLine.set(i, nIdx.length);
|
|
7957
|
+
nIdx.push(i);
|
|
7958
|
+
}
|
|
7959
|
+
if (nIdx.length === 0) return newText;
|
|
7960
|
+
const counterpart = /* @__PURE__ */ new Map();
|
|
7961
|
+
let p = 0;
|
|
7962
|
+
while (p < qIdx.length && p < nIdx.length && qLines[qIdx[p]].trim() === newLines[nIdx[p]].trim()) {
|
|
7963
|
+
counterpart.set(p, p);
|
|
7964
|
+
p++;
|
|
7965
|
+
}
|
|
7966
|
+
let s = 0;
|
|
7967
|
+
while (s < qIdx.length - p && s < nIdx.length - p && qLines[qIdx[qIdx.length - 1 - s]].trim() === newLines[nIdx[nIdx.length - 1 - s]].trim()) {
|
|
7968
|
+
counterpart.set(nIdx.length - 1 - s, qIdx.length - 1 - s);
|
|
7969
|
+
s++;
|
|
7970
|
+
}
|
|
7971
|
+
const midQ = [];
|
|
7972
|
+
for (let pos = p; pos < qIdx.length - s; pos++) midQ.push(pos);
|
|
7973
|
+
const midN = [];
|
|
7974
|
+
for (let pos = p; pos < nIdx.length - s; pos++) midN.push(pos);
|
|
7975
|
+
const pairRegion = (qLo, qHi, nLo, nHi) => {
|
|
7976
|
+
const qCount = qHi - qLo;
|
|
7977
|
+
const nCount = nHi - nLo;
|
|
7978
|
+
const paired = Math.min(qCount, nCount);
|
|
7979
|
+
for (let k = 0; k < paired; k++) counterpart.set(nLo + k, qLo + k);
|
|
7980
|
+
const fallbackQ = qCount > 0 ? qHi - 1 : Math.max(qLo - 1, 0);
|
|
7981
|
+
for (let k = paired; k < nCount; k++) counterpart.set(nLo + k, fallbackQ);
|
|
7982
|
+
};
|
|
7983
|
+
const qTextCounts = /* @__PURE__ */ new Map();
|
|
7984
|
+
for (const qPos of midQ) {
|
|
7985
|
+
const t = qLines[qIdx[qPos]].trim();
|
|
7986
|
+
qTextCounts.set(t, (qTextCounts.get(t) ?? 0) + 1);
|
|
7987
|
+
}
|
|
7988
|
+
const qPosByText = /* @__PURE__ */ new Map();
|
|
7989
|
+
for (const qPos of midQ) {
|
|
7990
|
+
const t = qLines[qIdx[qPos]].trim();
|
|
7991
|
+
if (qTextCounts.get(t) === 1) qPosByText.set(t, qPos);
|
|
7992
|
+
}
|
|
7993
|
+
const nTextCounts = /* @__PURE__ */ new Map();
|
|
7994
|
+
for (const nPos of midN) {
|
|
7995
|
+
const t = newLines[nIdx[nPos]].trim();
|
|
7996
|
+
nTextCounts.set(t, (nTextCounts.get(t) ?? 0) + 1);
|
|
7997
|
+
}
|
|
7998
|
+
let cursorQ = p;
|
|
7999
|
+
let cursorN = p;
|
|
8000
|
+
for (const nPos of midN) {
|
|
8001
|
+
const t = newLines[nIdx[nPos]].trim();
|
|
8002
|
+
const qPos = qPosByText.get(t);
|
|
8003
|
+
if (nTextCounts.get(t) !== 1 || qPos === void 0 || qPos < cursorQ) continue;
|
|
8004
|
+
pairRegion(cursorQ, qPos, cursorN, nPos);
|
|
8005
|
+
counterpart.set(nPos, qPos);
|
|
8006
|
+
cursorQ = qPos + 1;
|
|
8007
|
+
cursorN = nPos + 1;
|
|
8008
|
+
}
|
|
8009
|
+
pairRegion(cursorQ, qIdx.length - s, cursorN, nIdx.length - s);
|
|
8010
|
+
const result = [];
|
|
8011
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
8012
|
+
if (newLines[i].trim() === "") {
|
|
8013
|
+
result.push("");
|
|
8014
|
+
continue;
|
|
8015
|
+
}
|
|
8016
|
+
const nPos = nPosOfLine.get(i) ?? 0;
|
|
8017
|
+
const qPos = counterpart.get(nPos) ?? 0;
|
|
8018
|
+
result.push(qIndent(qIdx[qPos] ?? qIdx[0]) + newLines[i].trim());
|
|
8019
|
+
}
|
|
8020
|
+
return result.join("\n");
|
|
8021
|
+
}
|
|
8022
|
+
function findWhitespaceRelaxedMatch(content, oldText, newText) {
|
|
8023
|
+
const fileLines = content.split("\n");
|
|
8024
|
+
const qLines = oldText.split("\n");
|
|
8025
|
+
const windows = findRelaxedWindows(fileLines, qLines);
|
|
8026
|
+
if (windows.length === 0) {
|
|
8027
|
+
return { found: false, occurrences: 0, matches: [], matchKind: "none" };
|
|
8028
|
+
}
|
|
8029
|
+
const matches = windows.map((window) => {
|
|
8030
|
+
const span = lineSpanToCharSpan(fileLines, window.startLine, window.endLine);
|
|
8031
|
+
return { ...span, replacementText: reindentNewText(newText, qLines, fileLines, window) };
|
|
8032
|
+
});
|
|
8033
|
+
return { found: true, occurrences: matches.length, matches, matchKind: "whitespace-relaxed" };
|
|
8034
|
+
}
|
|
8035
|
+
function describeClosestRegion(content, oldText) {
|
|
8036
|
+
const qLines = oldText.split("\n");
|
|
8037
|
+
const firstNonBlank = qLines.findIndex((line) => line.trim() !== "");
|
|
8038
|
+
if (firstNonBlank === -1) return void 0;
|
|
8039
|
+
const anchor = qLines[firstNonBlank].trim();
|
|
8040
|
+
const fileLines = content.split("\n");
|
|
8041
|
+
const clip = (text) => text.length > 90 ? `${text.slice(0, 90)}\u2026` : text;
|
|
8042
|
+
let best;
|
|
8043
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
8044
|
+
if (fileLines[i].trim() !== anchor) continue;
|
|
8045
|
+
let matched = 0;
|
|
8046
|
+
let fi = i;
|
|
8047
|
+
let firstDiff;
|
|
8048
|
+
for (let qi = firstNonBlank; qi < qLines.length; qi++) {
|
|
8049
|
+
const q = qLines[qi].trim();
|
|
8050
|
+
if (!q) continue;
|
|
8051
|
+
while (fi < fileLines.length && !fileLines[fi].trim()) fi++;
|
|
8052
|
+
if (fi >= fileLines.length || fileLines[fi].trim() !== q) {
|
|
8053
|
+
firstDiff = { fileNo: fi + 1, fileLine: fileLines[fi] ?? "<end of file>", qLine: qLines[qi] };
|
|
8054
|
+
break;
|
|
8055
|
+
}
|
|
8056
|
+
matched++;
|
|
8057
|
+
fi++;
|
|
8058
|
+
}
|
|
8059
|
+
if (!best || matched > best.matched) best = { start: i, matched, firstDiff };
|
|
8060
|
+
}
|
|
8061
|
+
const total = qLines.filter((line) => line.trim() !== "").length;
|
|
8062
|
+
if (!best) {
|
|
8063
|
+
return `No line matching ${JSON.stringify(clip(anchor))} exists in this file \u2014 the text may live in a different file, or the file changed since it was read.`;
|
|
8064
|
+
}
|
|
8065
|
+
const parts = [`Closest match: file lines ${best.start + 1}\u2013${best.start + total} (${best.matched}/${total} lines matched by trimmed content).`];
|
|
8066
|
+
if (best.firstDiff) {
|
|
8067
|
+
parts.push(`First difference at line ${best.firstDiff.fileNo}: file has ${JSON.stringify(clip(best.firstDiff.fileLine.trim()))}, edit expected ${JSON.stringify(clip(best.firstDiff.qLine.trim()))}.`);
|
|
8068
|
+
} else {
|
|
8069
|
+
parts.push("All non-empty lines match by trimmed content \u2014 the mismatch is leading whitespace or blank-line placement.");
|
|
8070
|
+
}
|
|
8071
|
+
return parts.join(" ");
|
|
8072
|
+
}
|
|
7872
8073
|
function generateDiffString(oldContent, newContent, contextLines = 4) {
|
|
7873
8074
|
const oldLines = normalizeToLF(stripBom(oldContent).text).split("\n");
|
|
7874
8075
|
const newLines = normalizeToLF(stripBom(newContent).text).split("\n");
|
|
@@ -7982,7 +8183,8 @@ function createEditErrorResult(params) {
|
|
|
7982
8183
|
const detailLines = [
|
|
7983
8184
|
params.message,
|
|
7984
8185
|
params.recoveryHint,
|
|
7985
|
-
params.alreadyAppliedCandidate ? "Diagnostic: newText already appears in the current file, but edit_file did not treat that as success because it may be an unrelated occurrence." : ""
|
|
8186
|
+
params.alreadyAppliedCandidate ? "Diagnostic: newText already appears in the current file, but edit_file did not treat that as success because it may be an unrelated occurrence." : "",
|
|
8187
|
+
params.closestMatch ? `Diagnostic: ${params.closestMatch}` : ""
|
|
7986
8188
|
].filter(Boolean);
|
|
7987
8189
|
return {
|
|
7988
8190
|
content: [{ type: "text", text: detailLines.join("\n") }],
|
|
@@ -8034,7 +8236,7 @@ function createEditTool(cwd, options) {
|
|
|
8034
8236
|
newText: Type.String({ description: "Replacement text", maxLength: MAX_EDIT_INPUT_CHARS }),
|
|
8035
8237
|
replaceAll: Type.Optional(Type.Boolean({ description: "Replace every occurrence" }))
|
|
8036
8238
|
}), {
|
|
8037
|
-
description: "
|
|
8239
|
+
description: "Replacements applied in order: each edit matches the file as left by the previous edit, so later edits can build on earlier ones. Whole-line indentation drift is tolerated \u2014 matches locate by trimmed content and the replacement adopts the file's indentation.",
|
|
8038
8240
|
minItems: 1,
|
|
8039
8241
|
maxItems: MAX_EDIT_BLOCKS
|
|
8040
8242
|
})
|
|
@@ -8043,7 +8245,7 @@ function createEditTool(cwd, options) {
|
|
|
8043
8245
|
return {
|
|
8044
8246
|
name: "edit",
|
|
8045
8247
|
label: "edit",
|
|
8046
|
-
description: `Make precise exact-text replacements in one file. For multiple changes in the same file, put all
|
|
8248
|
+
description: `Make precise exact-text replacements in one file. For multiple changes in the same file, put all replacements in one edits[] call \u2014 they apply in order, so later edits can target what earlier ones just changed. ${pathResolutionDescription}`,
|
|
8047
8249
|
parameters: editSchema,
|
|
8048
8250
|
execute: async (_toolCallId, params, signal) => {
|
|
8049
8251
|
const { path: path4, root } = params;
|
|
@@ -8087,7 +8289,9 @@ function createEditTool(cwd, options) {
|
|
|
8087
8289
|
const { bom, text: content } = stripBom(rawContent);
|
|
8088
8290
|
const originalEnding = detectLineEnding(content);
|
|
8089
8291
|
const normalizedContent = normalizeToLF(content);
|
|
8090
|
-
|
|
8292
|
+
let workingContent = normalizedContent;
|
|
8293
|
+
const plannedKinds = /* @__PURE__ */ new Set();
|
|
8294
|
+
let replacedOccurrences = 0;
|
|
8091
8295
|
let totalOldOccurrences = 0;
|
|
8092
8296
|
let totalNewOccurrences = 0;
|
|
8093
8297
|
let anyReplaceAll = false;
|
|
@@ -8095,8 +8299,8 @@ function createEditTool(cwd, options) {
|
|
|
8095
8299
|
throwIfAborted();
|
|
8096
8300
|
const normalizedOldText = normalizeToLF(replacement.oldText);
|
|
8097
8301
|
const normalizedNewText = normalizeToLF(replacement.newText);
|
|
8098
|
-
const newTextOccurrences = countOccurrences(
|
|
8099
|
-
const matchResult =
|
|
8302
|
+
const newTextOccurrences = countOccurrences(workingContent, normalizedNewText);
|
|
8303
|
+
const matchResult = findEditMatch(workingContent, normalizedOldText, normalizedNewText);
|
|
8100
8304
|
totalOldOccurrences += matchResult.occurrences;
|
|
8101
8305
|
totalNewOccurrences += newTextOccurrences;
|
|
8102
8306
|
anyReplaceAll ||= replacement.replaceAll;
|
|
@@ -8111,7 +8315,8 @@ function createEditTool(cwd, options) {
|
|
|
8111
8315
|
replaceAll: replacement.replaceAll,
|
|
8112
8316
|
staleBaseLikely: newTextOccurrences > 0,
|
|
8113
8317
|
alreadyAppliedCandidate: newTextOccurrences > 0,
|
|
8114
|
-
recoveryHint: newTextOccurrences > 0 ? "The replacement may already be present. Read the current file before retrying." : "Read the latest file and retry with exact text."
|
|
8318
|
+
recoveryHint: newTextOccurrences > 0 ? "The replacement may already be present. Read the current file before retrying." : "Read the latest file and retry with exact text.",
|
|
8319
|
+
closestMatch: describeClosestRegion(workingContent, normalizedOldText)
|
|
8115
8320
|
}));
|
|
8116
8321
|
return;
|
|
8117
8322
|
}
|
|
@@ -8128,9 +8333,9 @@ function createEditTool(cwd, options) {
|
|
|
8128
8333
|
}));
|
|
8129
8334
|
return;
|
|
8130
8335
|
}
|
|
8131
|
-
const
|
|
8132
|
-
const changingMatches =
|
|
8133
|
-
(match2) =>
|
|
8336
|
+
const selected = replacement.replaceAll ? matchResult.matches : matchResult.matches.slice(0, 1);
|
|
8337
|
+
const changingMatches = selected.filter(
|
|
8338
|
+
(match2) => workingContent.slice(match2.index, match2.index + match2.matchLength) !== (match2.replacementText ?? normalizedNewText)
|
|
8134
8339
|
);
|
|
8135
8340
|
if (changingMatches.length === 0) {
|
|
8136
8341
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
@@ -8146,42 +8351,20 @@ function createEditTool(cwd, options) {
|
|
|
8146
8351
|
}));
|
|
8147
8352
|
return;
|
|
8148
8353
|
}
|
|
8149
|
-
for (const match2 of changingMatches) {
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
for (let index = 1; index < ordered.length; index++) {
|
|
8155
|
-
throwIfAborted();
|
|
8156
|
-
const previous = ordered[index - 1];
|
|
8157
|
-
const current = ordered[index];
|
|
8158
|
-
if (current.index < previous.index + previous.matchLength) {
|
|
8159
|
-
if (signal) signal.removeEventListener("abort", onAbort);
|
|
8160
|
-
resolve3(createEditErrorResult({
|
|
8161
|
-
message: `Edits overlap in ${path4}; no changes were made.`,
|
|
8162
|
-
matchKind: "mixed",
|
|
8163
|
-
errorReason: "overlapping_edits",
|
|
8164
|
-
oldTextOccurrences: totalOldOccurrences,
|
|
8165
|
-
newTextOccurrences: totalNewOccurrences,
|
|
8166
|
-
replaceAll: anyReplaceAll,
|
|
8167
|
-
recoveryHint: "Merge overlapping replacements into one exact edit."
|
|
8168
|
-
}));
|
|
8169
|
-
return;
|
|
8354
|
+
for (const match2 of [...changingMatches].sort((left, right) => right.index - left.index)) {
|
|
8355
|
+
throwIfAborted();
|
|
8356
|
+
workingContent = workingContent.substring(0, match2.index) + (match2.replacementText ?? normalizedNewText) + workingContent.substring(match2.index + match2.matchLength);
|
|
8357
|
+
replacedOccurrences++;
|
|
8358
|
+
if (matchResult.matchKind !== "none") plannedKinds.add(matchResult.matchKind);
|
|
8170
8359
|
}
|
|
8171
8360
|
}
|
|
8172
8361
|
throwIfAborted();
|
|
8173
|
-
|
|
8174
|
-
let newContent = baseContent;
|
|
8175
|
-
for (const match2 of [...planned].sort((left, right) => right.index - left.index)) {
|
|
8176
|
-
throwIfAborted();
|
|
8177
|
-
newContent = newContent.substring(0, match2.index) + match2.newText + newContent.substring(match2.index + match2.matchLength);
|
|
8178
|
-
}
|
|
8179
|
-
if (baseContent === newContent) {
|
|
8362
|
+
if (workingContent === normalizedContent) {
|
|
8180
8363
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
8181
8364
|
resolve3(
|
|
8182
8365
|
createEditErrorResult({
|
|
8183
8366
|
message: `No changes made to ${path4}. The replacement produced identical content.`,
|
|
8184
|
-
matchKind:
|
|
8367
|
+
matchKind: plannedKinds.size === 1 ? [...plannedKinds][0] : "mixed",
|
|
8185
8368
|
errorReason: "replacement_no_change",
|
|
8186
8369
|
oldTextOccurrences: totalOldOccurrences,
|
|
8187
8370
|
newTextOccurrences: totalNewOccurrences,
|
|
@@ -8192,6 +8375,7 @@ function createEditTool(cwd, options) {
|
|
|
8192
8375
|
);
|
|
8193
8376
|
return;
|
|
8194
8377
|
}
|
|
8378
|
+
const newContent = workingContent;
|
|
8195
8379
|
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
8196
8380
|
throwIfAborted();
|
|
8197
8381
|
await fs2.writeFile(absolutePath, finalContent, "utf-8");
|
|
@@ -8201,13 +8385,12 @@ function createEditTool(cwd, options) {
|
|
|
8201
8385
|
signal.removeEventListener("abort", onAbort);
|
|
8202
8386
|
}
|
|
8203
8387
|
const diffResult = generateDiffString(content, restoreLineEndings(newContent, originalEnding));
|
|
8204
|
-
const
|
|
8205
|
-
const matchKind = matchKinds.size === 1 ? planned[0]?.matchKind ?? "none" : "mixed";
|
|
8388
|
+
const matchKind = plannedKinds.size === 1 ? [...plannedKinds][0] : "mixed";
|
|
8206
8389
|
resolve3({
|
|
8207
8390
|
content: [
|
|
8208
8391
|
{
|
|
8209
8392
|
type: "text",
|
|
8210
|
-
text: `Changed ${path4}: ${replacements.length} edit block${replacements.length === 1 ? "" : "s"}, ${
|
|
8393
|
+
text: `Changed ${path4}: ${replacements.length} edit block${replacements.length === 1 ? "" : "s"}, ${replacedOccurrences} replacement${replacedOccurrences === 1 ? "" : "s"}. First changed line: ${diffResult.firstChangedLine}.`
|
|
8211
8394
|
},
|
|
8212
8395
|
{ type: "text", text: boundedDiff(diffResult.diff) }
|
|
8213
8396
|
],
|
|
@@ -8221,7 +8404,7 @@ function createEditTool(cwd, options) {
|
|
|
8221
8404
|
oldTextOccurrences: totalOldOccurrences,
|
|
8222
8405
|
newTextOccurrences: totalNewOccurrences,
|
|
8223
8406
|
replaceAll: anyReplaceAll,
|
|
8224
|
-
replacedOccurrences
|
|
8407
|
+
replacedOccurrences,
|
|
8225
8408
|
replacedBlocks: replacements.length
|
|
8226
8409
|
}
|
|
8227
8410
|
});
|
|
@@ -12363,7 +12546,7 @@ function sanitizeTitle(title) {
|
|
|
12363
12546
|
}
|
|
12364
12547
|
function isTextCompletionStepTitle(title) {
|
|
12365
12548
|
const normalized = sanitizeTitle(title).toLowerCase();
|
|
12366
|
-
return /反馈|总结|结论|汇报|说明|解释|收尾|验证|确认|位置|结果/u.test(
|
|
12549
|
+
return /反馈|总结|结论|汇报|说明|解释|收尾|验证|确认|位置|结果/u.test(normalized) || /\b(summarize|summary|report|explain|explanation|wrap[\s-]?up|verify|verification|confirm|result|results|location|findings)\b/i.test(normalized);
|
|
12367
12550
|
}
|
|
12368
12551
|
function maybeAdvanceTodoExecutionState(state, signal) {
|
|
12369
12552
|
if (state.mode !== "todo" || state.source === "user_todo" || state.lifecycle !== "active" || !state.currentStepId) {
|
|
@@ -14997,6 +15180,7 @@ export {
|
|
|
14997
15180
|
interpretShellExit,
|
|
14998
15181
|
createSubagentCapability,
|
|
14999
15182
|
ManageTodoListParamsSchema,
|
|
15183
|
+
MANAGE_TODO_LIST_TOOL_DESCRIPTION,
|
|
15000
15184
|
executeManageTodoList,
|
|
15001
15185
|
maybeAdvanceTodoExecutionState,
|
|
15002
15186
|
createAskUserCapability,
|