@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.
Files changed (91) hide show
  1. package/dist/agent/budget-ledger.d.ts +20 -0
  2. package/dist/agent/budget-ledger.js +51 -0
  3. package/dist/agent/execution-governor.js +1 -1
  4. package/dist/agent/profiles.d.ts +59 -0
  5. package/dist/agent/profiles.js +460 -0
  6. package/dist/agent/subagent-control.d.ts +52 -0
  7. package/dist/agent/subagent-control.js +38 -0
  8. package/dist/agent/task-size.d.ts +9 -0
  9. package/dist/agent/task-size.js +33 -0
  10. package/dist/agent/tool-intent.d.ts +1 -0
  11. package/dist/agent/tool-intent.js +1 -1
  12. package/dist/agent.d.ts +60 -1
  13. package/dist/agent.js +648 -55
  14. package/dist/context/budget.js +1 -0
  15. package/dist/context/compact-llm.js +7 -6
  16. package/dist/context/compact.js +6 -6
  17. package/dist/context/projector.d.ts +3 -3
  18. package/dist/context/projector.js +32 -18
  19. package/dist/context/prune.d.ts +2 -2
  20. package/dist/context/prune.js +1 -4
  21. package/dist/main.js +12 -5
  22. package/dist/mcp/manager.js +1 -0
  23. package/dist/orchestrator/default-hooks.js +85 -35
  24. package/dist/orchestrator/hooks.d.ts +5 -3
  25. package/dist/prompt/compose.d.ts +1 -0
  26. package/dist/prompt/compose.js +11 -1
  27. package/dist/prompt/environment.js +23 -2
  28. package/dist/prompt/provider-prompts/deepseek.js +1 -2
  29. package/dist/prompt/provider-prompts/kimi.js +1 -2
  30. package/dist/prompt/reminders.d.ts +21 -2
  31. package/dist/prompt/reminders.js +53 -8
  32. package/dist/prompt/runtime.d.ts +1 -1
  33. package/dist/prompt/runtime.js +17 -23
  34. package/dist/provider-artifacts.d.ts +7 -0
  35. package/dist/provider-artifacts.js +60 -0
  36. package/dist/provider.d.ts +16 -8
  37. package/dist/provider.js +149 -34
  38. package/dist/session-log.js +3 -1
  39. package/dist/system-prompt.d.ts +2 -0
  40. package/dist/tools/agent-lifecycle.d.ts +6 -0
  41. package/dist/tools/agent-lifecycle.js +355 -0
  42. package/dist/tools/bash.d.ts +2 -1
  43. package/dist/tools/bash.js +3 -1
  44. package/dist/tools/edit-apply.d.ts +25 -0
  45. package/dist/tools/edit-apply.js +228 -0
  46. package/dist/tools/edit.d.ts +2 -1
  47. package/dist/tools/edit.js +75 -56
  48. package/dist/tools/exit-plan-mode.js +3 -1
  49. package/dist/tools/file-mutation-queue.d.ts +1 -0
  50. package/dist/tools/file-mutation-queue.js +32 -0
  51. package/dist/tools/file-state.d.ts +25 -0
  52. package/dist/tools/file-state.js +52 -0
  53. package/dist/tools/glob.js +1 -0
  54. package/dist/tools/grep.js +1 -0
  55. package/dist/tools/index.d.ts +3 -1
  56. package/dist/tools/index.js +9 -7
  57. package/dist/tools/lsp.js +2 -0
  58. package/dist/tools/memory.js +2 -0
  59. package/dist/tools/question.js +2 -0
  60. package/dist/tools/read.d.ts +2 -1
  61. package/dist/tools/read.js +6 -1
  62. package/dist/tools/skill.js +1 -0
  63. package/dist/tools/task.js +1 -0
  64. package/dist/tools/todo.js +1 -0
  65. package/dist/tools/tool-search.js +2 -1
  66. package/dist/tools/web-fetch.js +1 -0
  67. package/dist/tools/web-search.js +1 -0
  68. package/dist/tools/write.d.ts +4 -3
  69. package/dist/tools/write.js +135 -54
  70. package/dist/tui/display-history.d.ts +10 -1
  71. package/dist/tui/markdown-inline.d.ts +22 -0
  72. package/dist/tui/markdown-inline.js +68 -0
  73. package/dist/tui/render-signature.d.ts +1 -0
  74. package/dist/tui/render-signature.js +7 -0
  75. package/dist/tui/run.js +811 -274
  76. package/dist/tui/streaming-tool-args.d.ts +15 -0
  77. package/dist/tui/streaming-tool-args.js +30 -0
  78. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  79. package/dist/tui/tool-renderers/fallback.js +75 -0
  80. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  81. package/dist/tui/tool-renderers/registry.js +11 -0
  82. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  83. package/dist/tui/tool-renderers/subagent.js +114 -0
  84. package/dist/tui/tool-renderers/types.d.ts +36 -0
  85. package/dist/tui/tool-renderers/types.js +1 -0
  86. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  87. package/dist/tui/tool-renderers/write-preview.js +30 -0
  88. package/dist/tui/tool-renderers/write.d.ts +6 -0
  89. package/dist/tui/tool-renderers/write.js +88 -0
  90. package/dist/types.d.ts +105 -10
  91. package/package.json +1 -1
@@ -1,84 +1,165 @@
1
1
  /**
2
- * Write tool - create or overwrite files.
2
+ * Write tool - create files or safely replace full file contents.
3
3
  */
4
- import { constants } from "node:fs";
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
- export function createWriteTool(cwd, options = {}, approval, lsp) {
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
- description: `Write a file to disk. Creates parent directories if needed.${options.refuseOverwrite ? " Will not overwrite existing files." : ""}`,
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
- if (options.refuseOverwrite) {
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 access(filePath, constants.F_OK);
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}. Use edit tool to modify existing files.`,
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
- catch {
33
- // file doesn't exist, proceed
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
- let existed = false;
37
- let oldContent = "";
38
- try {
39
- oldContent = await readFile(filePath, "utf-8");
40
- existed = true;
41
- }
42
- catch {
43
- // new file
44
- }
45
- const diff = createTwoFilesPatch(filePath, filePath, oldContent, args.content, "original", "modified", { context: 3 });
46
- const gate = await gateToolAction(approval, {
47
- type: "write",
48
- path: filePath,
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
- catch {
67
- // LSP diagnostics should not turn a successful write into a failed tool call.
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
- return {
71
- content,
72
- status: "success",
73
- metadata: {
74
- kind: "write",
75
- path: filePath,
76
- },
77
- };
78
- }
79
- catch (err) {
80
- return { content: `Error: ${err.message}`, isError: true };
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;
@@ -0,0 +1,7 @@
1
+ export function hashString(value) {
2
+ let hash = 5381;
3
+ for (let index = 0; index < value.length; index++) {
4
+ hash = ((hash << 5) + hash) ^ value.charCodeAt(index);
5
+ }
6
+ return (hash >>> 0).toString(36);
7
+ }