@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,174 @@
|
|
|
1
|
+
import { QuestionRejectedError } from "../question/index.js";
|
|
2
|
+
export function createQuestionTool(controller) {
|
|
3
|
+
return {
|
|
4
|
+
name: "question",
|
|
5
|
+
readOnly: true,
|
|
6
|
+
description: `Ask the user one or more structured questions during execution.
|
|
7
|
+
|
|
8
|
+
Use this when you need to:
|
|
9
|
+
1. Gather user preferences or requirements
|
|
10
|
+
2. Clarify ambiguous instructions
|
|
11
|
+
3. Get decisions on implementation choices
|
|
12
|
+
4. Offer choices about what direction to take
|
|
13
|
+
|
|
14
|
+
Usage notes:
|
|
15
|
+
- Each question needs a short header, complete question text, and concise options.
|
|
16
|
+
- When custom is enabled (default), the UI adds "Type your own answer"; do not include "Other" or catch-all options yourself.
|
|
17
|
+
- Answers are returned as arrays of labels; set multiple: true when more than one answer may be selected.
|
|
18
|
+
- If you recommend a specific option, make it the first option and add "(Recommended)" to the label.
|
|
19
|
+
- Ask only targeted questions that unblock real work; do not ask "Should I proceed?".`,
|
|
20
|
+
parameters: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
questions: {
|
|
24
|
+
type: "array",
|
|
25
|
+
description: "Questions to ask the user.",
|
|
26
|
+
items: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
header: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Very short label for this question, ideally 1-4 words.",
|
|
32
|
+
},
|
|
33
|
+
question: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Complete user-facing question.",
|
|
36
|
+
},
|
|
37
|
+
options: {
|
|
38
|
+
type: "array",
|
|
39
|
+
description: "Available choices. Do not include Other when custom is enabled.",
|
|
40
|
+
items: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
label: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Display label, concise and selectable.",
|
|
46
|
+
},
|
|
47
|
+
description: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Short explanation of the choice and its tradeoff.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
required: ["label", "description"],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
multiple: {
|
|
57
|
+
type: "boolean",
|
|
58
|
+
description: "Allow selecting multiple choices.",
|
|
59
|
+
},
|
|
60
|
+
custom: {
|
|
61
|
+
type: "boolean",
|
|
62
|
+
description: "Allow typing a custom answer. Defaults to true.",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
required: ["header", "question", "options"],
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
required: ["questions"],
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
},
|
|
73
|
+
async execute(args, ctx) {
|
|
74
|
+
const normalized = normalizeQuestions(args.questions);
|
|
75
|
+
if ("error" in normalized) {
|
|
76
|
+
return { content: normalized.error, isError: true };
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const answers = await controller.ask({
|
|
80
|
+
sessionID: ctx.sessionID,
|
|
81
|
+
questions: normalized.questions,
|
|
82
|
+
tool: ctx.toolCall ? { callID: ctx.toolCall.id } : undefined,
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
content: formatQuestionToolResult(normalized.questions, answers),
|
|
86
|
+
status: "success",
|
|
87
|
+
metadata: {
|
|
88
|
+
kind: "question",
|
|
89
|
+
questions: normalized.questions,
|
|
90
|
+
answers,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const message = err instanceof QuestionRejectedError
|
|
96
|
+
? err.message
|
|
97
|
+
: err instanceof Error
|
|
98
|
+
? err.message
|
|
99
|
+
: String(err);
|
|
100
|
+
return {
|
|
101
|
+
content: `QuestionRejectedError: ${message}`,
|
|
102
|
+
isError: true,
|
|
103
|
+
status: "blocked",
|
|
104
|
+
metadata: {
|
|
105
|
+
kind: "question",
|
|
106
|
+
questions: normalized.questions,
|
|
107
|
+
rejected: true,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function normalizeQuestions(input) {
|
|
115
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
116
|
+
return { error: "Error: questions must be a non-empty array." };
|
|
117
|
+
}
|
|
118
|
+
if (input.length > 3) {
|
|
119
|
+
return { error: "Error: ask at most 3 questions at a time." };
|
|
120
|
+
}
|
|
121
|
+
const questions = [];
|
|
122
|
+
for (let index = 0; index < input.length; index++) {
|
|
123
|
+
const raw = input[index];
|
|
124
|
+
if (!raw || typeof raw !== "object") {
|
|
125
|
+
return { error: `Error: question at index ${index} must be an object.` };
|
|
126
|
+
}
|
|
127
|
+
const q = raw;
|
|
128
|
+
const header = typeof q.header === "string" ? q.header.trim() : "";
|
|
129
|
+
const question = typeof q.question === "string" ? q.question.trim() : "";
|
|
130
|
+
const options = q.options;
|
|
131
|
+
if (!header)
|
|
132
|
+
return { error: `Error: question at index ${index} has an empty header.` };
|
|
133
|
+
if (!question)
|
|
134
|
+
return { error: `Error: question at index ${index} has empty question text.` };
|
|
135
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
136
|
+
return { error: `Error: question at index ${index} needs at least one option.` };
|
|
137
|
+
}
|
|
138
|
+
if (options.length > 9) {
|
|
139
|
+
return { error: `Error: question at index ${index} has too many options; max is 9.` };
|
|
140
|
+
}
|
|
141
|
+
const normalizedOptions = [];
|
|
142
|
+
for (let optIndex = 0; optIndex < options.length; optIndex++) {
|
|
143
|
+
const opt = options[optIndex];
|
|
144
|
+
if (!opt || typeof opt !== "object") {
|
|
145
|
+
return { error: `Error: option ${optIndex + 1} for "${header}" must be an object.` };
|
|
146
|
+
}
|
|
147
|
+
const value = opt;
|
|
148
|
+
const label = typeof value.label === "string" ? value.label.trim() : "";
|
|
149
|
+
const description = typeof value.description === "string" ? value.description.trim() : "";
|
|
150
|
+
if (!label)
|
|
151
|
+
return { error: `Error: option ${optIndex + 1} for "${header}" has empty label.` };
|
|
152
|
+
if (!description)
|
|
153
|
+
return { error: `Error: option "${label}" for "${header}" has empty description.` };
|
|
154
|
+
normalizedOptions.push({ label, description });
|
|
155
|
+
}
|
|
156
|
+
questions.push({
|
|
157
|
+
header,
|
|
158
|
+
question,
|
|
159
|
+
options: normalizedOptions,
|
|
160
|
+
multiple: q.multiple === true,
|
|
161
|
+
custom: q.custom === false ? false : undefined,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return { questions };
|
|
165
|
+
}
|
|
166
|
+
function formatQuestionToolResult(questions, answers) {
|
|
167
|
+
const formatted = questions
|
|
168
|
+
.map((question, index) => {
|
|
169
|
+
const answer = answers[index]?.length ? answers[index].join(", ") : "Unanswered";
|
|
170
|
+
return `"${question.question}"="${answer}"`;
|
|
171
|
+
})
|
|
172
|
+
.join(", ");
|
|
173
|
+
return `User has answered your questions: ${formatted}. You can now continue with the user's answers in mind.`;
|
|
174
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read tool - read file contents with truncation.
|
|
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 declare function createReadTool(cwd: string, approval?: ApprovalController, lsp?: LspService): ToolRegistryEntry;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read tool - read file contents with truncation.
|
|
3
|
+
*/
|
|
4
|
+
import { constants } from "node:fs";
|
|
5
|
+
import { access, readFile } from "node:fs/promises";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { isSensitivePath } from "./sensitive-paths.js";
|
|
8
|
+
const MAX_LINES = 250;
|
|
9
|
+
const MAX_BYTES = 100 * 1024;
|
|
10
|
+
export function createReadTool(cwd, approval, lsp) {
|
|
11
|
+
return {
|
|
12
|
+
name: "read",
|
|
13
|
+
readOnly: true,
|
|
14
|
+
description: `Read the contents of a file. Output is truncated to ${MAX_LINES} lines or ${MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
path: { type: "string", description: "Path to the file (relative or absolute)" },
|
|
19
|
+
offset: { type: "number", description: "Line number to start from (1-indexed)" },
|
|
20
|
+
limit: { type: "number", description: "Maximum number of lines to read" },
|
|
21
|
+
},
|
|
22
|
+
required: ["path"],
|
|
23
|
+
},
|
|
24
|
+
async execute(args) {
|
|
25
|
+
const filePath = resolve(cwd, args.path);
|
|
26
|
+
if (isSensitivePath(filePath)) {
|
|
27
|
+
return {
|
|
28
|
+
content: `Error: Access to sensitive credential storage is blocked: ${filePath}`,
|
|
29
|
+
isError: true,
|
|
30
|
+
status: "blocked",
|
|
31
|
+
metadata: {
|
|
32
|
+
kind: "security",
|
|
33
|
+
path: filePath,
|
|
34
|
+
reason: "Sensitive credential storage is not readable from general-purpose tasks.",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (approval) {
|
|
39
|
+
const result = approval.checkRules({ tool: "Read", path: filePath, cwd });
|
|
40
|
+
if (result.decision === "deny") {
|
|
41
|
+
return {
|
|
42
|
+
content: `Error: Read blocked by deny rule: ${result.rule?.source ?? "<unknown>"} (${filePath})`,
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await access(filePath, constants.R_OK);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { content: `Error: Cannot read file: ${filePath}`, isError: true };
|
|
52
|
+
}
|
|
53
|
+
let content = await readFile(filePath, "utf-8");
|
|
54
|
+
const lines = content.split("\n");
|
|
55
|
+
const offset = typeof args.offset === "number" ? Math.max(0, args.offset - 1) : 0;
|
|
56
|
+
const limit = typeof args.limit === "number" ? args.limit : lines.length;
|
|
57
|
+
let sliced = lines.slice(offset, offset + limit);
|
|
58
|
+
let truncated = false;
|
|
59
|
+
if (sliced.length > MAX_LINES) {
|
|
60
|
+
sliced = sliced.slice(0, MAX_LINES);
|
|
61
|
+
truncated = true;
|
|
62
|
+
}
|
|
63
|
+
let result = sliced.join("\n");
|
|
64
|
+
const byteLength = Buffer.byteLength(result, "utf-8");
|
|
65
|
+
if (byteLength > MAX_BYTES) {
|
|
66
|
+
result = Buffer.from(result, "utf-8").subarray(0, MAX_BYTES).toString("utf-8");
|
|
67
|
+
truncated = true;
|
|
68
|
+
}
|
|
69
|
+
if (truncated) {
|
|
70
|
+
result += `\n[Output truncated: exceeded ${MAX_LINES} lines or ${MAX_BYTES / 1024}KB limit]`;
|
|
71
|
+
}
|
|
72
|
+
void lsp?.touchFile(filePath).catch(() => undefined);
|
|
73
|
+
return {
|
|
74
|
+
content: result,
|
|
75
|
+
status: "success",
|
|
76
|
+
metadata: {
|
|
77
|
+
kind: "read",
|
|
78
|
+
path: filePath,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { getBubbleHome } from "../bubble-home.js";
|
|
4
|
+
export function getSensitivePaths() {
|
|
5
|
+
const bubbleHome = getBubbleHome();
|
|
6
|
+
return [
|
|
7
|
+
resolve(bubbleHome, "config.json"),
|
|
8
|
+
resolve(bubbleHome, "auth.json"),
|
|
9
|
+
];
|
|
10
|
+
}
|
|
11
|
+
export function isSensitivePath(filePath) {
|
|
12
|
+
const resolved = resolve(filePath);
|
|
13
|
+
return getSensitivePaths().includes(resolved);
|
|
14
|
+
}
|
|
15
|
+
export function referencesSensitivePath(command) {
|
|
16
|
+
const lower = command.toLowerCase();
|
|
17
|
+
if (lower.includes("~/.bubble/config.json") || lower.includes("~/.bubble/auth.json")) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return getSensitivePaths().some((filePath) => {
|
|
21
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
22
|
+
return lower.includes(normalized.toLowerCase()) || lower.includes(normalized.toLowerCase().replace(homedir().toLowerCase(), "~"));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SkillRecord } from "../skills/types.js";
|
|
2
|
+
import type { SkillRegistry } from "../skills/registry.js";
|
|
3
|
+
import type { ToolRegistryEntry } from "../types.js";
|
|
4
|
+
export declare function formatLoadedSkill(skill: SkillRecord): string;
|
|
5
|
+
export declare function createSkillTool(registry: SkillRegistry): ToolRegistryEntry;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function formatLoadedSkill(skill) {
|
|
2
|
+
const resources = [
|
|
3
|
+
...skill.resources.references,
|
|
4
|
+
...skill.resources.scripts,
|
|
5
|
+
...skill.resources.assets,
|
|
6
|
+
];
|
|
7
|
+
const sections = [
|
|
8
|
+
`Skill: ${skill.meta.name}`,
|
|
9
|
+
`Description: ${skill.meta.description}`,
|
|
10
|
+
`Base directory: ${skill.rootDir}`,
|
|
11
|
+
"",
|
|
12
|
+
skill.content,
|
|
13
|
+
];
|
|
14
|
+
if (resources.length > 0) {
|
|
15
|
+
sections.push("", "Resources:", ...resources.map((resource) => `- ${resource}`));
|
|
16
|
+
}
|
|
17
|
+
sections.push("", "Relative paths mentioned in this skill are resolved from the base directory above.");
|
|
18
|
+
return sections.join("\n");
|
|
19
|
+
}
|
|
20
|
+
export function createSkillTool(registry) {
|
|
21
|
+
return {
|
|
22
|
+
name: "skill",
|
|
23
|
+
readOnly: true,
|
|
24
|
+
description: "Load a named skill on demand. Use this when a task clearly matches one of the available skills.",
|
|
25
|
+
parameters: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
name: { type: "string", description: "The exact skill name to load" },
|
|
29
|
+
},
|
|
30
|
+
required: ["name"],
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
},
|
|
33
|
+
async execute(args) {
|
|
34
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
35
|
+
if (!name) {
|
|
36
|
+
return { content: "Error: skill name is required", isError: true };
|
|
37
|
+
}
|
|
38
|
+
const skill = registry.get(name);
|
|
39
|
+
if (!skill) {
|
|
40
|
+
const available = registry.summaries().map((item) => item.name).join(", ");
|
|
41
|
+
return {
|
|
42
|
+
content: available
|
|
43
|
+
? `Error: Unknown skill "${name}". Available skills: ${available}`
|
|
44
|
+
: `Error: Unknown skill "${name}". No skills are currently available.`,
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { content: formatLoadedSkill(skill) };
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function createTaskTool() {
|
|
2
|
+
return {
|
|
3
|
+
name: "task",
|
|
4
|
+
readOnly: true,
|
|
5
|
+
description: `Delegate a bounded investigative subtask to a read-only sub-agent and return a concise summary.
|
|
6
|
+
|
|
7
|
+
Use this when:
|
|
8
|
+
- a search/investigation can be scoped as a sub-problem
|
|
9
|
+
- you want a focused summary before continuing the main task
|
|
10
|
+
- you need to inspect a specific hypothesis or area without polluting the main loop with exploratory churn
|
|
11
|
+
|
|
12
|
+
Do not use this for edits or shell-heavy workflows. The subtask agent runs in read-only plan mode.`,
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
prompt: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "The focused subtask prompt to investigate.",
|
|
19
|
+
},
|
|
20
|
+
description: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Short description of what this subtask is trying to verify.",
|
|
23
|
+
},
|
|
24
|
+
subtaskType: {
|
|
25
|
+
type: "string",
|
|
26
|
+
enum: ["search", "security_investigation", "evidence_correlation", "general_readonly"],
|
|
27
|
+
description: "The bounded subtask policy to apply.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
required: ["prompt"],
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
},
|
|
33
|
+
async execute(args, ctx) {
|
|
34
|
+
const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
|
|
35
|
+
const description = typeof args.description === "string" ? args.description.trim() : "";
|
|
36
|
+
const subtaskType = typeof args.subtaskType === "string" ? args.subtaskType : undefined;
|
|
37
|
+
if (!prompt) {
|
|
38
|
+
return { content: "Error: task prompt is required", isError: true };
|
|
39
|
+
}
|
|
40
|
+
if (!ctx.agent?.runSubtask) {
|
|
41
|
+
return { content: "Error: task tool requires an agent runtime", isError: true };
|
|
42
|
+
}
|
|
43
|
+
const composed = description
|
|
44
|
+
? `${description}\n\n${prompt}\n\nInvestigate this sub-problem in read-only mode and return a concise evidence-based summary.`
|
|
45
|
+
: `${prompt}\n\nInvestigate this sub-problem in read-only mode and return a concise evidence-based summary.`;
|
|
46
|
+
const result = await ctx.agent.runSubtask(composed, ctx.cwd, { subtaskType, description });
|
|
47
|
+
return {
|
|
48
|
+
...result,
|
|
49
|
+
metadata: {
|
|
50
|
+
...result.metadata,
|
|
51
|
+
kind: "security",
|
|
52
|
+
reason: description || result.metadata?.reason,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Todo tool - manage an agent-visible task list for multi-step work.
|
|
3
|
+
*
|
|
4
|
+
* The tool overwrites the entire list on each call. The model should re-send the full
|
|
5
|
+
* array with updated statuses rather than trying to patch individual items.
|
|
6
|
+
*/
|
|
7
|
+
import type { Todo, ToolRegistryEntry } from "../types.js";
|
|
8
|
+
export interface TodoStore {
|
|
9
|
+
getTodos: () => Todo[];
|
|
10
|
+
setTodos: (todos: Todo[]) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function createTodoTool(store: TodoStore): ToolRegistryEntry;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Todo tool - manage an agent-visible task list for multi-step work.
|
|
3
|
+
*
|
|
4
|
+
* The tool overwrites the entire list on each call. The model should re-send the full
|
|
5
|
+
* array with updated statuses rather than trying to patch individual items.
|
|
6
|
+
*/
|
|
7
|
+
export function createTodoTool(store) {
|
|
8
|
+
return {
|
|
9
|
+
name: "todo_write",
|
|
10
|
+
readOnly: true,
|
|
11
|
+
description: `Create or update the task list for the current work. Send the COMPLETE list each call; this overwrites the prior list entirely.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
Use this tool proactively when any of these apply:
|
|
16
|
+
1. Complex multi-step work — 3 or more distinct steps or file locations
|
|
17
|
+
2. Non-trivial tasks requiring planning or coordination across multiple operations
|
|
18
|
+
3. The user explicitly asks for a todo list
|
|
19
|
+
4. The user provides a list of things to do (numbered, comma-separated, bulleted)
|
|
20
|
+
5. New instructions arrive mid-session — capture them as todos before starting
|
|
21
|
+
6. Starting work on a task — mark it in_progress BEFORE beginning. Only one item may be in_progress at a time
|
|
22
|
+
7. Finishing a task — mark it completed immediately, don't batch completions
|
|
23
|
+
|
|
24
|
+
## When NOT to use
|
|
25
|
+
|
|
26
|
+
Skip this tool when:
|
|
27
|
+
1. There is a single, straightforward task
|
|
28
|
+
2. The task is trivial and tracking provides no organizational benefit
|
|
29
|
+
3. The work can be completed in fewer than 3 trivial steps
|
|
30
|
+
4. The request is purely conversational or informational
|
|
31
|
+
|
|
32
|
+
If there is only one trivial task, just do it — don't create a todo first.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
<example>
|
|
37
|
+
User: Add a dark mode toggle to the settings page, then run tests and build.
|
|
38
|
+
Assistant: *creates a 5-item todo: toggle UI, theme state, CSS tokens, update components, run tests + build*
|
|
39
|
+
<reasoning>Multiple distinct steps across UI, state, styles, and verification. User explicitly asked for tests + build.</reasoning>
|
|
40
|
+
</example>
|
|
41
|
+
|
|
42
|
+
<example>
|
|
43
|
+
User: Rename getCwd to getCurrentWorkingDirectory across the project.
|
|
44
|
+
Assistant: *greps, finds 15 call sites across 8 files, creates a per-file todo list*
|
|
45
|
+
<reasoning>Scope discovered via grep shows many locations; a todo ensures each file is tracked and none are missed.</reasoning>
|
|
46
|
+
</example>
|
|
47
|
+
|
|
48
|
+
<example>
|
|
49
|
+
User: How do I print "Hello World" in Python?
|
|
50
|
+
Assistant: *answers in one sentence with a snippet — no todo*
|
|
51
|
+
<reasoning>Informational, one-step, no tracking benefit.</reasoning>
|
|
52
|
+
</example>
|
|
53
|
+
|
|
54
|
+
<example>
|
|
55
|
+
User: Add a comment to calculateTotal explaining what it does.
|
|
56
|
+
Assistant: *calls edit directly — no todo*
|
|
57
|
+
<reasoning>Single, localized change in one file.</reasoning>
|
|
58
|
+
</example>
|
|
59
|
+
|
|
60
|
+
## Task states
|
|
61
|
+
|
|
62
|
+
- pending: not yet started
|
|
63
|
+
- in_progress: currently working on — exactly ONE at a time
|
|
64
|
+
- completed: finished successfully
|
|
65
|
+
|
|
66
|
+
Each item needs:
|
|
67
|
+
- content: imperative form (e.g. "Run tests")
|
|
68
|
+
- activeForm: present continuous, shown while in progress (e.g. "Running tests")
|
|
69
|
+
|
|
70
|
+
## Rules
|
|
71
|
+
|
|
72
|
+
- Update status in real time; mark completed IMMEDIATELY on finishing.
|
|
73
|
+
- Never mark completed if tests are failing, implementation is partial, errors are unresolved, or needed files are missing — keep as in_progress.
|
|
74
|
+
- When blocked, add a new task describing what must be resolved.
|
|
75
|
+
- Remove items that are no longer relevant; don't leave stale entries.`,
|
|
76
|
+
parameters: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
todos: {
|
|
80
|
+
type: "array",
|
|
81
|
+
description: "The complete list of todos. Replaces any existing list.",
|
|
82
|
+
items: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
content: {
|
|
86
|
+
type: "string",
|
|
87
|
+
description: "Imperative form describing the task (e.g., 'Add unit tests for foo')",
|
|
88
|
+
},
|
|
89
|
+
activeForm: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "Present continuous form shown while in progress (e.g., 'Adding unit tests for foo')",
|
|
92
|
+
},
|
|
93
|
+
status: {
|
|
94
|
+
type: "string",
|
|
95
|
+
enum: ["pending", "in_progress", "completed"],
|
|
96
|
+
description: "Current status of the task",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: ["content", "activeForm", "status"],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ["todos"],
|
|
104
|
+
},
|
|
105
|
+
async execute(args) {
|
|
106
|
+
if (!Array.isArray(args.todos)) {
|
|
107
|
+
return { content: "Error: 'todos' must be an array", isError: true };
|
|
108
|
+
}
|
|
109
|
+
const normalized = [];
|
|
110
|
+
for (let i = 0; i < args.todos.length; i++) {
|
|
111
|
+
const raw = args.todos[i];
|
|
112
|
+
if (!raw || typeof raw !== "object") {
|
|
113
|
+
return { content: `Error: todo at index ${i} is not an object`, isError: true };
|
|
114
|
+
}
|
|
115
|
+
const content = typeof raw.content === "string" ? raw.content.trim() : "";
|
|
116
|
+
const activeForm = typeof raw.activeForm === "string" ? raw.activeForm.trim() : "";
|
|
117
|
+
const status = raw.status;
|
|
118
|
+
if (!content) {
|
|
119
|
+
return { content: `Error: todo at index ${i} has empty content`, isError: true };
|
|
120
|
+
}
|
|
121
|
+
if (!activeForm) {
|
|
122
|
+
return { content: `Error: todo at index ${i} has empty activeForm`, isError: true };
|
|
123
|
+
}
|
|
124
|
+
if (status !== "pending" && status !== "in_progress" && status !== "completed") {
|
|
125
|
+
return {
|
|
126
|
+
content: `Error: todo at index ${i} has invalid status "${status}". Must be pending|in_progress|completed`,
|
|
127
|
+
isError: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
normalized.push({ content, activeForm, status });
|
|
131
|
+
}
|
|
132
|
+
const inProgressCount = normalized.filter((t) => t.status === "in_progress").length;
|
|
133
|
+
if (inProgressCount > 1) {
|
|
134
|
+
return {
|
|
135
|
+
content: `Error: at most one todo may be in_progress at a time, found ${inProgressCount}`,
|
|
136
|
+
isError: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
store.setTodos(normalized);
|
|
140
|
+
const counts = {
|
|
141
|
+
pending: normalized.filter((t) => t.status === "pending").length,
|
|
142
|
+
in_progress: inProgressCount,
|
|
143
|
+
completed: normalized.filter((t) => t.status === "completed").length,
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
content: `Todo list updated: ${normalized.length} item${normalized.length === 1 ? "" : "s"} ` +
|
|
147
|
+
`(${counts.completed} completed, ${counts.in_progress} in progress, ${counts.pending} pending).`,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
import type { ToolRegistryEntry } from "../types.js";
|
|
17
|
+
export interface ToolSearchController {
|
|
18
|
+
/** All deferred tools in the current session, whether unlocked or not. */
|
|
19
|
+
listDeferred: () => ToolRegistryEntry[];
|
|
20
|
+
/** Mark a set of deferred tool names as unlocked. */
|
|
21
|
+
unlock: (names: string[]) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createToolSearchTool(controller: ToolSearchController): ToolRegistryEntry;
|