@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,8 @@
|
|
|
1
|
+
import { SlashCommandRegistry } from "./registry.js";
|
|
2
|
+
import { builtinSlashCommands } from "./commands.js";
|
|
3
|
+
export const registry = new SlashCommandRegistry();
|
|
4
|
+
for (const cmd of builtinSlashCommands) {
|
|
5
|
+
registry.register(cmd);
|
|
6
|
+
}
|
|
7
|
+
export * from "./types.js";
|
|
8
|
+
export * from "./registry.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SlashCommand, SlashCommandContext } from "./types.js";
|
|
2
|
+
import { type CommandSource, type UnifiedCommand } from "./unified.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dynamic source: called at lookup time to produce extra commands (e.g. MCP
|
|
5
|
+
* prompts loaded from a server after connect). The registry only keeps the
|
|
6
|
+
* callback — commands are never cached, so re-registering after a reconnect
|
|
7
|
+
* just works.
|
|
8
|
+
*
|
|
9
|
+
* Sources may return bare SlashCommand objects for backwards compatibility;
|
|
10
|
+
* the registry treats unlabelled commands as source: "builtin". MCP's dynamic
|
|
11
|
+
* source returns UnifiedCommand with source: "mcp".
|
|
12
|
+
*/
|
|
13
|
+
export type DynamicSource = () => SlashCommand[];
|
|
14
|
+
export declare class SlashCommandRegistry {
|
|
15
|
+
private commands;
|
|
16
|
+
private dynamicSources;
|
|
17
|
+
register(cmd: SlashCommand): void;
|
|
18
|
+
addDynamicSource(source: DynamicSource): void;
|
|
19
|
+
get(name: string): UnifiedCommand | undefined;
|
|
20
|
+
list(): UnifiedCommand[];
|
|
21
|
+
/**
|
|
22
|
+
* Convenience filter used by UI code that wants to group by source
|
|
23
|
+
* (e.g. builtin first, then mcp) without filtering in-line every time.
|
|
24
|
+
*/
|
|
25
|
+
listBySource(source: CommandSource): UnifiedCommand[];
|
|
26
|
+
execute(input: string, ctx: SlashCommandContext): Promise<{
|
|
27
|
+
handled: boolean;
|
|
28
|
+
result?: string;
|
|
29
|
+
inject?: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { asUnified } from "./unified.js";
|
|
2
|
+
export class SlashCommandRegistry {
|
|
3
|
+
commands = new Map();
|
|
4
|
+
dynamicSources = [];
|
|
5
|
+
register(cmd) {
|
|
6
|
+
this.commands.set(cmd.name, asUnified(cmd, "builtin"));
|
|
7
|
+
}
|
|
8
|
+
addDynamicSource(source) {
|
|
9
|
+
this.dynamicSources.push(source);
|
|
10
|
+
}
|
|
11
|
+
get(name) {
|
|
12
|
+
const builtin = this.commands.get(name);
|
|
13
|
+
if (builtin)
|
|
14
|
+
return builtin;
|
|
15
|
+
for (const source of this.dynamicSources) {
|
|
16
|
+
for (const cmd of source()) {
|
|
17
|
+
if (cmd.name === name)
|
|
18
|
+
return asUnified(cmd);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
list() {
|
|
24
|
+
const out = [...this.commands.values()];
|
|
25
|
+
for (const source of this.dynamicSources) {
|
|
26
|
+
for (const cmd of source()) {
|
|
27
|
+
out.push(asUnified(cmd));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Convenience filter used by UI code that wants to group by source
|
|
34
|
+
* (e.g. builtin first, then mcp) without filtering in-line every time.
|
|
35
|
+
*/
|
|
36
|
+
listBySource(source) {
|
|
37
|
+
return this.list().filter((cmd) => cmd.source === source);
|
|
38
|
+
}
|
|
39
|
+
async execute(input, ctx) {
|
|
40
|
+
if (!input.startsWith("/"))
|
|
41
|
+
return { handled: false };
|
|
42
|
+
const spaceIndex = input.indexOf(" ");
|
|
43
|
+
const name = spaceIndex === -1 ? input.slice(1) : input.slice(1, spaceIndex);
|
|
44
|
+
const args = spaceIndex === -1 ? "" : input.slice(spaceIndex + 1).trim();
|
|
45
|
+
const cmd = this.get(name);
|
|
46
|
+
if (!cmd) {
|
|
47
|
+
const skill = ctx.skillRegistry.get(name);
|
|
48
|
+
if (skill) {
|
|
49
|
+
return {
|
|
50
|
+
handled: true,
|
|
51
|
+
result: `Skill "${skill.meta.name}": ${skill.meta.description}\nUse /${skill.meta.name} <your request> to run with this skill, or /skill ${skill.meta.name} to inspect it.`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
handled: true,
|
|
56
|
+
result: `Unknown command: /${name}. Use /help to see available commands.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const output = await cmd.handler(args, ctx);
|
|
61
|
+
if (output && typeof output === "object" && "inject" in output) {
|
|
62
|
+
return { handled: true, inject: output.inject };
|
|
63
|
+
}
|
|
64
|
+
return { handled: true, result: typeof output === "string" ? output : undefined };
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { handled: true, result: `Error: ${err.message || String(err)}` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Agent } from "../agent.js";
|
|
2
|
+
import type { SessionManager } from "../session.js";
|
|
3
|
+
import type { Provider } from "../types.js";
|
|
4
|
+
import type { ProviderRegistry } from "../provider-registry.js";
|
|
5
|
+
import type { SkillRegistry } from "../skills/registry.js";
|
|
6
|
+
import type { BashAllowlist } from "../approval/session-cache.js";
|
|
7
|
+
import type { SettingsManager } from "../permissions/settings.js";
|
|
8
|
+
import type { McpManager } from "../mcp/manager.js";
|
|
9
|
+
import type { LspService } from "../lsp/index.js";
|
|
10
|
+
import type { MemoryScope } from "../memory/index.js";
|
|
11
|
+
export interface SlashCommandContext {
|
|
12
|
+
agent: Agent;
|
|
13
|
+
addMessage: (role: "user" | "assistant" | "error", content: string) => void;
|
|
14
|
+
clearMessages: () => void;
|
|
15
|
+
cwd: string;
|
|
16
|
+
exit: () => void;
|
|
17
|
+
sessionManager?: SessionManager;
|
|
18
|
+
createProvider: (providerId: string, apiKey: string, baseURL: string) => Provider;
|
|
19
|
+
openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout", providerId?: string) => void;
|
|
20
|
+
registry: ProviderRegistry;
|
|
21
|
+
skillRegistry: SkillRegistry;
|
|
22
|
+
bashAllowlist?: BashAllowlist;
|
|
23
|
+
settingsManager?: SettingsManager;
|
|
24
|
+
mcpManager?: McpManager;
|
|
25
|
+
lspService?: LspService;
|
|
26
|
+
flushMemory?: () => Promise<void>;
|
|
27
|
+
runMemoryCompaction?: () => Promise<string>;
|
|
28
|
+
runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
|
|
29
|
+
runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Return types for a slash command handler:
|
|
33
|
+
* - string | void: the string (if any) is displayed as an assistant message
|
|
34
|
+
* - { inject }: the content is sent to the agent as the user's next turn
|
|
35
|
+
* (used by MCP prompts that expand a template into a user message)
|
|
36
|
+
*/
|
|
37
|
+
export type SlashCommandOutput = string | void | {
|
|
38
|
+
inject: string;
|
|
39
|
+
};
|
|
40
|
+
export interface SlashCommand {
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
handler: (args: string, ctx: SlashCommandContext) => Promise<SlashCommandOutput>;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UnifiedCommand — a SlashCommand annotated with its source.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors opencode's Command.Info.source field so consumers (TUI autocomplete,
|
|
5
|
+
* /help, /mcp list) can group and label commands uniformly without knowing
|
|
6
|
+
* where each one came from.
|
|
7
|
+
*
|
|
8
|
+
* Today only "builtin" and "mcp" are produced. A future "skill" source is
|
|
9
|
+
* reserved in the type so we don't need another migration when skills move
|
|
10
|
+
* into this abstraction.
|
|
11
|
+
*/
|
|
12
|
+
import type { SlashCommand } from "./types.js";
|
|
13
|
+
export type CommandSource = "builtin" | "mcp" | "skill";
|
|
14
|
+
export interface UnifiedCommand extends SlashCommand {
|
|
15
|
+
source: CommandSource;
|
|
16
|
+
/**
|
|
17
|
+
* For source === "mcp" this is the server name. Used by the TUI to render
|
|
18
|
+
* a ":mcp" badge and by /mcp list to group prompts under their server.
|
|
19
|
+
*/
|
|
20
|
+
sourceLabel?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function isUnifiedCommand(cmd: SlashCommand): cmd is UnifiedCommand;
|
|
23
|
+
/**
|
|
24
|
+
* Wrap a bare SlashCommand into a UnifiedCommand, defaulting to builtin.
|
|
25
|
+
* Used by the registry so callers that still pass plain SlashCommand objects
|
|
26
|
+
* (tests, older code paths) keep working.
|
|
27
|
+
*/
|
|
28
|
+
export declare function asUnified(cmd: SlashCommand, source?: CommandSource): UnifiedCommand;
|
|
29
|
+
/**
|
|
30
|
+
* Stable source ordering used by UIs that group commands (e.g. the slash
|
|
31
|
+
* autocomplete). Lower rank renders first.
|
|
32
|
+
*
|
|
33
|
+
* builtin → skill → mcp
|
|
34
|
+
*
|
|
35
|
+
* Tuned so that "native" commands dominate the palette and user-installed
|
|
36
|
+
* MCP servers appear after, echoing opencode's autocomplete layout.
|
|
37
|
+
*/
|
|
38
|
+
export declare function sourceRank(source: CommandSource | undefined): number;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UnifiedCommand — a SlashCommand annotated with its source.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors opencode's Command.Info.source field so consumers (TUI autocomplete,
|
|
5
|
+
* /help, /mcp list) can group and label commands uniformly without knowing
|
|
6
|
+
* where each one came from.
|
|
7
|
+
*
|
|
8
|
+
* Today only "builtin" and "mcp" are produced. A future "skill" source is
|
|
9
|
+
* reserved in the type so we don't need another migration when skills move
|
|
10
|
+
* into this abstraction.
|
|
11
|
+
*/
|
|
12
|
+
export function isUnifiedCommand(cmd) {
|
|
13
|
+
return typeof cmd.source === "string";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wrap a bare SlashCommand into a UnifiedCommand, defaulting to builtin.
|
|
17
|
+
* Used by the registry so callers that still pass plain SlashCommand objects
|
|
18
|
+
* (tests, older code paths) keep working.
|
|
19
|
+
*/
|
|
20
|
+
export function asUnified(cmd, source = "builtin") {
|
|
21
|
+
return isUnifiedCommand(cmd) ? cmd : { ...cmd, source };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Stable source ordering used by UIs that group commands (e.g. the slash
|
|
25
|
+
* autocomplete). Lower rank renders first.
|
|
26
|
+
*
|
|
27
|
+
* builtin → skill → mcp
|
|
28
|
+
*
|
|
29
|
+
* Tuned so that "native" commands dominate the palette and user-installed
|
|
30
|
+
* MCP servers appear after, echoing opencode's autocomplete layout.
|
|
31
|
+
*/
|
|
32
|
+
export function sourceRank(source) {
|
|
33
|
+
if (!source || source === "builtin")
|
|
34
|
+
return 0;
|
|
35
|
+
if (source === "skill")
|
|
36
|
+
return 1;
|
|
37
|
+
return 2; // "mcp"
|
|
38
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backward-compatible system prompt wrapper.
|
|
3
|
+
*/
|
|
4
|
+
import type { PermissionMode, ThinkingLevel } from "./types.js";
|
|
5
|
+
import type { SkillSummary } from "./skills/types.js";
|
|
6
|
+
export interface SystemPromptOptions {
|
|
7
|
+
/** Agent display name */
|
|
8
|
+
agentName?: string;
|
|
9
|
+
/** Configured provider id */
|
|
10
|
+
configuredProvider?: string;
|
|
11
|
+
/** Configured model name */
|
|
12
|
+
configuredModel?: string;
|
|
13
|
+
/** Full configured model id */
|
|
14
|
+
configuredModelId?: string;
|
|
15
|
+
/** Names of available tools */
|
|
16
|
+
tools?: string[];
|
|
17
|
+
/** One-line description for each tool */
|
|
18
|
+
toolSnippets?: Record<string, string>;
|
|
19
|
+
/** Extra guidelines */
|
|
20
|
+
guidelines?: string[];
|
|
21
|
+
/** Working directory to include in prompt */
|
|
22
|
+
workingDir?: string;
|
|
23
|
+
/** Current thinking level */
|
|
24
|
+
thinkingLevel?: ThinkingLevel;
|
|
25
|
+
/** Current agent mode (default or plan) */
|
|
26
|
+
mode?: PermissionMode;
|
|
27
|
+
/** Current date override */
|
|
28
|
+
currentDate?: string;
|
|
29
|
+
/** Prompt-visible skills */
|
|
30
|
+
skills?: SkillSummary[];
|
|
31
|
+
/** Prompt-visible memory guidance and summaries */
|
|
32
|
+
memoryPrompt?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function buildSystemPrompt(options?: SystemPromptOptions): string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash tool - execute shell commands with streaming capture.
|
|
3
|
+
*/
|
|
4
|
+
import type { ApprovalController } from "../approval/types.js";
|
|
5
|
+
import type { ToolRegistryEntry } from "../types.js";
|
|
6
|
+
export declare function createBashTool(cwd: string, approval?: ApprovalController): ToolRegistryEntry;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash tool - execute shell commands with streaming capture.
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { platform } from "node:os";
|
|
7
|
+
import { gateToolAction } from "../approval/tool-helper.js";
|
|
8
|
+
import { parseSearchBashCommand } from "../agent/tool-intent.js";
|
|
9
|
+
import { referencesSensitivePath } from "./sensitive-paths.js";
|
|
10
|
+
const MAX_OUTPUT = 50 * 1024;
|
|
11
|
+
export function createBashTool(cwd, approval) {
|
|
12
|
+
return {
|
|
13
|
+
name: "bash",
|
|
14
|
+
description: "Execute a bash command in the working directory. Use timeout for long-running commands.",
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
command: { type: "string", description: "Bash command to execute" },
|
|
19
|
+
timeout: { type: "number", description: "Timeout in seconds (optional)" },
|
|
20
|
+
},
|
|
21
|
+
required: ["command"],
|
|
22
|
+
},
|
|
23
|
+
async execute(args) {
|
|
24
|
+
if (!existsSync(cwd)) {
|
|
25
|
+
return { content: `Error: Working directory does not exist: ${cwd}`, isError: true };
|
|
26
|
+
}
|
|
27
|
+
const command = String(args.command);
|
|
28
|
+
const timeoutSec = typeof args.timeout === "number" ? args.timeout : 60;
|
|
29
|
+
const parsedSearch = parseSearchBashCommand(command);
|
|
30
|
+
if (referencesSensitivePath(command)) {
|
|
31
|
+
return {
|
|
32
|
+
content: "Error: Bash access to sensitive credential storage is blocked.",
|
|
33
|
+
isError: true,
|
|
34
|
+
status: "blocked",
|
|
35
|
+
metadata: {
|
|
36
|
+
kind: "security",
|
|
37
|
+
reason: "Sensitive credential storage is not accessible from general-purpose bash commands.",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const gate = await gateToolAction(approval, { type: "bash", command, cwd });
|
|
42
|
+
if (!gate.approved)
|
|
43
|
+
return gate.result;
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const shell = platform() === "win32" ? "cmd.exe" : "bash";
|
|
46
|
+
const shellArgs = platform() === "win32" ? ["/c", command] : ["-c", command];
|
|
47
|
+
const child = spawn(shell, shellArgs, {
|
|
48
|
+
cwd,
|
|
49
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
50
|
+
env: process.env,
|
|
51
|
+
});
|
|
52
|
+
let stdout = "";
|
|
53
|
+
let stderr = "";
|
|
54
|
+
let timedOut = false;
|
|
55
|
+
const timeoutHandle = setTimeout(() => {
|
|
56
|
+
timedOut = true;
|
|
57
|
+
child.kill("SIGTERM");
|
|
58
|
+
setTimeout(() => child.kill("SIGKILL"), 5000);
|
|
59
|
+
}, timeoutSec * 1000);
|
|
60
|
+
child.stdout?.on("data", (data) => {
|
|
61
|
+
stdout += data.toString();
|
|
62
|
+
});
|
|
63
|
+
child.stderr?.on("data", (data) => {
|
|
64
|
+
stderr += data.toString();
|
|
65
|
+
});
|
|
66
|
+
child.on("error", (err) => {
|
|
67
|
+
clearTimeout(timeoutHandle);
|
|
68
|
+
resolve({ content: `Error: ${err.message}`, isError: true });
|
|
69
|
+
});
|
|
70
|
+
child.on("close", (code) => {
|
|
71
|
+
clearTimeout(timeoutHandle);
|
|
72
|
+
let output = "";
|
|
73
|
+
if (stdout)
|
|
74
|
+
output += `stdout:\n${stdout}\n`;
|
|
75
|
+
if (stderr)
|
|
76
|
+
output += `stderr:\n${stderr}\n`;
|
|
77
|
+
if (output === "")
|
|
78
|
+
output = "(no output)\n";
|
|
79
|
+
if (timedOut) {
|
|
80
|
+
output += `[Command timed out after ${timeoutSec}s]`;
|
|
81
|
+
resolve({
|
|
82
|
+
content: output.trim(),
|
|
83
|
+
isError: true,
|
|
84
|
+
status: "timeout",
|
|
85
|
+
metadata: {
|
|
86
|
+
kind: parsedSearch ? "search" : "shell",
|
|
87
|
+
pattern: parsedSearch?.pattern,
|
|
88
|
+
path: parsedSearch?.path,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (Buffer.byteLength(output, "utf-8") > MAX_OUTPUT) {
|
|
94
|
+
output = Buffer.from(output, "utf-8").subarray(0, MAX_OUTPUT).toString("utf-8");
|
|
95
|
+
output += "\n[Output truncated]";
|
|
96
|
+
}
|
|
97
|
+
const normalizedOutput = output.trim();
|
|
98
|
+
if (parsedSearch && code === 1 && !stderr.trim()) {
|
|
99
|
+
resolve({
|
|
100
|
+
content: normalizedOutput === "(no output)" ? "stdout:\n(no matches)" : normalizedOutput,
|
|
101
|
+
isError: false,
|
|
102
|
+
status: "no_match",
|
|
103
|
+
metadata: {
|
|
104
|
+
kind: "search",
|
|
105
|
+
pattern: parsedSearch.pattern,
|
|
106
|
+
path: parsedSearch.path,
|
|
107
|
+
matches: 0,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const isError = code !== 0;
|
|
113
|
+
resolve({
|
|
114
|
+
content: normalizedOutput,
|
|
115
|
+
isError,
|
|
116
|
+
status: isError ? "command_error" : "success",
|
|
117
|
+
metadata: {
|
|
118
|
+
kind: parsedSearch ? "search" : "shell",
|
|
119
|
+
pattern: parsedSearch?.pattern,
|
|
120
|
+
path: parsedSearch?.path,
|
|
121
|
+
matches: parsedSearch ? countSearchMatches(stdout) : undefined,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function countSearchMatches(stdout) {
|
|
130
|
+
return stdout
|
|
131
|
+
.split("\n")
|
|
132
|
+
.map((line) => line.trim())
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.length;
|
|
135
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit tool - targeted string replacements with diff validation.
|
|
3
|
+
*
|
|
4
|
+
* This is the safest way to edit files: old_string must exist exactly once.
|
|
5
|
+
*/
|
|
6
|
+
import type { ApprovalController } from "../approval/types.js";
|
|
7
|
+
import type { ToolRegistryEntry } from "../types.js";
|
|
8
|
+
import { type LspService } from "../lsp/index.js";
|
|
9
|
+
export interface EditArgs {
|
|
10
|
+
path: string;
|
|
11
|
+
edits: Array<{
|
|
12
|
+
oldText: string;
|
|
13
|
+
newText: string;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createEditTool(cwd: string, approval?: ApprovalController, lsp?: LspService): ToolRegistryEntry;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit tool - targeted string replacements with diff validation.
|
|
3
|
+
*
|
|
4
|
+
* This is the safest way to edit files: old_string must exist exactly once.
|
|
5
|
+
*/
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { createTwoFilesPatch } from "diff";
|
|
10
|
+
import { gateToolAction } from "../approval/tool-helper.js";
|
|
11
|
+
import { formatDiagnosticBlocks } from "../lsp/index.js";
|
|
12
|
+
export function createEditTool(cwd, approval, lsp) {
|
|
13
|
+
return {
|
|
14
|
+
name: "edit",
|
|
15
|
+
description: "Apply targeted string replacements to a file. Each oldText must match exactly once. All edits apply to the original file contents simultaneously.",
|
|
16
|
+
parameters: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
path: { type: "string", description: "Path to the file" },
|
|
20
|
+
edits: {
|
|
21
|
+
type: "array",
|
|
22
|
+
description: "List of replacements. Each oldText must be unique in the file.",
|
|
23
|
+
items: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
oldText: { type: "string", description: "Exact text to replace" },
|
|
27
|
+
newText: { type: "string", description: "Replacement text" },
|
|
28
|
+
},
|
|
29
|
+
required: ["oldText", "newText"],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
required: ["path", "edits"],
|
|
34
|
+
},
|
|
35
|
+
async execute(args) {
|
|
36
|
+
const filePath = resolve(cwd, args.path);
|
|
37
|
+
try {
|
|
38
|
+
await access(filePath, constants.R_OK | constants.W_OK);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { content: `Error: Cannot read/write file: ${filePath}`, isError: true };
|
|
42
|
+
}
|
|
43
|
+
const original = await readFile(filePath, "utf-8");
|
|
44
|
+
let content = original;
|
|
45
|
+
const edits = args.edits;
|
|
46
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
47
|
+
return { content: "Error: No edits provided", isError: true };
|
|
48
|
+
}
|
|
49
|
+
// Validate each oldText exists exactly once
|
|
50
|
+
for (const edit of edits) {
|
|
51
|
+
const count = content.split(edit.oldText).length - 1;
|
|
52
|
+
if (count === 0) {
|
|
53
|
+
return {
|
|
54
|
+
content: `Error: oldText not found in file: "${edit.oldText.slice(0, 50)}..."`,
|
|
55
|
+
isError: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (count > 1) {
|
|
59
|
+
return {
|
|
60
|
+
content: `Error: oldText appears ${count} times in file. Must be unique: "${edit.oldText.slice(0, 50)}..."`,
|
|
61
|
+
isError: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Apply all edits in-memory to compute the proposed next content + diff.
|
|
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) {
|
|
82
|
+
try {
|
|
83
|
+
await lsp.touchFile(filePath, "document");
|
|
84
|
+
output += formatDiagnosticBlocks(cwd, filePath, lsp.diagnostics());
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// LSP diagnostics should not turn a successful edit into a failed tool call.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
content: output,
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = "https://mcp.exa.ai/mcp";
|
|
2
|
+
export function resolveExaMcpUrl() {
|
|
3
|
+
const baseUrl = (process.env.BUBBLE_WEB_SEARCH_URL || DEFAULT_BASE_URL).trim();
|
|
4
|
+
const url = new URL(baseUrl);
|
|
5
|
+
const enabledTools = new Set((url.searchParams.get("tools") || "web_search_exa,crawling_exa")
|
|
6
|
+
.split(",")
|
|
7
|
+
.map((item) => item.trim())
|
|
8
|
+
.filter(Boolean));
|
|
9
|
+
enabledTools.add("web_search_exa");
|
|
10
|
+
enabledTools.add("crawling_exa");
|
|
11
|
+
url.searchParams.set("tools", [...enabledTools].join(","));
|
|
12
|
+
const apiKey = process.env.BUBBLE_EXA_API_KEY?.trim();
|
|
13
|
+
if (apiKey && !url.searchParams.get("exaApiKey")) {
|
|
14
|
+
url.searchParams.set("exaApiKey", apiKey);
|
|
15
|
+
}
|
|
16
|
+
return url.toString();
|
|
17
|
+
}
|
|
18
|
+
export async function callExaMcpTool(name, args) {
|
|
19
|
+
const response = await fetch(resolveExaMcpUrl(), {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
Accept: "application/json, text/event-stream",
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
"User-Agent": "bubble",
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
jsonrpc: "2.0",
|
|
28
|
+
id: 1,
|
|
29
|
+
method: "tools/call",
|
|
30
|
+
params: {
|
|
31
|
+
name,
|
|
32
|
+
arguments: args,
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
}).catch((error) => {
|
|
36
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const errorText = await response.text().catch(() => "");
|
|
40
|
+
return {
|
|
41
|
+
content: `Error: Remote MCP request failed with status ${response.status}${errorText ? `: ${errorText}` : ""}`,
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const text = await response.text();
|
|
46
|
+
const result = parseMcpResponse(text);
|
|
47
|
+
if (!result) {
|
|
48
|
+
return { content: "No results found." };
|
|
49
|
+
}
|
|
50
|
+
return { content: result };
|
|
51
|
+
}
|
|
52
|
+
function parseMcpResponse(body) {
|
|
53
|
+
const lines = body.split("\n");
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (!line.startsWith("data: "))
|
|
56
|
+
continue;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(line.slice(6));
|
|
59
|
+
const text = parsed.result?.content?.find((item) => item.type === "text")?.text;
|
|
60
|
+
if (text)
|
|
61
|
+
return text;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Ignore malformed SSE lines.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(body);
|
|
69
|
+
return parsed.result?.content?.find((item) => item.type === "text")?.text;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exit_plan_mode tool - proposes a plan, awaits user approval, and (on approve) flips the
|
|
3
|
+
* agent out of plan mode so subsequent turns may execute it.
|
|
4
|
+
*
|
|
5
|
+
* The tool is read-only from the agent's perspective; the side effects (prompting the user,
|
|
6
|
+
* flipping mode) happen via the injected PlanController.
|
|
7
|
+
*/
|
|
8
|
+
import type { PermissionMode, PlanDecision, ToolRegistryEntry } from "../types.js";
|
|
9
|
+
export interface PlanController {
|
|
10
|
+
/** Reads the current permission mode (used to gate the tool). */
|
|
11
|
+
getMode: () => PermissionMode;
|
|
12
|
+
/** Ask the user to approve/reject/edit the proposed plan. */
|
|
13
|
+
requestApproval(plan: string): Promise<PlanDecision>;
|
|
14
|
+
/** Switch the agent's mode. Called after an approval so the next turn runs unconstrained. */
|
|
15
|
+
setMode(mode: PermissionMode): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function createExitPlanModeTool(controller: PlanController): ToolRegistryEntry;
|