@bubblebrain-ai/bubble 0.0.4 → 0.0.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/dist/agent/budget-ledger.d.ts +20 -0
- package/dist/agent/budget-ledger.js +51 -0
- package/dist/agent/execution-governor.js +1 -1
- package/dist/agent/profiles.d.ts +59 -0
- package/dist/agent/profiles.js +460 -0
- package/dist/agent/subagent-control.d.ts +52 -0
- package/dist/agent/subagent-control.js +38 -0
- package/dist/agent.d.ts +60 -1
- package/dist/agent.js +602 -53
- package/dist/context/budget.js +1 -0
- package/dist/context/compact-llm.js +7 -6
- package/dist/context/compact.js +6 -6
- package/dist/context/projector.d.ts +3 -3
- package/dist/context/projector.js +32 -18
- package/dist/context/prune.d.ts +2 -2
- package/dist/context/prune.js +1 -4
- package/dist/main.js +12 -5
- package/dist/mcp/manager.js +1 -0
- package/dist/orchestrator/default-hooks.js +48 -9
- package/dist/orchestrator/hooks.d.ts +5 -0
- package/dist/prompt/compose.d.ts +1 -0
- package/dist/prompt/compose.js +8 -1
- package/dist/prompt/environment.js +21 -2
- package/dist/prompt/reminders.d.ts +3 -1
- package/dist/prompt/reminders.js +23 -4
- package/dist/prompt/runtime.d.ts +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-artifacts.d.ts +7 -0
- package/dist/provider-artifacts.js +60 -0
- package/dist/provider.d.ts +6 -7
- package/dist/provider.js +77 -15
- package/dist/session-log.js +3 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +6 -0
- package/dist/tools/agent-lifecycle.js +355 -0
- package/dist/tools/bash.js +2 -0
- package/dist/tools/edit-apply.d.ts +25 -0
- package/dist/tools/edit-apply.js +197 -0
- package/dist/tools/edit.js +63 -56
- package/dist/tools/exit-plan-mode.js +3 -1
- package/dist/tools/file-mutation-queue.d.ts +1 -0
- package/dist/tools/file-mutation-queue.js +32 -0
- package/dist/tools/glob.js +1 -0
- package/dist/tools/grep.js +1 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/lsp.js +2 -0
- package/dist/tools/memory.js +2 -0
- package/dist/tools/question.js +2 -0
- package/dist/tools/read.js +1 -0
- package/dist/tools/skill.js +1 -0
- package/dist/tools/task.js +1 -0
- package/dist/tools/todo.js +1 -0
- package/dist/tools/tool-search.js +2 -1
- package/dist/tools/web-fetch.js +1 -0
- package/dist/tools/web-search.js +1 -0
- package/dist/tools/write.js +2 -0
- package/dist/tui/display-history.d.ts +8 -1
- package/dist/tui/markdown-inline.d.ts +22 -0
- package/dist/tui/markdown-inline.js +68 -0
- package/dist/tui/render-signature.d.ts +1 -0
- package/dist/tui/render-signature.js +7 -0
- package/dist/tui/run.js +712 -267
- package/dist/tui/tool-renderers/fallback.d.ts +2 -0
- package/dist/tui/tool-renderers/fallback.js +75 -0
- package/dist/tui/tool-renderers/registry.d.ts +3 -0
- package/dist/tui/tool-renderers/registry.js +11 -0
- package/dist/tui/tool-renderers/subagent.d.ts +2 -0
- package/dist/tui/tool-renderers/subagent.js +114 -0
- package/dist/tui/tool-renderers/types.d.ts +36 -0
- package/dist/tui/tool-renderers/types.js +1 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
- package/dist/tui/tool-renderers/write-preview.js +22 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +82 -0
- package/dist/types.d.ts +90 -10
- package/package.json +1 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
export class EditApplyError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
constructor(message, status = "no_match") {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.name = "EditApplyError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function detectLineEnding(content) {
|
|
10
|
+
const crlf = content.indexOf("\r\n");
|
|
11
|
+
const lf = content.indexOf("\n");
|
|
12
|
+
return crlf !== -1 && crlf === lf - 1 ? "\r\n" : "\n";
|
|
13
|
+
}
|
|
14
|
+
function stripBom(content) {
|
|
15
|
+
return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
|
|
16
|
+
}
|
|
17
|
+
function normalizeToLF(text) {
|
|
18
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
19
|
+
}
|
|
20
|
+
function restoreLineEndings(text, lineEnding) {
|
|
21
|
+
return lineEnding === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
22
|
+
}
|
|
23
|
+
function normalizeLineForMatch(line) {
|
|
24
|
+
return line
|
|
25
|
+
.normalize("NFKC")
|
|
26
|
+
.trimEnd()
|
|
27
|
+
.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
|
|
28
|
+
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
|
|
29
|
+
.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-")
|
|
30
|
+
.replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
|
|
31
|
+
}
|
|
32
|
+
function findAllOccurrences(content, needle) {
|
|
33
|
+
const indexes = [];
|
|
34
|
+
if (needle.length === 0)
|
|
35
|
+
return indexes;
|
|
36
|
+
let index = content.indexOf(needle);
|
|
37
|
+
while (index !== -1) {
|
|
38
|
+
indexes.push(index);
|
|
39
|
+
index = content.indexOf(needle, index + needle.length);
|
|
40
|
+
}
|
|
41
|
+
return indexes;
|
|
42
|
+
}
|
|
43
|
+
function splitLines(content) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
let start = 0;
|
|
46
|
+
for (let i = 0; i < content.length; i++) {
|
|
47
|
+
if (content[i] === "\n") {
|
|
48
|
+
lines.push({ text: content.slice(start, i), start, endNoNewline: i });
|
|
49
|
+
start = i + 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
lines.push({ text: content.slice(start), start, endNoNewline: content.length });
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
|
55
|
+
function nonBlankLines(lines) {
|
|
56
|
+
const result = [];
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
const normalized = normalizeLineForMatch(lines[i].text);
|
|
59
|
+
if (normalized.trim().length > 0) {
|
|
60
|
+
result.push({ lineIndex: i, normalized });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function normalizedOldNonBlankLines(oldText) {
|
|
66
|
+
return splitLines(oldText)
|
|
67
|
+
.map((line) => normalizeLineForMatch(line.text))
|
|
68
|
+
.filter((line) => line.trim().length > 0);
|
|
69
|
+
}
|
|
70
|
+
function findNormalizedLineMatches(content, oldText) {
|
|
71
|
+
const contentLines = splitLines(content);
|
|
72
|
+
const searchable = nonBlankLines(contentLines);
|
|
73
|
+
const oldLines = normalizedOldNonBlankLines(oldText);
|
|
74
|
+
if (oldLines.length === 0)
|
|
75
|
+
return [];
|
|
76
|
+
const matches = [];
|
|
77
|
+
for (let i = 0; i <= searchable.length - oldLines.length; i++) {
|
|
78
|
+
let matched = true;
|
|
79
|
+
for (let j = 0; j < oldLines.length; j++) {
|
|
80
|
+
if (searchable[i + j].normalized !== oldLines[j]) {
|
|
81
|
+
matched = false;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (matched) {
|
|
86
|
+
const first = contentLines[searchable[i].lineIndex];
|
|
87
|
+
const last = contentLines[searchable[i + oldLines.length - 1].lineIndex];
|
|
88
|
+
matches.push({ start: first.start, end: last.endNoNewline });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return matches;
|
|
92
|
+
}
|
|
93
|
+
function summarizeOldText(oldText) {
|
|
94
|
+
const firstLine = normalizeToLF(oldText).split("\n").find((line) => line.trim().length > 0) ?? oldText;
|
|
95
|
+
return firstLine.length > 80 ? `${firstLine.slice(0, 80)}...` : firstLine;
|
|
96
|
+
}
|
|
97
|
+
function findBestLineHint(content, oldText) {
|
|
98
|
+
const oldLines = normalizedOldNonBlankLines(oldText);
|
|
99
|
+
if (oldLines.length === 0)
|
|
100
|
+
return undefined;
|
|
101
|
+
const contentLines = nonBlankLines(splitLines(content));
|
|
102
|
+
let best;
|
|
103
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
104
|
+
let score = 0;
|
|
105
|
+
for (let j = 0; j < oldLines.length && i + j < contentLines.length; j++) {
|
|
106
|
+
if (contentLines[i + j].normalized === oldLines[j])
|
|
107
|
+
score++;
|
|
108
|
+
}
|
|
109
|
+
if (!best || score > best.score)
|
|
110
|
+
best = { index: i, score };
|
|
111
|
+
}
|
|
112
|
+
if (!best || best.score === 0)
|
|
113
|
+
return undefined;
|
|
114
|
+
const startLine = contentLines[best.index].lineIndex + 1;
|
|
115
|
+
return `Closest line-based candidate starts near line ${startLine} and matched ${best.score}/${oldLines.length} non-blank lines.`;
|
|
116
|
+
}
|
|
117
|
+
function matchEdit(content, edit, index, total) {
|
|
118
|
+
if (edit.oldText.length === 0) {
|
|
119
|
+
throw new EditApplyError(total === 1 ? "Error: oldText must not be empty." : `Error: edits[${index}].oldText must not be empty.`);
|
|
120
|
+
}
|
|
121
|
+
const oldText = normalizeToLF(edit.oldText);
|
|
122
|
+
const exact = findAllOccurrences(content, oldText);
|
|
123
|
+
if (exact.length === 1) {
|
|
124
|
+
return { editIndex: index, mode: "exact", start: exact[0], end: exact[0] + oldText.length };
|
|
125
|
+
}
|
|
126
|
+
if (exact.length > 1) {
|
|
127
|
+
throw new EditApplyError(total === 1
|
|
128
|
+
? `Error: oldText appears ${exact.length} times in file. Must be unique: "${summarizeOldText(oldText)}"`
|
|
129
|
+
: `Error: edits[${index}].oldText appears ${exact.length} times in file. Must be unique: "${summarizeOldText(oldText)}"`);
|
|
130
|
+
}
|
|
131
|
+
const normalizedLineMatches = findNormalizedLineMatches(content, oldText);
|
|
132
|
+
if (normalizedLineMatches.length === 1) {
|
|
133
|
+
return {
|
|
134
|
+
editIndex: index,
|
|
135
|
+
mode: "normalized-line",
|
|
136
|
+
start: normalizedLineMatches[0].start,
|
|
137
|
+
end: normalizedLineMatches[0].end,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (normalizedLineMatches.length > 1) {
|
|
141
|
+
throw new EditApplyError(total === 1
|
|
142
|
+
? `Error: oldText matched ${normalizedLineMatches.length} normalized line blocks in file. Provide more surrounding context.`
|
|
143
|
+
: `Error: edits[${index}].oldText matched ${normalizedLineMatches.length} normalized line blocks in file. Provide more surrounding context.`);
|
|
144
|
+
}
|
|
145
|
+
const hint = findBestLineHint(content, oldText);
|
|
146
|
+
const suffix = hint ? `\n${hint}` : "";
|
|
147
|
+
throw new EditApplyError(total === 1
|
|
148
|
+
? `Error: oldText not found in file: "${summarizeOldText(oldText)}"${suffix}`
|
|
149
|
+
: `Error: edits[${index}].oldText not found in file: "${summarizeOldText(oldText)}"${suffix}`);
|
|
150
|
+
}
|
|
151
|
+
function assertNoOverlaps(matches) {
|
|
152
|
+
const sorted = [...matches].sort((a, b) => a.start - b.start);
|
|
153
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
154
|
+
const previous = sorted[i - 1];
|
|
155
|
+
const current = sorted[i];
|
|
156
|
+
if (previous.end > current.start) {
|
|
157
|
+
throw new EditApplyError(`Error: edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in file. Merge them into one edit or target disjoint regions.`, "blocked");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export function applyEditsToContent(rawContent, edits) {
|
|
162
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
163
|
+
throw new EditApplyError("Error: No edits provided");
|
|
164
|
+
}
|
|
165
|
+
const { bom, text } = stripBom(rawContent);
|
|
166
|
+
const lineEnding = detectLineEnding(text);
|
|
167
|
+
const normalizedOriginal = normalizeToLF(text);
|
|
168
|
+
const normalizedEdits = edits.map((edit) => ({
|
|
169
|
+
oldText: normalizeToLF(edit.oldText),
|
|
170
|
+
newText: normalizeToLF(edit.newText),
|
|
171
|
+
}));
|
|
172
|
+
const matches = normalizedEdits.map((edit, index) => matchEdit(normalizedOriginal, edit, index, normalizedEdits.length));
|
|
173
|
+
assertNoOverlaps(matches);
|
|
174
|
+
const byDescendingStart = [...matches].sort((a, b) => b.start - a.start);
|
|
175
|
+
let normalizedNext = normalizedOriginal;
|
|
176
|
+
for (const match of byDescendingStart) {
|
|
177
|
+
const edit = normalizedEdits[match.editIndex];
|
|
178
|
+
normalizedNext = normalizedNext.slice(0, match.start) + edit.newText + normalizedNext.slice(match.end);
|
|
179
|
+
}
|
|
180
|
+
if (normalizedNext === normalizedOriginal) {
|
|
181
|
+
throw new EditApplyError("Error: No changes made. The replacement produced identical content.");
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
content: bom + restoreLineEndings(normalizedNext, lineEnding),
|
|
185
|
+
normalizedOriginal,
|
|
186
|
+
normalizedNext,
|
|
187
|
+
bom,
|
|
188
|
+
lineEnding,
|
|
189
|
+
matches,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
export function formatEditMatchNotes(matches) {
|
|
193
|
+
const normalizedCount = matches.filter((match) => match.mode !== "exact").length;
|
|
194
|
+
if (normalizedCount === 0)
|
|
195
|
+
return "";
|
|
196
|
+
return `\n\nNote: ${normalizedCount} edit${normalizedCount === 1 ? "" : "s"} applied using normalized line matching for whitespace/formatting differences.`;
|
|
197
|
+
}
|
package/dist/tools/edit.js
CHANGED
|
@@ -5,14 +5,22 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { constants } from "node:fs";
|
|
7
7
|
import { access, readFile, writeFile } from "node:fs/promises";
|
|
8
|
-
import { resolve } from "node:path";
|
|
8
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
9
9
|
import { createTwoFilesPatch } from "diff";
|
|
10
10
|
import { gateToolAction } from "../approval/tool-helper.js";
|
|
11
11
|
import { formatDiagnosticBlocks } from "../lsp/index.js";
|
|
12
|
+
import { applyEditsToContent, EditApplyError, formatEditMatchNotes } from "./edit-apply.js";
|
|
13
|
+
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
14
|
+
function isWithinWorkspace(cwd, filePath) {
|
|
15
|
+
const rel = relative(resolve(cwd), filePath);
|
|
16
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
17
|
+
}
|
|
12
18
|
export function createEditTool(cwd, approval, lsp) {
|
|
13
19
|
return {
|
|
14
20
|
name: "edit",
|
|
15
|
-
|
|
21
|
+
effect: "write_direct",
|
|
22
|
+
requiresApproval: true,
|
|
23
|
+
description: "Apply targeted string replacements to a file. Prefer exact oldText. The tool can tolerate line ending, trailing whitespace, Unicode punctuation/space, and blank-line differences only when the target is unique.",
|
|
16
24
|
parameters: {
|
|
17
25
|
type: "object",
|
|
18
26
|
properties: {
|
|
@@ -34,67 +42,66 @@ export function createEditTool(cwd, approval, lsp) {
|
|
|
34
42
|
},
|
|
35
43
|
async execute(args) {
|
|
36
44
|
const filePath = resolve(cwd, args.path);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
if (!isWithinWorkspace(cwd, filePath)) {
|
|
46
|
+
return {
|
|
47
|
+
content: `Error: Edit path is outside the workspace: ${filePath}`,
|
|
48
|
+
isError: true,
|
|
49
|
+
status: "blocked",
|
|
50
|
+
metadata: {
|
|
51
|
+
kind: "security",
|
|
52
|
+
path: filePath,
|
|
53
|
+
reason: "Edit path is outside the workspace.",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
48
56
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (count === 0) {
|
|
53
|
-
return {
|
|
54
|
-
content: `Error: oldText not found in file: "${edit.oldText.slice(0, 50)}..."`,
|
|
55
|
-
isError: true,
|
|
56
|
-
};
|
|
57
|
+
return withFileMutationQueue(filePath, async () => {
|
|
58
|
+
try {
|
|
59
|
+
await access(filePath, constants.R_OK | constants.W_OK);
|
|
57
60
|
}
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
content: `Error: oldText appears ${count} times in file. Must be unique: "${edit.oldText.slice(0, 50)}..."`,
|
|
61
|
-
isError: true,
|
|
62
|
-
};
|
|
61
|
+
catch {
|
|
62
|
+
return { content: `Error: Cannot read/write file: ${filePath}`, isError: true };
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
for (const edit of edits) {
|
|
67
|
-
content = content.replace(edit.oldText, edit.newText);
|
|
68
|
-
}
|
|
69
|
-
const diff = createTwoFilesPatch(filePath, filePath, original, content, "original", "modified", { context: 3 });
|
|
70
|
-
// Gate on the approval controller BEFORE persisting the change.
|
|
71
|
-
const gate = await gateToolAction(approval, {
|
|
72
|
-
type: "edit",
|
|
73
|
-
path: filePath,
|
|
74
|
-
diff,
|
|
75
|
-
fileExists: true,
|
|
76
|
-
});
|
|
77
|
-
if (!gate.approved)
|
|
78
|
-
return gate.result;
|
|
79
|
-
await writeFile(filePath, content, "utf-8");
|
|
80
|
-
let output = `Edited ${filePath}\n\nDiff:\n${diff}`;
|
|
81
|
-
if (lsp) {
|
|
64
|
+
const original = await readFile(filePath, "utf-8");
|
|
65
|
+
let applied;
|
|
82
66
|
try {
|
|
83
|
-
|
|
84
|
-
output += formatDiagnosticBlocks(cwd, filePath, lsp.diagnostics());
|
|
67
|
+
applied = applyEditsToContent(original, args.edits);
|
|
85
68
|
}
|
|
86
|
-
catch {
|
|
87
|
-
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err instanceof EditApplyError) {
|
|
71
|
+
return { content: err.message, isError: true, status: err.status };
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
88
74
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
metadata: {
|
|
94
|
-
kind: "edit",
|
|
75
|
+
const diff = createTwoFilesPatch(filePath, filePath, original, applied.content, "original", "modified", { context: 3 });
|
|
76
|
+
// Gate on the approval controller BEFORE persisting the change.
|
|
77
|
+
const gate = await gateToolAction(approval, {
|
|
78
|
+
type: "edit",
|
|
95
79
|
path: filePath,
|
|
96
|
-
|
|
97
|
-
|
|
80
|
+
diff,
|
|
81
|
+
fileExists: true,
|
|
82
|
+
});
|
|
83
|
+
if (!gate.approved)
|
|
84
|
+
return gate.result;
|
|
85
|
+
await writeFile(filePath, applied.content, "utf-8");
|
|
86
|
+
let output = `Edited ${filePath}${formatEditMatchNotes(applied.matches)}\n\nDiff:\n${diff}`;
|
|
87
|
+
if (lsp) {
|
|
88
|
+
try {
|
|
89
|
+
await lsp.touchFile(filePath, "document");
|
|
90
|
+
output += formatDiagnosticBlocks(cwd, filePath, lsp.diagnostics());
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// LSP diagnostics should not turn a successful edit into a failed tool call.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
content: output,
|
|
98
|
+
status: "success",
|
|
99
|
+
metadata: {
|
|
100
|
+
kind: "edit",
|
|
101
|
+
path: filePath,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
});
|
|
98
105
|
},
|
|
99
106
|
};
|
|
100
107
|
}
|
|
@@ -9,7 +9,9 @@ export function createExitPlanModeTool(controller) {
|
|
|
9
9
|
return {
|
|
10
10
|
name: "exit_plan_mode",
|
|
11
11
|
readOnly: true,
|
|
12
|
-
|
|
12
|
+
effect: "read",
|
|
13
|
+
requiresApproval: true,
|
|
14
|
+
description: "ONLY call this tool when the harness has told you via a runtime reminder that plan mode is ACTIVE. " +
|
|
13
15
|
"Do NOT call it during ordinary work — in default mode you should just use the regular tools directly. " +
|
|
14
16
|
"In plan mode: after investigating, call this with a concrete step-by-step plan so the user can approve, edit, or reject. " +
|
|
15
17
|
"Approval automatically switches the agent out of plan mode.",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function withFileMutationQueue<T>(filePath: string, fn: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const queues = new Map();
|
|
4
|
+
function queueKey(filePath) {
|
|
5
|
+
const resolved = resolve(filePath);
|
|
6
|
+
try {
|
|
7
|
+
return realpathSync.native(resolved);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return resolved;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function withFileMutationQueue(filePath, fn) {
|
|
14
|
+
const key = queueKey(filePath);
|
|
15
|
+
const current = queues.get(key) ?? Promise.resolve();
|
|
16
|
+
let release;
|
|
17
|
+
const next = new Promise((resolveNext) => {
|
|
18
|
+
release = resolveNext;
|
|
19
|
+
});
|
|
20
|
+
const chained = current.then(() => next);
|
|
21
|
+
queues.set(key, chained);
|
|
22
|
+
await current;
|
|
23
|
+
try {
|
|
24
|
+
return await fn();
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
release();
|
|
28
|
+
if (queues.get(key) === chained) {
|
|
29
|
+
queues.delete(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/tools/glob.js
CHANGED
|
@@ -20,6 +20,7 @@ export function createGlobTool(cwd) {
|
|
|
20
20
|
return {
|
|
21
21
|
name: "glob",
|
|
22
22
|
readOnly: true,
|
|
23
|
+
effect: "read",
|
|
23
24
|
description: `Find files by glob pattern without using the shell. Use this for project structure discovery and filename searches. Returns up to ${MAX_RESULTS} files sorted by recent modification time.`,
|
|
24
25
|
parameters: {
|
|
25
26
|
type: "object",
|
package/dist/tools/grep.js
CHANGED
|
@@ -10,6 +10,7 @@ export function createGrepTool(cwd) {
|
|
|
10
10
|
return {
|
|
11
11
|
name: "grep",
|
|
12
12
|
readOnly: true,
|
|
13
|
+
effect: "read",
|
|
13
14
|
description: `Search file contents using regex (via ripgrep). Use this instead of running grep, rg, or ripgrep through bash. Returns up to ${MAX_MATCHES} matches.`,
|
|
14
15
|
parameters: {
|
|
15
16
|
type: "object",
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { createLspTool } from "./lsp.js";
|
|
|
11
11
|
export { createWebFetchTool } from "./web-fetch.js";
|
|
12
12
|
export { createWebSearchTool } from "./web-search.js";
|
|
13
13
|
export { createSkillTool } from "./skill.js";
|
|
14
|
-
export {
|
|
14
|
+
export { createAgentLifecycleTools, createCloseAgentTool, createSendInputTool, createSpawnAgentTool, createWaitAgentTool } from "./agent-lifecycle.js";
|
|
15
15
|
export { createTodoTool, type TodoStore } from "./todo.js";
|
|
16
16
|
export { createExitPlanModeTool, type PlanController } from "./exit-plan-mode.js";
|
|
17
17
|
export { createToolSearchTool, type ToolSearchController } from "./tool-search.js";
|
package/dist/tools/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { createLspTool } from "./lsp.js";
|
|
|
11
11
|
export { createWebFetchTool } from "./web-fetch.js";
|
|
12
12
|
export { createWebSearchTool } from "./web-search.js";
|
|
13
13
|
export { createSkillTool } from "./skill.js";
|
|
14
|
-
export {
|
|
14
|
+
export { createAgentLifecycleTools, createCloseAgentTool, createSendInputTool, createSpawnAgentTool, createWaitAgentTool } from "./agent-lifecycle.js";
|
|
15
15
|
export { createTodoTool } from "./todo.js";
|
|
16
16
|
export { createExitPlanModeTool } from "./exit-plan-mode.js";
|
|
17
17
|
export { createToolSearchTool } from "./tool-search.js";
|
|
@@ -26,7 +26,7 @@ import { getLspService } from "../lsp/index.js";
|
|
|
26
26
|
import { createLspTool } from "./lsp.js";
|
|
27
27
|
import { createReadTool } from "./read.js";
|
|
28
28
|
import { createSkillTool } from "./skill.js";
|
|
29
|
-
import {
|
|
29
|
+
import { createAgentLifecycleTools } from "./agent-lifecycle.js";
|
|
30
30
|
import { createTodoTool } from "./todo.js";
|
|
31
31
|
import { createToolSearchTool } from "./tool-search.js";
|
|
32
32
|
import { createWebFetchTool } from "./web-fetch.js";
|
|
@@ -49,7 +49,7 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
|
|
|
49
49
|
createWebFetchTool(approval),
|
|
50
50
|
createMemorySearchTool(cwd),
|
|
51
51
|
createMemoryReadSummaryTool(cwd),
|
|
52
|
-
|
|
52
|
+
...createAgentLifecycleTools(),
|
|
53
53
|
...(options.questionController ? [createQuestionTool(options.questionController)] : []),
|
|
54
54
|
...(skillRegistry ? [createSkillTool(skillRegistry)] : []),
|
|
55
55
|
...(options.todoStore ? [createTodoTool(options.todoStore)] : []),
|
package/dist/tools/lsp.js
CHANGED
|
@@ -18,6 +18,8 @@ export function createLspTool(cwd, lsp = getLspService(cwd), approval) {
|
|
|
18
18
|
return {
|
|
19
19
|
name: "lsp",
|
|
20
20
|
readOnly: true,
|
|
21
|
+
effect: "read",
|
|
22
|
+
requiresApproval: true,
|
|
21
23
|
description: "Use the language server for code navigation. Supports goToDefinition, findReferences, hover, documentSymbol, workspaceSymbol, goToImplementation, prepareCallHierarchy, incomingCalls, and outgoingCalls.",
|
|
22
24
|
parameters: {
|
|
23
25
|
type: "object",
|
package/dist/tools/memory.js
CHANGED
|
@@ -5,6 +5,7 @@ export function createMemorySearchTool(cwd) {
|
|
|
5
5
|
name: "memory_search",
|
|
6
6
|
description: "Search persistent Bubble memory for prior project facts, user preferences, workflows, decisions, and gotchas.",
|
|
7
7
|
readOnly: true,
|
|
8
|
+
effect: "read",
|
|
8
9
|
parameters: {
|
|
9
10
|
type: "object",
|
|
10
11
|
properties: {
|
|
@@ -56,6 +57,7 @@ export function createMemoryReadSummaryTool(cwd) {
|
|
|
56
57
|
name: "memory_read_summary",
|
|
57
58
|
description: "Read the concise persistent memory summary for the current project, global scope, or both.",
|
|
58
59
|
readOnly: true,
|
|
60
|
+
effect: "read",
|
|
59
61
|
parameters: {
|
|
60
62
|
type: "object",
|
|
61
63
|
properties: {
|
package/dist/tools/question.js
CHANGED
package/dist/tools/read.js
CHANGED
|
@@ -11,6 +11,7 @@ export function createReadTool(cwd, approval, lsp) {
|
|
|
11
11
|
return {
|
|
12
12
|
name: "read",
|
|
13
13
|
readOnly: true,
|
|
14
|
+
effect: "read",
|
|
14
15
|
description: `Read the contents of a file. Output is truncated to ${MAX_LINES} lines or ${MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,
|
|
15
16
|
parameters: {
|
|
16
17
|
type: "object",
|
package/dist/tools/skill.js
CHANGED
|
@@ -21,6 +21,7 @@ export function createSkillTool(registry) {
|
|
|
21
21
|
return {
|
|
22
22
|
name: "skill",
|
|
23
23
|
readOnly: true,
|
|
24
|
+
effect: "read",
|
|
24
25
|
description: "Load a named skill on demand. Use this when a task clearly matches one of the available skills.",
|
|
25
26
|
parameters: {
|
|
26
27
|
type: "object",
|
package/dist/tools/task.js
CHANGED
package/dist/tools/todo.js
CHANGED
|
@@ -8,6 +8,7 @@ export function createTodoTool(store) {
|
|
|
8
8
|
return {
|
|
9
9
|
name: "todo_write",
|
|
10
10
|
readOnly: true,
|
|
11
|
+
effect: "read",
|
|
11
12
|
description: `Create or update the task list for the current work. Send the COMPLETE list each call; this overwrites the prior list entirely.
|
|
12
13
|
|
|
13
14
|
## When to use
|
|
@@ -17,8 +17,9 @@ export function createToolSearchTool(controller) {
|
|
|
17
17
|
return {
|
|
18
18
|
name: "tool_search",
|
|
19
19
|
readOnly: true,
|
|
20
|
+
effect: "read",
|
|
20
21
|
description: 'Fetches full schema definitions for deferred tools so they can be called. ' +
|
|
21
|
-
'Deferred tools appear by name in
|
|
22
|
+
'Deferred tools appear by name in hidden runtime reminders; their parameters are unknown ' +
|
|
22
23
|
'until loaded. Use this tool with query "select:<name>[,<name>...]" to load specific tools, ' +
|
|
23
24
|
'or with free-text keywords to search for relevant tools.',
|
|
24
25
|
parameters: {
|
package/dist/tools/web-fetch.js
CHANGED
package/dist/tools/web-search.js
CHANGED
package/dist/tools/write.js
CHANGED
|
@@ -10,6 +10,8 @@ import { formatDiagnosticBlocks } from "../lsp/index.js";
|
|
|
10
10
|
export function createWriteTool(cwd, options = {}, approval, lsp) {
|
|
11
11
|
return {
|
|
12
12
|
name: "write",
|
|
13
|
+
effect: "write_direct",
|
|
14
|
+
requiresApproval: true,
|
|
13
15
|
description: `Write a file to disk. Creates parent directories if needed.${options.refuseOverwrite ? " Will not overwrite existing files." : ""}`,
|
|
14
16
|
parameters: {
|
|
15
17
|
type: "object",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolResultMetadata } from "../types.js";
|
|
1
|
+
import type { ToolResultMetadata, TokenUsage } from "../types.js";
|
|
2
2
|
export interface CompactionMeta {
|
|
3
3
|
turns: number;
|
|
4
4
|
messages: number;
|
|
@@ -20,15 +20,22 @@ export interface DisplayMessage {
|
|
|
20
20
|
syntheticKind?: "ui_compact_card";
|
|
21
21
|
hiddenCount?: number;
|
|
22
22
|
compactionMeta?: CompactionMeta;
|
|
23
|
+
turnStartedAt?: number;
|
|
24
|
+
turnCompletedAt?: number;
|
|
25
|
+
turnUsage?: TokenUsage;
|
|
23
26
|
}
|
|
24
27
|
export interface DisplayToolCall {
|
|
25
28
|
id: string;
|
|
26
29
|
name: string;
|
|
27
30
|
args: Record<string, any>;
|
|
31
|
+
rawArguments?: string;
|
|
32
|
+
streamingArgs?: boolean;
|
|
28
33
|
status?: "pending" | "running" | "completed" | "error";
|
|
29
34
|
result?: string;
|
|
30
35
|
isError?: boolean;
|
|
31
36
|
metadata?: ToolResultMetadata;
|
|
37
|
+
startedAt?: number;
|
|
38
|
+
completedAt?: number;
|
|
32
39
|
}
|
|
33
40
|
export declare function compactDisplayMessages(messages: DisplayMessage[]): DisplayMessage[];
|
|
34
41
|
export declare function truncateText(value: string, maxChars: number): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface MarkdownInlineSegment {
|
|
2
|
+
text: string;
|
|
3
|
+
color?: "text" | "textMuted" | "success" | "warning" | "secondary";
|
|
4
|
+
bold?: boolean;
|
|
5
|
+
italic?: boolean;
|
|
6
|
+
dim?: boolean;
|
|
7
|
+
}
|
|
8
|
+
type InlineToken = {
|
|
9
|
+
type?: string;
|
|
10
|
+
text?: string;
|
|
11
|
+
raw?: string;
|
|
12
|
+
href?: string;
|
|
13
|
+
tokens?: InlineToken[];
|
|
14
|
+
};
|
|
15
|
+
interface InlineStyle {
|
|
16
|
+
bold?: boolean;
|
|
17
|
+
italic?: boolean;
|
|
18
|
+
dim?: boolean;
|
|
19
|
+
color?: MarkdownInlineSegment["color"];
|
|
20
|
+
}
|
|
21
|
+
export declare function markdownInlineSegments(tokens: InlineToken[] | undefined, fallback?: string, style?: InlineStyle): MarkdownInlineSegment[];
|
|
22
|
+
export {};
|