@bubblebrain-ai/bubble 0.0.1
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 +70 -0
- package/dist/agent/evidence-tracker.d.ts +15 -0
- package/dist/agent/evidence-tracker.js +93 -0
- package/dist/agent/execution-governor.d.ts +30 -0
- package/dist/agent/execution-governor.js +169 -0
- package/dist/agent/subtask-policy.d.ts +14 -0
- package/dist/agent/subtask-policy.js +60 -0
- package/dist/agent/task-classifier.d.ts +3 -0
- package/dist/agent/task-classifier.js +36 -0
- package/dist/agent/tool-arbiter.d.ts +7 -0
- package/dist/agent/tool-arbiter.js +33 -0
- package/dist/agent/tool-intent.d.ts +20 -0
- package/dist/agent/tool-intent.js +176 -0
- package/dist/agent.d.ts +95 -0
- package/dist/agent.js +672 -0
- package/dist/approval/controller.d.ts +48 -0
- package/dist/approval/controller.js +78 -0
- package/dist/approval/danger.d.ts +13 -0
- package/dist/approval/danger.js +55 -0
- package/dist/approval/diff-hunks.d.ts +12 -0
- package/dist/approval/diff-hunks.js +32 -0
- package/dist/approval/session-cache.d.ts +35 -0
- package/dist/approval/session-cache.js +68 -0
- package/dist/approval/tool-helper.d.ts +14 -0
- package/dist/approval/tool-helper.js +32 -0
- package/dist/approval/types.d.ts +56 -0
- package/dist/approval/types.js +8 -0
- package/dist/bubble-home.d.ts +8 -0
- package/dist/bubble-home.js +19 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +82 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.js +144 -0
- package/dist/context/budget.d.ts +21 -0
- package/dist/context/budget.js +72 -0
- package/dist/context/compact-llm.d.ts +16 -0
- package/dist/context/compact-llm.js +132 -0
- package/dist/context/compact.d.ts +15 -0
- package/dist/context/compact.js +251 -0
- package/dist/context/overflow.d.ts +9 -0
- package/dist/context/overflow.js +46 -0
- package/dist/context/projector.d.ts +26 -0
- package/dist/context/projector.js +150 -0
- package/dist/context/prune.d.ts +9 -0
- package/dist/context/prune.js +111 -0
- package/dist/lsp/config.d.ts +18 -0
- package/dist/lsp/config.js +58 -0
- package/dist/lsp/diagnostics.d.ts +24 -0
- package/dist/lsp/diagnostics.js +103 -0
- package/dist/lsp/index.d.ts +3 -0
- package/dist/lsp/index.js +3 -0
- package/dist/lsp/service.d.ts +85 -0
- package/dist/lsp/service.js +695 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.js +352 -0
- package/dist/mcp/client.d.ts +68 -0
- package/dist/mcp/client.js +163 -0
- package/dist/mcp/config.d.ts +26 -0
- package/dist/mcp/config.js +127 -0
- package/dist/mcp/manager.d.ts +55 -0
- package/dist/mcp/manager.js +296 -0
- package/dist/mcp/name.d.ts +26 -0
- package/dist/mcp/name.js +40 -0
- package/dist/mcp/transports.d.ts +53 -0
- package/dist/mcp/transports.js +248 -0
- package/dist/mcp/types.d.ts +111 -0
- package/dist/mcp/types.js +14 -0
- package/dist/memory/db.d.ts +62 -0
- package/dist/memory/db.js +313 -0
- package/dist/memory/index.d.ts +9 -0
- package/dist/memory/index.js +9 -0
- package/dist/memory/paths.d.ts +18 -0
- package/dist/memory/paths.js +38 -0
- package/dist/memory/phase1.d.ts +23 -0
- package/dist/memory/phase1.js +172 -0
- package/dist/memory/phase2.d.ts +19 -0
- package/dist/memory/phase2.js +100 -0
- package/dist/memory/prompts.d.ts +19 -0
- package/dist/memory/prompts.js +99 -0
- package/dist/memory/reset.d.ts +1 -0
- package/dist/memory/reset.js +13 -0
- package/dist/memory/start.d.ts +24 -0
- package/dist/memory/start.js +50 -0
- package/dist/memory/storage.d.ts +10 -0
- package/dist/memory/storage.js +82 -0
- package/dist/memory/store.d.ts +43 -0
- package/dist/memory/store.js +193 -0
- package/dist/memory/usage.d.ts +1 -0
- package/dist/memory/usage.js +38 -0
- package/dist/model-catalog.d.ts +20 -0
- package/dist/model-catalog.js +99 -0
- package/dist/model-config.d.ts +32 -0
- package/dist/model-config.js +59 -0
- package/dist/model-pricing.d.ts +23 -0
- package/dist/model-pricing.js +46 -0
- package/dist/oauth/index.d.ts +3 -0
- package/dist/oauth/index.js +2 -0
- package/dist/oauth/openai-codex.d.ts +9 -0
- package/dist/oauth/openai-codex.js +173 -0
- package/dist/oauth/storage.d.ts +18 -0
- package/dist/oauth/storage.js +60 -0
- package/dist/oauth/types.d.ts +15 -0
- package/dist/oauth/types.js +1 -0
- package/dist/orchestrator/default-hooks.d.ts +2 -0
- package/dist/orchestrator/default-hooks.js +96 -0
- package/dist/orchestrator/hooks.d.ts +78 -0
- package/dist/orchestrator/hooks.js +52 -0
- package/dist/orchestrator/workflow.d.ts +10 -0
- package/dist/orchestrator/workflow.js +22 -0
- package/dist/permission/mode.d.ts +23 -0
- package/dist/permission/mode.js +20 -0
- package/dist/permissions/rule.d.ts +39 -0
- package/dist/permissions/rule.js +234 -0
- package/dist/permissions/settings.d.ts +71 -0
- package/dist/permissions/settings.js +202 -0
- package/dist/permissions/types.d.ts +61 -0
- package/dist/permissions/types.js +14 -0
- package/dist/prompt/compose.d.ts +12 -0
- package/dist/prompt/compose.js +67 -0
- package/dist/prompt/environment.d.ts +12 -0
- package/dist/prompt/environment.js +38 -0
- package/dist/prompt/provider-prompts/anthropic.d.ts +1 -0
- package/dist/prompt/provider-prompts/anthropic.js +5 -0
- package/dist/prompt/provider-prompts/codex.d.ts +1 -0
- package/dist/prompt/provider-prompts/codex.js +5 -0
- package/dist/prompt/provider-prompts/default.d.ts +1 -0
- package/dist/prompt/provider-prompts/default.js +6 -0
- package/dist/prompt/provider-prompts/gemini.d.ts +1 -0
- package/dist/prompt/provider-prompts/gemini.js +5 -0
- package/dist/prompt/provider-prompts/gpt.d.ts +1 -0
- package/dist/prompt/provider-prompts/gpt.js +5 -0
- package/dist/prompt/reminders.d.ts +30 -0
- package/dist/prompt/reminders.js +164 -0
- package/dist/prompt/runtime.d.ts +12 -0
- package/dist/prompt/runtime.js +31 -0
- package/dist/prompt/skills.d.ts +2 -0
- package/dist/prompt/skills.js +4 -0
- package/dist/provider-openai-codex.d.ts +14 -0
- package/dist/provider-openai-codex.js +409 -0
- package/dist/provider-registry.d.ts +56 -0
- package/dist/provider-registry.js +244 -0
- package/dist/provider-transform.d.ts +10 -0
- package/dist/provider-transform.js +69 -0
- package/dist/provider.d.ts +31 -0
- package/dist/provider.js +269 -0
- package/dist/question/controller.d.ts +22 -0
- package/dist/question/controller.js +97 -0
- package/dist/question/index.d.ts +2 -0
- package/dist/question/index.js +2 -0
- package/dist/question/types.d.ts +42 -0
- package/dist/question/types.js +6 -0
- package/dist/session-log.d.ts +16 -0
- package/dist/session-log.js +267 -0
- package/dist/session-types.d.ts +55 -0
- package/dist/session-types.js +1 -0
- package/dist/session.d.ts +32 -0
- package/dist/session.js +135 -0
- package/dist/skills/discovery.d.ts +12 -0
- package/dist/skills/discovery.js +148 -0
- package/dist/skills/format.d.ts +2 -0
- package/dist/skills/format.js +47 -0
- package/dist/skills/frontmatter.d.ts +5 -0
- package/dist/skills/frontmatter.js +60 -0
- package/dist/skills/invocation.d.ts +8 -0
- package/dist/skills/invocation.js +51 -0
- package/dist/skills/registry.d.ts +17 -0
- package/dist/skills/registry.js +42 -0
- package/dist/skills/types.d.ts +32 -0
- package/dist/skills/types.js +1 -0
- package/dist/slash-commands/commands.d.ts +7 -0
- package/dist/slash-commands/commands.js +779 -0
- package/dist/slash-commands/index.d.ts +4 -0
- package/dist/slash-commands/index.js +8 -0
- package/dist/slash-commands/registry.d.ts +31 -0
- package/dist/slash-commands/registry.js +70 -0
- package/dist/slash-commands/types.d.ts +44 -0
- package/dist/slash-commands/types.js +1 -0
- package/dist/slash-commands/unified.d.ts +38 -0
- package/dist/slash-commands/unified.js +38 -0
- package/dist/system-prompt.d.ts +34 -0
- package/dist/system-prompt.js +7 -0
- package/dist/tools/bash.d.ts +6 -0
- package/dist/tools/bash.js +135 -0
- package/dist/tools/edit.d.ts +16 -0
- package/dist/tools/edit.js +95 -0
- package/dist/tools/exa-mcp.d.ts +3 -0
- package/dist/tools/exa-mcp.js +74 -0
- package/dist/tools/exit-plan-mode.d.ts +17 -0
- package/dist/tools/exit-plan-mode.js +68 -0
- package/dist/tools/glob.d.ts +5 -0
- package/dist/tools/glob.js +129 -0
- package/dist/tools/grep.d.ts +5 -0
- package/dist/tools/grep.js +111 -0
- package/dist/tools/index.d.ts +36 -0
- package/dist/tools/index.js +59 -0
- package/dist/tools/lsp.d.ts +4 -0
- package/dist/tools/lsp.js +92 -0
- package/dist/tools/memory.d.ts +3 -0
- package/dist/tools/memory.js +90 -0
- package/dist/tools/question.d.ts +3 -0
- package/dist/tools/question.js +174 -0
- package/dist/tools/read.d.ts +7 -0
- package/dist/tools/read.js +83 -0
- package/dist/tools/sensitive-paths.d.ts +3 -0
- package/dist/tools/sensitive-paths.js +24 -0
- package/dist/tools/skill.d.ts +5 -0
- package/dist/tools/skill.js +51 -0
- package/dist/tools/task.d.ts +2 -0
- package/dist/tools/task.js +57 -0
- package/dist/tools/todo.d.ts +12 -0
- package/dist/tools/todo.js +151 -0
- package/dist/tools/tool-search.d.ts +23 -0
- package/dist/tools/tool-search.js +124 -0
- package/dist/tools/web-fetch.d.ts +6 -0
- package/dist/tools/web-fetch.js +75 -0
- package/dist/tools/web-search.d.ts +5 -0
- package/dist/tools/web-search.js +49 -0
- package/dist/tools/write.d.ts +11 -0
- package/dist/tools/write.js +77 -0
- package/dist/tui/display-history.d.ts +35 -0
- package/dist/tui/display-history.js +243 -0
- package/dist/tui/file-mentions.d.ts +29 -0
- package/dist/tui/file-mentions.js +174 -0
- package/dist/tui/image-paste.d.ts +54 -0
- package/dist/tui/image-paste.js +288 -0
- package/dist/tui/markdown-theme-rules.d.ts +23 -0
- package/dist/tui/markdown-theme-rules.js +164 -0
- package/dist/tui/markdown-theme.d.ts +5 -0
- package/dist/tui/markdown-theme.js +27 -0
- package/dist/tui/opencode-spinner.d.ts +21 -0
- package/dist/tui/opencode-spinner.js +216 -0
- package/dist/tui/prompt-keybindings.d.ts +41 -0
- package/dist/tui/prompt-keybindings.js +28 -0
- package/dist/tui/recent-activity.d.ts +8 -0
- package/dist/tui/recent-activity.js +71 -0
- package/dist/tui/run.d.ts +39 -0
- package/dist/tui/run.js +5696 -0
- package/dist/tui/sidebar-mcp.d.ts +31 -0
- package/dist/tui/sidebar-mcp.js +62 -0
- package/dist/tui/sidebar-state.d.ts +12 -0
- package/dist/tui/sidebar-state.js +69 -0
- package/dist/types.d.ts +219 -0
- package/dist/types.js +4 -0
- package/dist/variant/thinking-level.d.ts +5 -0
- package/dist/variant/thinking-level.js +25 -0
- package/dist/variant/variant-resolver.d.ts +4 -0
- package/dist/variant/variant-resolver.js +12 -0
- package/package.json +47 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool_search — meta-tool that loads schemas for deferred tools on demand.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Claude Code's ToolSearch. Two query modes:
|
|
5
|
+
*
|
|
6
|
+
* - "select:a,b,c" → fetch these exact tools by name (ignores ranking)
|
|
7
|
+
* - free text → rank deferred tools by substring match against name
|
|
8
|
+
* and description, return the top N
|
|
9
|
+
*
|
|
10
|
+
* Output is a <functions>…</functions> block containing one
|
|
11
|
+
* `<function>{schema JSON}</function>` per match — the same encoding the
|
|
12
|
+
* provider uses for the tool list at the top of the prompt. The matched
|
|
13
|
+
* tools are also unlocked on the agent so subsequent turns include them in
|
|
14
|
+
* the real tool list.
|
|
15
|
+
*/
|
|
16
|
+
export function createToolSearchTool(controller) {
|
|
17
|
+
return {
|
|
18
|
+
name: "tool_search",
|
|
19
|
+
readOnly: true,
|
|
20
|
+
description: 'Fetches full schema definitions for deferred tools so they can be called. ' +
|
|
21
|
+
'Deferred tools appear by name in <system-reminder> messages; their parameters are unknown ' +
|
|
22
|
+
'until loaded. Use this tool with query "select:<name>[,<name>...]" to load specific tools, ' +
|
|
23
|
+
'or with free-text keywords to search for relevant tools.',
|
|
24
|
+
parameters: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
query: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: 'Query to find deferred tools. Use "select:<name>,<name>" for direct selection, or keywords to search.',
|
|
30
|
+
},
|
|
31
|
+
max_results: {
|
|
32
|
+
type: "number",
|
|
33
|
+
description: "Maximum number of matches to return (default 5).",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: ["query"],
|
|
37
|
+
},
|
|
38
|
+
async execute(args) {
|
|
39
|
+
const query = typeof args.query === "string" ? args.query : "";
|
|
40
|
+
const maxResults = typeof args.max_results === "number" && args.max_results > 0
|
|
41
|
+
? Math.min(Math.floor(args.max_results), 25)
|
|
42
|
+
: 5;
|
|
43
|
+
const deferred = controller.listDeferred();
|
|
44
|
+
if (deferred.length === 0) {
|
|
45
|
+
return { content: "No deferred tools are registered in this session." };
|
|
46
|
+
}
|
|
47
|
+
let matches;
|
|
48
|
+
const selectPrefix = "select:";
|
|
49
|
+
if (query.startsWith(selectPrefix)) {
|
|
50
|
+
const names = new Set(query
|
|
51
|
+
.slice(selectPrefix.length)
|
|
52
|
+
.split(",")
|
|
53
|
+
.map((s) => s.trim())
|
|
54
|
+
.filter(Boolean));
|
|
55
|
+
matches = deferred.filter((t) => names.has(t.name));
|
|
56
|
+
if (matches.length === 0) {
|
|
57
|
+
const available = deferred.map((t) => t.name).join(", ");
|
|
58
|
+
return {
|
|
59
|
+
content: `No deferred tool matched select list. Known deferred tools: ${available}`,
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
matches = rankByKeywords(deferred, query).slice(0, maxResults);
|
|
66
|
+
if (matches.length === 0) {
|
|
67
|
+
return {
|
|
68
|
+
content: `No deferred tools matched "${query}". Use query "select:<name>" to fetch by exact name, or try different keywords.`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
controller.unlock(matches.map((t) => t.name));
|
|
73
|
+
const lines = ["<functions>"];
|
|
74
|
+
for (const tool of matches) {
|
|
75
|
+
const schema = {
|
|
76
|
+
description: tool.description,
|
|
77
|
+
name: tool.name,
|
|
78
|
+
parameters: tool.parameters,
|
|
79
|
+
};
|
|
80
|
+
lines.push(`<function>${JSON.stringify(schema)}</function>`);
|
|
81
|
+
}
|
|
82
|
+
lines.push("</functions>");
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push(`Loaded ${matches.length} tool${matches.length === 1 ? "" : "s"}. They are now available and callable on the next turn.`);
|
|
85
|
+
return { content: lines.join("\n") };
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function rankByKeywords(tools, rawQuery) {
|
|
90
|
+
const terms = rawQuery.toLowerCase().split(/\s+/).filter(Boolean);
|
|
91
|
+
if (terms.length === 0)
|
|
92
|
+
return [];
|
|
93
|
+
const requiredTerms = [];
|
|
94
|
+
const optionalTerms = [];
|
|
95
|
+
for (const term of terms) {
|
|
96
|
+
if (term.startsWith("+") && term.length > 1) {
|
|
97
|
+
requiredTerms.push(term.slice(1));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
optionalTerms.push(term);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const scored = [];
|
|
104
|
+
for (const tool of tools) {
|
|
105
|
+
const haystack = `${tool.name} ${tool.description}`.toLowerCase();
|
|
106
|
+
if (!requiredTerms.every((t) => haystack.includes(t)))
|
|
107
|
+
continue;
|
|
108
|
+
let score = 0;
|
|
109
|
+
for (const t of optionalTerms) {
|
|
110
|
+
if (tool.name.toLowerCase().includes(t))
|
|
111
|
+
score += 3;
|
|
112
|
+
if (tool.description.toLowerCase().includes(t))
|
|
113
|
+
score += 1;
|
|
114
|
+
}
|
|
115
|
+
if (requiredTerms.length > 0 && optionalTerms.length === 0) {
|
|
116
|
+
// Required-only query: every required term already matched, surface by name strength.
|
|
117
|
+
score = requiredTerms.reduce((acc, t) => acc + (tool.name.toLowerCase().includes(t) ? 3 : 1), 0);
|
|
118
|
+
}
|
|
119
|
+
if (score > 0)
|
|
120
|
+
scored.push({ tool, score });
|
|
121
|
+
}
|
|
122
|
+
scored.sort((a, b) => b.score - a.score);
|
|
123
|
+
return scored.map((s) => s.tool);
|
|
124
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web fetch tool - remote Exa MCP-backed page extraction.
|
|
3
|
+
*/
|
|
4
|
+
import type { ApprovalController } from "../approval/types.js";
|
|
5
|
+
import type { ToolRegistryEntry } from "../types.js";
|
|
6
|
+
export declare function createWebFetchTool(approval?: ApprovalController): ToolRegistryEntry;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web fetch tool - remote Exa MCP-backed page extraction.
|
|
3
|
+
*/
|
|
4
|
+
import { callExaMcpTool } from "./exa-mcp.js";
|
|
5
|
+
export function createWebFetchTool(approval) {
|
|
6
|
+
return {
|
|
7
|
+
name: "web_fetch",
|
|
8
|
+
readOnly: true,
|
|
9
|
+
description: "Fetch and extract the contents of a specific URL using a remote web crawling service.",
|
|
10
|
+
parameters: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
url: { type: "string", description: "The URL to fetch and extract" },
|
|
14
|
+
query: { type: "string", description: "Optional topic to focus the extracted summary/highlights on" },
|
|
15
|
+
maxCharacters: {
|
|
16
|
+
type: "number",
|
|
17
|
+
description: "Maximum number of characters to include in the extracted context",
|
|
18
|
+
},
|
|
19
|
+
livecrawl: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Live crawl mode",
|
|
22
|
+
enum: ["never", "fallback", "preferred", "always"],
|
|
23
|
+
},
|
|
24
|
+
livecrawlTimeout: {
|
|
25
|
+
type: "number",
|
|
26
|
+
description: "Maximum live crawl time in milliseconds",
|
|
27
|
+
},
|
|
28
|
+
subpages: {
|
|
29
|
+
type: "number",
|
|
30
|
+
description: "Optional number of linked subpages to fetch as well",
|
|
31
|
+
},
|
|
32
|
+
subpageTarget: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Optional guidance for which subpages matter most",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["url"],
|
|
38
|
+
},
|
|
39
|
+
async execute(args) {
|
|
40
|
+
const url = typeof args.url === "string" ? args.url.trim() : "";
|
|
41
|
+
if (!url) {
|
|
42
|
+
return { content: "Error: url is required", isError: true };
|
|
43
|
+
}
|
|
44
|
+
if (approval) {
|
|
45
|
+
const result = approval.checkRules({ tool: "WebFetch", url });
|
|
46
|
+
if (result.decision === "deny") {
|
|
47
|
+
return {
|
|
48
|
+
content: `Error: WebFetch blocked by deny rule: ${result.rule?.source ?? "<unknown>"} (${url})`,
|
|
49
|
+
isError: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const query = typeof args.query === "string" ? args.query.trim() : undefined;
|
|
54
|
+
return callExaMcpTool("crawling_exa", {
|
|
55
|
+
urls: [url],
|
|
56
|
+
livecrawl: typeof args.livecrawl === "string" ? args.livecrawl : "fallback",
|
|
57
|
+
...(typeof args.livecrawlTimeout === "number" ? { livecrawlTimeout: args.livecrawlTimeout } : {}),
|
|
58
|
+
...(typeof args.subpages === "number" ? { subpages: args.subpages } : {}),
|
|
59
|
+
...(typeof args.subpageTarget === "string" && args.subpageTarget.trim()
|
|
60
|
+
? { subpageTarget: args.subpageTarget.trim() }
|
|
61
|
+
: {}),
|
|
62
|
+
text: true,
|
|
63
|
+
context: typeof args.maxCharacters === "number" ? { maxCharacters: args.maxCharacters } : true,
|
|
64
|
+
summary: query ? { query } : true,
|
|
65
|
+
highlights: query
|
|
66
|
+
? {
|
|
67
|
+
query,
|
|
68
|
+
numSentences: 3,
|
|
69
|
+
highlightsPerUrl: 5,
|
|
70
|
+
}
|
|
71
|
+
: undefined,
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web search tool - remote Exa MCP-backed search.
|
|
3
|
+
*/
|
|
4
|
+
import { callExaMcpTool } from "./exa-mcp.js";
|
|
5
|
+
const DEFAULT_RESULTS = 8;
|
|
6
|
+
export function createWebSearchTool() {
|
|
7
|
+
return {
|
|
8
|
+
name: "web_search",
|
|
9
|
+
readOnly: true,
|
|
10
|
+
description: "Search the web using a remote search service and return current, structured results.",
|
|
11
|
+
parameters: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
query: { type: "string", description: "Web search query" },
|
|
15
|
+
numResults: { type: "number", description: `Number of search results to return (default: ${DEFAULT_RESULTS})` },
|
|
16
|
+
livecrawl: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Live crawl mode",
|
|
19
|
+
enum: ["fallback", "preferred"],
|
|
20
|
+
},
|
|
21
|
+
type: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Search type",
|
|
24
|
+
enum: ["auto", "fast", "deep"],
|
|
25
|
+
},
|
|
26
|
+
contextMaxCharacters: {
|
|
27
|
+
type: "number",
|
|
28
|
+
description: "Maximum number of characters to return in the result context",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ["query"],
|
|
32
|
+
},
|
|
33
|
+
async execute(args) {
|
|
34
|
+
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
35
|
+
if (!query) {
|
|
36
|
+
return { content: "Error: query is required", isError: true };
|
|
37
|
+
}
|
|
38
|
+
return callExaMcpTool("web_search_exa", {
|
|
39
|
+
query,
|
|
40
|
+
type: typeof args.type === "string" ? args.type : "auto",
|
|
41
|
+
numResults: typeof args.numResults === "number" ? args.numResults : DEFAULT_RESULTS,
|
|
42
|
+
livecrawl: typeof args.livecrawl === "string" ? args.livecrawl : "fallback",
|
|
43
|
+
...(typeof args.contextMaxCharacters === "number"
|
|
44
|
+
? { contextMaxCharacters: args.contextMaxCharacters }
|
|
45
|
+
: {}),
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write tool - create or overwrite files.
|
|
3
|
+
*/
|
|
4
|
+
import type { ApprovalController } from "../approval/types.js";
|
|
5
|
+
import type { ToolRegistryEntry } from "../types.js";
|
|
6
|
+
import { type LspService } from "../lsp/index.js";
|
|
7
|
+
export interface WriteToolOptions {
|
|
8
|
+
/** If true, refuse to overwrite existing files */
|
|
9
|
+
refuseOverwrite?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function createWriteTool(cwd: string, options?: WriteToolOptions, approval?: ApprovalController, lsp?: LspService): ToolRegistryEntry;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write tool - create or overwrite files.
|
|
3
|
+
*/
|
|
4
|
+
import { constants } from "node:fs";
|
|
5
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
7
|
+
import { createTwoFilesPatch } from "diff";
|
|
8
|
+
import { gateToolAction } from "../approval/tool-helper.js";
|
|
9
|
+
import { formatDiagnosticBlocks } from "../lsp/index.js";
|
|
10
|
+
export function createWriteTool(cwd, options = {}, approval, lsp) {
|
|
11
|
+
return {
|
|
12
|
+
name: "write",
|
|
13
|
+
description: `Write a file to disk. Creates parent directories if needed.${options.refuseOverwrite ? " Will not overwrite existing files." : ""}`,
|
|
14
|
+
parameters: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
path: { type: "string", description: "Path to the file (relative or absolute)" },
|
|
18
|
+
content: { type: "string", description: "File contents" },
|
|
19
|
+
},
|
|
20
|
+
required: ["path", "content"],
|
|
21
|
+
},
|
|
22
|
+
async execute(args) {
|
|
23
|
+
const filePath = resolve(cwd, args.path);
|
|
24
|
+
if (options.refuseOverwrite) {
|
|
25
|
+
try {
|
|
26
|
+
await access(filePath, constants.F_OK);
|
|
27
|
+
return {
|
|
28
|
+
content: `Error: File already exists: ${filePath}. Use edit tool to modify existing files.`,
|
|
29
|
+
isError: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// file doesn't exist, proceed
|
|
34
|
+
}
|
|
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());
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// LSP diagnostics should not turn a successful write into a failed tool call.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { content };
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return { content: `Error: ${err.message}`, isError: true };
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ToolResultMetadata } from "../types.js";
|
|
2
|
+
export interface CompactionMeta {
|
|
3
|
+
turns: number;
|
|
4
|
+
messages: number;
|
|
5
|
+
tokensSaved: number;
|
|
6
|
+
summarySections: Array<{
|
|
7
|
+
label: string;
|
|
8
|
+
content: string;
|
|
9
|
+
}>;
|
|
10
|
+
contextWindow?: number;
|
|
11
|
+
compactedAt: number;
|
|
12
|
+
}
|
|
13
|
+
export interface DisplayMessage {
|
|
14
|
+
role: "user" | "assistant" | "error";
|
|
15
|
+
content: string;
|
|
16
|
+
reasoning?: string;
|
|
17
|
+
toolCalls?: DisplayToolCall[];
|
|
18
|
+
status?: "thinking" | "responding";
|
|
19
|
+
streaming?: boolean;
|
|
20
|
+
syntheticKind?: "ui_compact_card";
|
|
21
|
+
hiddenCount?: number;
|
|
22
|
+
compactionMeta?: CompactionMeta;
|
|
23
|
+
}
|
|
24
|
+
export interface DisplayToolCall {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
args: Record<string, any>;
|
|
28
|
+
status?: "pending" | "running" | "completed" | "error";
|
|
29
|
+
result?: string;
|
|
30
|
+
isError?: boolean;
|
|
31
|
+
metadata?: ToolResultMetadata;
|
|
32
|
+
}
|
|
33
|
+
export declare function compactDisplayMessages(messages: DisplayMessage[]): DisplayMessage[];
|
|
34
|
+
export declare function truncateText(value: string, maxChars: number): string;
|
|
35
|
+
export declare function formatCompactNumber(n: number): string;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
const MAX_VISIBLE_MESSAGES = 80;
|
|
2
|
+
const FULL_DETAIL_WINDOW = 24;
|
|
3
|
+
const MAX_OLD_CONTENT_CHARS = 1200;
|
|
4
|
+
const MAX_OLD_REASONING_CHARS = 600;
|
|
5
|
+
const MAX_OLD_TOOL_RESULT_CHARS = 800;
|
|
6
|
+
const COMPACTION_SUMMARY_ITEMS = 6;
|
|
7
|
+
const COMPACTION_FILE_LIMIT = 8;
|
|
8
|
+
const TOOL_PATH_KEYS = ["file", "path", "paths", "filePath"];
|
|
9
|
+
export function compactDisplayMessages(messages) {
|
|
10
|
+
if (messages.length === 0) {
|
|
11
|
+
return messages;
|
|
12
|
+
}
|
|
13
|
+
let hiddenCount = 0;
|
|
14
|
+
let accumulatedTurns = 0;
|
|
15
|
+
let accumulatedTokens = 0;
|
|
16
|
+
const summarySections = [];
|
|
17
|
+
const withoutSynthetic = messages.filter((message) => {
|
|
18
|
+
if (message.syntheticKind !== "ui_compact_card") {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
hiddenCount += message.hiddenCount ?? 0;
|
|
22
|
+
if (message.compactionMeta) {
|
|
23
|
+
accumulatedTurns += message.compactionMeta.turns;
|
|
24
|
+
accumulatedTokens += message.compactionMeta.tokensSaved;
|
|
25
|
+
for (const section of message.compactionMeta.summarySections) {
|
|
26
|
+
summarySections.push(section);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
});
|
|
31
|
+
const overflow = Math.max(0, withoutSynthetic.length - MAX_VISIBLE_MESSAGES);
|
|
32
|
+
hiddenCount += overflow;
|
|
33
|
+
const visible = overflow > 0 ? withoutSynthetic.slice(overflow) : withoutSynthetic;
|
|
34
|
+
const detailStart = Math.max(0, visible.length - FULL_DETAIL_WINDOW);
|
|
35
|
+
const compacted = visible.map((message, index) => {
|
|
36
|
+
if (message.syntheticKind === "ui_compact_card") {
|
|
37
|
+
return message;
|
|
38
|
+
}
|
|
39
|
+
return index < detailStart ? compactDisplayMessage(message) : message;
|
|
40
|
+
});
|
|
41
|
+
if (hiddenCount === 0) {
|
|
42
|
+
return compacted;
|
|
43
|
+
}
|
|
44
|
+
const truncatedMessages = visible.slice(0, Math.max(1, detailStart));
|
|
45
|
+
const extractedMeta = extractCompactionMeta(truncatedMessages, hiddenCount, accumulatedTurns, accumulatedTokens, summarySections);
|
|
46
|
+
return [buildCompactCard(extractedMeta), ...compacted];
|
|
47
|
+
}
|
|
48
|
+
function extractCompactionMeta(truncatedMessages, hiddenCount, previousTurns, previousTokens, previousSections) {
|
|
49
|
+
const turnsInBatch = countUserTurns(truncatedMessages);
|
|
50
|
+
const totalTurns = previousTurns + turnsInBatch;
|
|
51
|
+
const messagesInBatch = truncatedMessages.length;
|
|
52
|
+
const totalMessages = hiddenCount;
|
|
53
|
+
const estimatedTokens = estimateTokenSavings(truncatedMessages);
|
|
54
|
+
const totalTokens = previousTokens + estimatedTokens;
|
|
55
|
+
const sections = [
|
|
56
|
+
...previousSections,
|
|
57
|
+
...extractSummarySections(truncatedMessages),
|
|
58
|
+
];
|
|
59
|
+
return {
|
|
60
|
+
turns: totalTurns,
|
|
61
|
+
messages: totalMessages,
|
|
62
|
+
tokensSaved: totalTokens > 0 ? totalTokens : estimatedTokens,
|
|
63
|
+
summarySections: mergeSummarySections(sections, COMPACTION_SUMMARY_ITEMS),
|
|
64
|
+
compactedAt: Date.now(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function countUserTurns(messages) {
|
|
68
|
+
return messages.filter((message) => message.role === "user").length;
|
|
69
|
+
}
|
|
70
|
+
function estimateTokenSavings(messages) {
|
|
71
|
+
let chars = 0;
|
|
72
|
+
for (const message of messages) {
|
|
73
|
+
chars += message.content.length;
|
|
74
|
+
chars += (message.reasoning?.length ?? 0);
|
|
75
|
+
for (const tool of message.toolCalls ?? []) {
|
|
76
|
+
chars += (tool.result?.length ?? 0);
|
|
77
|
+
chars += JSON.stringify(tool.args).length;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return Math.ceil(chars / 4);
|
|
81
|
+
}
|
|
82
|
+
function extractSummarySections(messages) {
|
|
83
|
+
const sections = [];
|
|
84
|
+
const userMessages = messages
|
|
85
|
+
.filter((m) => m.role === "user")
|
|
86
|
+
.map((m) => m.content);
|
|
87
|
+
if (userMessages.length > 0) {
|
|
88
|
+
sections.push({
|
|
89
|
+
label: "Progress",
|
|
90
|
+
content: userMessages.slice(0, 5).map((c) => `- ${shorten(c, 100)}`).join("\n"),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const assistantInsights = messages
|
|
94
|
+
.filter((m) => m.role === "assistant" && m.content.trim())
|
|
95
|
+
.map((m) => m.content.trim());
|
|
96
|
+
if (assistantInsights.length > 0) {
|
|
97
|
+
sections.push({
|
|
98
|
+
label: "Decisions",
|
|
99
|
+
content: assistantInsights.slice(0, 3).map((c) => `- ${shorten(c, 120)}`).join("\n"),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const files = collectFiles(messages);
|
|
103
|
+
if (files.length > 0) {
|
|
104
|
+
sections.push({
|
|
105
|
+
label: "Files",
|
|
106
|
+
content: files.slice(0, COMPACTION_FILE_LIMIT).join(", "),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const toolFindings = collectToolFindings(messages);
|
|
110
|
+
if (toolFindings.length > 0) {
|
|
111
|
+
sections.push({
|
|
112
|
+
label: "Tools",
|
|
113
|
+
content: toolFindings.slice(0, 5).map((f) => `- ${f}`).join("\n"),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return sections;
|
|
117
|
+
}
|
|
118
|
+
function collectFiles(messages) {
|
|
119
|
+
const files = new Set();
|
|
120
|
+
for (const message of messages) {
|
|
121
|
+
for (const tool of message.toolCalls ?? []) {
|
|
122
|
+
for (const key of TOOL_PATH_KEYS) {
|
|
123
|
+
const value = tool.args[key];
|
|
124
|
+
if (typeof value === "string" && value) {
|
|
125
|
+
files.add(value);
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(value)) {
|
|
128
|
+
for (const item of value) {
|
|
129
|
+
if (typeof item === "string" && item) {
|
|
130
|
+
files.add(item);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [...files].slice(0, COMPACTION_FILE_LIMIT);
|
|
138
|
+
}
|
|
139
|
+
function collectToolFindings(messages) {
|
|
140
|
+
const findings = [];
|
|
141
|
+
for (const message of messages) {
|
|
142
|
+
for (const tool of message.toolCalls ?? []) {
|
|
143
|
+
if (tool.result && tool.result.length > 0) {
|
|
144
|
+
findings.push(`${tool.name}: ${shorten(tool.result, 80)}`);
|
|
145
|
+
if (findings.length >= 10)
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (findings.length >= 10)
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
return findings;
|
|
153
|
+
}
|
|
154
|
+
function mergeSummarySections(sections, maxItems) {
|
|
155
|
+
const merged = new Map();
|
|
156
|
+
for (const section of sections) {
|
|
157
|
+
const existing = merged.get(section.label);
|
|
158
|
+
if (existing) {
|
|
159
|
+
merged.set(section.label, `${existing}\n${section.content}`);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
merged.set(section.label, section.content);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return [...merged.entries()]
|
|
166
|
+
.map(([label, content]) => ({ label, content }))
|
|
167
|
+
.slice(0, maxItems);
|
|
168
|
+
}
|
|
169
|
+
function buildCompactCard(meta) {
|
|
170
|
+
const formatNum = (n) => {
|
|
171
|
+
if (n >= 1_000_000)
|
|
172
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
173
|
+
if (n >= 1_000)
|
|
174
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
175
|
+
return String(n);
|
|
176
|
+
};
|
|
177
|
+
const parts = [];
|
|
178
|
+
if (meta.turns > 0) {
|
|
179
|
+
parts.push(`${meta.turns} turn${meta.turns === 1 ? "" : "s"}`);
|
|
180
|
+
}
|
|
181
|
+
if (meta.messages > 0) {
|
|
182
|
+
parts.push(`${meta.messages} message${meta.messages === 1 ? "" : "s"}`);
|
|
183
|
+
}
|
|
184
|
+
if (meta.tokensSaved > 0) {
|
|
185
|
+
parts.push(`~${formatNum(meta.tokensSaved)} tokens`);
|
|
186
|
+
}
|
|
187
|
+
const statsLine = parts.length > 0 ? `┃ ${parts.join(" · ")}` : "";
|
|
188
|
+
const sectionLines = [];
|
|
189
|
+
for (const section of meta.summarySections) {
|
|
190
|
+
sectionLines.push(`┃ ${section.label}: ${section.content.split("\n")[0]}`);
|
|
191
|
+
}
|
|
192
|
+
const content = [statsLine, ...sectionLines].filter(Boolean).join("\n");
|
|
193
|
+
return {
|
|
194
|
+
role: "assistant",
|
|
195
|
+
content,
|
|
196
|
+
syntheticKind: "ui_compact_card",
|
|
197
|
+
hiddenCount: meta.messages,
|
|
198
|
+
compactionMeta: meta,
|
|
199
|
+
status: "responding",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function compactDisplayMessage(message) {
|
|
203
|
+
if (message.syntheticKind === "ui_compact_card") {
|
|
204
|
+
return message;
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
...message,
|
|
208
|
+
content: truncateText(message.content, MAX_OLD_CONTENT_CHARS),
|
|
209
|
+
reasoning: message.reasoning
|
|
210
|
+
? truncateText(message.reasoning, MAX_OLD_REASONING_CHARS)
|
|
211
|
+
: message.reasoning,
|
|
212
|
+
toolCalls: message.toolCalls?.map((toolCall) => ({
|
|
213
|
+
...toolCall,
|
|
214
|
+
result: toolCall.result
|
|
215
|
+
? truncateText(toolCall.result, MAX_OLD_TOOL_RESULT_CHARS)
|
|
216
|
+
: toolCall.result,
|
|
217
|
+
})),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
export function truncateText(value, maxChars) {
|
|
221
|
+
if (value.length <= maxChars) {
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
const head = Math.max(1, Math.floor(maxChars * 0.7));
|
|
225
|
+
const tail = Math.max(1, maxChars - head - 32);
|
|
226
|
+
const omitted = value.length - head - tail;
|
|
227
|
+
const separator = "─".repeat(12);
|
|
228
|
+
return `${value.slice(0, head)}\n${separator} ✂ ${omitted} chars truncated ${separator}\n${value.slice(-tail)}`;
|
|
229
|
+
}
|
|
230
|
+
function shorten(text, maxChars) {
|
|
231
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
232
|
+
if (normalized.length <= maxChars) {
|
|
233
|
+
return normalized;
|
|
234
|
+
}
|
|
235
|
+
return `${normalized.slice(0, maxChars - 1)}…`;
|
|
236
|
+
}
|
|
237
|
+
export function formatCompactNumber(n) {
|
|
238
|
+
if (n >= 1_000_000)
|
|
239
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
240
|
+
if (n >= 1_000)
|
|
241
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
242
|
+
return String(n);
|
|
243
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface AtContext {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
query: string;
|
|
5
|
+
}
|
|
6
|
+
export interface FileSuggestion {
|
|
7
|
+
path: string;
|
|
8
|
+
score: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ExpandedMention {
|
|
11
|
+
path: string;
|
|
12
|
+
bytes: number;
|
|
13
|
+
truncated: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ExpandResult {
|
|
16
|
+
text: string;
|
|
17
|
+
expanded: ExpandedMention[];
|
|
18
|
+
missing: string[];
|
|
19
|
+
skipped: Array<{
|
|
20
|
+
path: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
bytes?: number;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export declare function findAtContext(text: string, cursor: number): AtContext | null;
|
|
26
|
+
export declare function filterFileSuggestions(files: string[], query: string, limit?: number): FileSuggestion[];
|
|
27
|
+
export declare function listProjectFiles(cwd: string): Promise<string[]>;
|
|
28
|
+
export declare function invalidateFileListCache(cwd?: string): void;
|
|
29
|
+
export declare function expandAtMentions(text: string, cwd: string): Promise<ExpandResult>;
|