@bubblebrain-ai/bubble 0.0.4 → 0.0.6
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/task-size.d.ts +9 -0
- package/dist/agent/task-size.js +33 -0
- package/dist/agent/tool-intent.d.ts +1 -0
- package/dist/agent/tool-intent.js +1 -1
- package/dist/agent.d.ts +60 -1
- package/dist/agent.js +648 -55
- 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 +85 -35
- package/dist/orchestrator/hooks.d.ts +5 -3
- package/dist/prompt/compose.d.ts +1 -0
- package/dist/prompt/compose.js +11 -1
- package/dist/prompt/environment.js +23 -2
- package/dist/prompt/provider-prompts/deepseek.js +1 -2
- package/dist/prompt/provider-prompts/kimi.js +1 -2
- package/dist/prompt/reminders.d.ts +21 -2
- package/dist/prompt/reminders.js +53 -8
- package/dist/prompt/runtime.d.ts +1 -1
- package/dist/prompt/runtime.js +17 -23
- package/dist/provider-artifacts.d.ts +7 -0
- package/dist/provider-artifacts.js +60 -0
- package/dist/provider.d.ts +16 -8
- package/dist/provider.js +149 -34
- 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.d.ts +2 -1
- package/dist/tools/bash.js +3 -1
- package/dist/tools/edit-apply.d.ts +25 -0
- package/dist/tools/edit-apply.js +228 -0
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +75 -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/file-state.d.ts +25 -0
- package/dist/tools/file-state.js +52 -0
- package/dist/tools/glob.js +1 -0
- package/dist/tools/grep.js +1 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.js +9 -7
- 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.d.ts +2 -1
- package/dist/tools/read.js +6 -1
- 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.d.ts +4 -3
- package/dist/tools/write.js +135 -54
- package/dist/tui/display-history.d.ts +10 -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 +811 -274
- package/dist/tui/streaming-tool-args.d.ts +15 -0
- package/dist/tui/streaming-tool-args.js +30 -0
- 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 +30 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +88 -0
- package/dist/types.d.ts +105 -10
- package/package.json +1 -1
package/dist/tools/write.js
CHANGED
|
@@ -1,84 +1,165 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Write tool - create or
|
|
2
|
+
* Write tool - create files or safely replace full file contents.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
5
|
import { dirname, resolve } from "node:path";
|
|
7
6
|
import { createTwoFilesPatch } from "diff";
|
|
8
7
|
import { gateToolAction } from "../approval/tool-helper.js";
|
|
9
8
|
import { formatDiagnosticBlocks } from "../lsp/index.js";
|
|
10
|
-
|
|
9
|
+
import { isWithinWorkspace } from "./file-state.js";
|
|
10
|
+
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
11
|
+
export function createWriteTool(cwd, options = {}, approval, lsp, fileState) {
|
|
11
12
|
return {
|
|
12
13
|
name: "write",
|
|
13
|
-
|
|
14
|
+
effect: "write_direct",
|
|
15
|
+
requiresApproval: true,
|
|
16
|
+
description: "Write a file to disk. Creates parent directories if needed. For an existing file, use overwrite=true only for full-file replacement after the file has been read or modified in this session; use edit for small targeted changes.",
|
|
14
17
|
parameters: {
|
|
15
18
|
type: "object",
|
|
16
19
|
properties: {
|
|
17
20
|
path: { type: "string", description: "Path to the file (relative or absolute)" },
|
|
18
21
|
content: { type: "string", description: "File contents" },
|
|
22
|
+
overwrite: {
|
|
23
|
+
type: "boolean",
|
|
24
|
+
description: "Set true only for full-file replacement of an existing file. Existing files must have been read or modified in this session.",
|
|
25
|
+
},
|
|
19
26
|
},
|
|
20
27
|
required: ["path", "content"],
|
|
21
28
|
},
|
|
22
29
|
async execute(args) {
|
|
23
30
|
const filePath = resolve(cwd, args.path);
|
|
24
|
-
|
|
31
|
+
const overwrite = args.overwrite === true;
|
|
32
|
+
if (!isWithinWorkspace(cwd, filePath)) {
|
|
33
|
+
return {
|
|
34
|
+
content: `Error: Write path is outside the workspace: ${filePath}`,
|
|
35
|
+
isError: true,
|
|
36
|
+
status: "blocked",
|
|
37
|
+
metadata: {
|
|
38
|
+
kind: "security",
|
|
39
|
+
path: filePath,
|
|
40
|
+
reason: "Write path is outside the workspace.",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return withFileMutationQueue(filePath, async () => {
|
|
45
|
+
let existed = false;
|
|
46
|
+
let oldContent = "";
|
|
25
47
|
try {
|
|
26
|
-
await
|
|
48
|
+
oldContent = await readFile(filePath, "utf-8");
|
|
49
|
+
existed = true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// New file.
|
|
53
|
+
}
|
|
54
|
+
if (existed && options.refuseOverwrite && !overwrite) {
|
|
27
55
|
return {
|
|
28
|
-
content: `Error: File already exists: ${filePath}
|
|
56
|
+
content: `Error: File already exists: ${filePath}.\n\n`
|
|
57
|
+
+ "For small targeted changes, use edit.\n"
|
|
58
|
+
+ "For a full-file replacement, call write again with overwrite=true. Existing files must be read or modified in this session before they can be safely overwritten.\n"
|
|
59
|
+
+ "Do not delete and recreate the file just to overwrite it.",
|
|
29
60
|
isError: true,
|
|
30
61
|
};
|
|
31
62
|
}
|
|
32
|
-
|
|
33
|
-
|
|
63
|
+
if (existed && overwrite && options.refuseOverwrite) {
|
|
64
|
+
if (!fileState) {
|
|
65
|
+
return {
|
|
66
|
+
content: `Error: Cannot safely overwrite ${filePath} because file-state tracking is unavailable. `
|
|
67
|
+
+ "Read the file first in this agent session, then retry the full-file replacement.",
|
|
68
|
+
isError: true,
|
|
69
|
+
status: "blocked",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const freshness = await fileState.checkFresh(filePath);
|
|
73
|
+
if (!freshness.ok) {
|
|
74
|
+
return staleOverwriteResult(filePath, freshness.reason);
|
|
75
|
+
}
|
|
34
76
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
content: args.content,
|
|
50
|
-
diff,
|
|
51
|
-
fileExists: existed,
|
|
52
|
-
});
|
|
53
|
-
if (!gate.approved)
|
|
54
|
-
return gate.result;
|
|
55
|
-
try {
|
|
56
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
57
|
-
await writeFile(filePath, args.content, "utf-8");
|
|
58
|
-
const lineCount = args.content.split("\n").length;
|
|
59
|
-
const verb = existed ? "Updated" : "Wrote";
|
|
60
|
-
let content = `${verb} ${lineCount} lines to ${filePath}`;
|
|
61
|
-
if (lsp) {
|
|
62
|
-
try {
|
|
63
|
-
await lsp.touchFile(filePath, "document");
|
|
64
|
-
content += formatDiagnosticBlocks(cwd, filePath, lsp.diagnostics());
|
|
77
|
+
const diff = createTwoFilesPatch(filePath, filePath, oldContent, args.content, "original", "modified", { context: 3 });
|
|
78
|
+
const gate = await gateToolAction(approval, {
|
|
79
|
+
type: "write",
|
|
80
|
+
path: filePath,
|
|
81
|
+
content: args.content,
|
|
82
|
+
diff,
|
|
83
|
+
fileExists: existed,
|
|
84
|
+
});
|
|
85
|
+
if (!gate.approved)
|
|
86
|
+
return gate.result;
|
|
87
|
+
if (existed && overwrite && options.refuseOverwrite && fileState) {
|
|
88
|
+
const freshness = await fileState.checkFresh(filePath);
|
|
89
|
+
if (!freshness.ok) {
|
|
90
|
+
return staleOverwriteResult(filePath, freshness.reason);
|
|
65
91
|
}
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
95
|
+
await writeFile(filePath, args.content, "utf-8");
|
|
96
|
+
await fileState?.observe(filePath, "write", args.content).catch(() => undefined);
|
|
97
|
+
const lineCount = args.content.split("\n").length;
|
|
98
|
+
const verb = existed ? "Updated" : "Wrote";
|
|
99
|
+
let content = `${verb} ${lineCount} lines to ${filePath}`;
|
|
100
|
+
if (lsp) {
|
|
101
|
+
try {
|
|
102
|
+
await lsp.touchFile(filePath, "document");
|
|
103
|
+
content += formatDiagnosticBlocks(cwd, filePath, lsp.diagnostics());
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// LSP diagnostics should not turn a successful write into a failed tool call.
|
|
107
|
+
}
|
|
68
108
|
}
|
|
109
|
+
return {
|
|
110
|
+
content,
|
|
111
|
+
status: "success",
|
|
112
|
+
metadata: {
|
|
113
|
+
kind: "write",
|
|
114
|
+
path: filePath,
|
|
115
|
+
overwrite,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
69
118
|
}
|
|
70
|
-
|
|
71
|
-
content
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
119
|
+
catch (err) {
|
|
120
|
+
return { content: `Error: ${err.message}`, isError: true };
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function staleOverwriteResult(filePath, reason) {
|
|
127
|
+
if (reason === "unobserved") {
|
|
128
|
+
return {
|
|
129
|
+
content: `Error: Cannot safely overwrite existing file: ${filePath}.\n\n`
|
|
130
|
+
+ "This file has not been read or modified in this agent session. Read it first, then retry write with overwrite=true.\n"
|
|
131
|
+
+ "For small targeted changes, use edit. Do not delete and recreate the file just to overwrite it.",
|
|
132
|
+
isError: true,
|
|
133
|
+
status: "blocked",
|
|
134
|
+
metadata: {
|
|
135
|
+
kind: "security",
|
|
136
|
+
path: filePath,
|
|
137
|
+
reason,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (reason === "changed") {
|
|
142
|
+
return {
|
|
143
|
+
content: `Error: Cannot safely overwrite ${filePath} because it changed since the last read/write/edit in this agent session.\n\n`
|
|
144
|
+
+ "Re-read the file to pick up the latest content, then retry write with overwrite=true if a full-file replacement is still intended.",
|
|
145
|
+
isError: true,
|
|
146
|
+
status: "blocked",
|
|
147
|
+
metadata: {
|
|
148
|
+
kind: "security",
|
|
149
|
+
path: filePath,
|
|
150
|
+
reason,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
content: `Error: Cannot safely overwrite ${filePath} because it is missing now.\n\n`
|
|
156
|
+
+ "Check the path before retrying.",
|
|
157
|
+
isError: true,
|
|
158
|
+
status: "blocked",
|
|
159
|
+
metadata: {
|
|
160
|
+
kind: "security",
|
|
161
|
+
path: filePath,
|
|
162
|
+
reason,
|
|
82
163
|
},
|
|
83
164
|
};
|
|
84
165
|
}
|
|
@@ -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,24 @@ 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;
|
|
33
|
+
/** During streaming, an approximate line count derived from `\n` escapes in rawArguments. */
|
|
34
|
+
streamingNewlineCount?: number;
|
|
28
35
|
status?: "pending" | "running" | "completed" | "error";
|
|
29
36
|
result?: string;
|
|
30
37
|
isError?: boolean;
|
|
31
38
|
metadata?: ToolResultMetadata;
|
|
39
|
+
startedAt?: number;
|
|
40
|
+
completedAt?: number;
|
|
32
41
|
}
|
|
33
42
|
export declare function compactDisplayMessages(messages: DisplayMessage[]): DisplayMessage[];
|
|
34
43
|
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 {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function markdownInlineSegments(tokens, fallback = "", style = {}) {
|
|
2
|
+
const segments = [];
|
|
3
|
+
for (const token of tokens ?? []) {
|
|
4
|
+
appendInlineToken(segments, token, style);
|
|
5
|
+
}
|
|
6
|
+
if (segments.length === 0 && fallback) {
|
|
7
|
+
appendStyled(segments, fallback, style);
|
|
8
|
+
}
|
|
9
|
+
return segments;
|
|
10
|
+
}
|
|
11
|
+
function appendInlineToken(segments, token, style) {
|
|
12
|
+
switch (token.type) {
|
|
13
|
+
case "strong":
|
|
14
|
+
appendInlineTokens(segments, token.tokens, { ...style, bold: true });
|
|
15
|
+
return;
|
|
16
|
+
case "em":
|
|
17
|
+
appendInlineTokens(segments, token.tokens, { ...style, italic: true, color: style.color ?? "warning" });
|
|
18
|
+
return;
|
|
19
|
+
case "del":
|
|
20
|
+
appendInlineTokens(segments, token.tokens, { ...style, dim: true, color: style.color ?? "textMuted" });
|
|
21
|
+
return;
|
|
22
|
+
case "codespan":
|
|
23
|
+
appendStyled(segments, token.text ?? "", { ...style, color: "success" });
|
|
24
|
+
return;
|
|
25
|
+
case "link":
|
|
26
|
+
appendInlineTokens(segments, token.tokens, { ...style, color: style.color ?? "secondary" });
|
|
27
|
+
return;
|
|
28
|
+
case "br":
|
|
29
|
+
appendStyled(segments, "\n", style);
|
|
30
|
+
return;
|
|
31
|
+
case "text":
|
|
32
|
+
case "paragraph":
|
|
33
|
+
case "list_item":
|
|
34
|
+
case "heading":
|
|
35
|
+
if (token.tokens?.length) {
|
|
36
|
+
appendInlineTokens(segments, token.tokens, style);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
appendStyled(segments, token.text ?? token.raw ?? "", style);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
case "space":
|
|
43
|
+
return;
|
|
44
|
+
default:
|
|
45
|
+
if (token.tokens?.length) {
|
|
46
|
+
appendInlineTokens(segments, token.tokens, style);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
appendStyled(segments, token.text ?? token.raw ?? "", style);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function appendInlineTokens(segments, tokens, style) {
|
|
54
|
+
for (const child of tokens ?? []) {
|
|
55
|
+
appendInlineToken(segments, child, style);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function appendStyled(segments, text, style) {
|
|
59
|
+
if (!text)
|
|
60
|
+
return;
|
|
61
|
+
segments.push({
|
|
62
|
+
text,
|
|
63
|
+
color: style.color ?? "text",
|
|
64
|
+
bold: style.bold,
|
|
65
|
+
italic: style.italic,
|
|
66
|
+
dim: style.dim,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function hashString(value: string): string;
|