@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,48 @@
|
|
|
1
|
+
import type { PermissionCheckResult, PermissionQuery, PermissionRuleSet } from "../permissions/types.js";
|
|
2
|
+
import type { PermissionMode } from "../types.js";
|
|
3
|
+
import type { BashAllowlist } from "./session-cache.js";
|
|
4
|
+
import type { ApprovalController, ApprovalDecision, ApprovalRequest } from "./types.js";
|
|
5
|
+
export interface ApprovalControllerOptions {
|
|
6
|
+
/** Reads the live agent mode on each request so mode flips take effect immediately. */
|
|
7
|
+
getMode: () => PermissionMode;
|
|
8
|
+
/**
|
|
9
|
+
* UI handler attached by the TUI on mount. Returns the user's decision from
|
|
10
|
+
* an interactive dialog. When not attached (e.g. --print mode), the
|
|
11
|
+
* controller falls back to rejecting in safe modes.
|
|
12
|
+
*/
|
|
13
|
+
handlerRef: {
|
|
14
|
+
current?: (req: ApprovalRequest) => Promise<ApprovalDecision>;
|
|
15
|
+
};
|
|
16
|
+
/** Session-scoped bash command prefix allowlist. Optional. */
|
|
17
|
+
bashAllowlist?: BashAllowlist;
|
|
18
|
+
/** Working directory — used to anchor relative path rules. */
|
|
19
|
+
cwd: string;
|
|
20
|
+
/**
|
|
21
|
+
* Live view of configured allow/deny rules (from ~/.bubble/settings.json and
|
|
22
|
+
* project-level equivalents). Called on each request so edits via
|
|
23
|
+
* /permissions take effect immediately. Omit to disable rule-based gating.
|
|
24
|
+
*/
|
|
25
|
+
getRuleSet?: () => PermissionRuleSet;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Default ApprovalController. Decision tree:
|
|
29
|
+
*
|
|
30
|
+
* deny rule match → reject (applies even under bypassPermissions)
|
|
31
|
+
* bypassPermissions / dontAsk → auto-approve, no prompt
|
|
32
|
+
* acceptEdits + edit|write → auto-approve
|
|
33
|
+
* plan → reject with instructions to use exit_plan_mode
|
|
34
|
+
* allow rule match → auto-approve
|
|
35
|
+
* bash in session allowlist → auto-approve
|
|
36
|
+
* default / other → delegate to UI; if no UI, reject
|
|
37
|
+
*
|
|
38
|
+
* Deny rules sit at the top as a hard ceiling: bypassPermissions is a trust
|
|
39
|
+
* escalation, not a policy override. Users who want to permit a currently-
|
|
40
|
+
* denied action must edit their settings.json, not bypass checks at runtime.
|
|
41
|
+
*/
|
|
42
|
+
export declare class PermissionAwareApprovalController implements ApprovalController {
|
|
43
|
+
private readonly options;
|
|
44
|
+
constructor(options: ApprovalControllerOptions);
|
|
45
|
+
checkRules(query: PermissionQuery): PermissionCheckResult;
|
|
46
|
+
request(req: ApprovalRequest): Promise<ApprovalDecision>;
|
|
47
|
+
private requestToQuery;
|
|
48
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { checkPermission } from "../permissions/rule.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default ApprovalController. Decision tree:
|
|
4
|
+
*
|
|
5
|
+
* deny rule match → reject (applies even under bypassPermissions)
|
|
6
|
+
* bypassPermissions / dontAsk → auto-approve, no prompt
|
|
7
|
+
* acceptEdits + edit|write → auto-approve
|
|
8
|
+
* plan → reject with instructions to use exit_plan_mode
|
|
9
|
+
* allow rule match → auto-approve
|
|
10
|
+
* bash in session allowlist → auto-approve
|
|
11
|
+
* default / other → delegate to UI; if no UI, reject
|
|
12
|
+
*
|
|
13
|
+
* Deny rules sit at the top as a hard ceiling: bypassPermissions is a trust
|
|
14
|
+
* escalation, not a policy override. Users who want to permit a currently-
|
|
15
|
+
* denied action must edit their settings.json, not bypass checks at runtime.
|
|
16
|
+
*/
|
|
17
|
+
export class PermissionAwareApprovalController {
|
|
18
|
+
options;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
}
|
|
22
|
+
checkRules(query) {
|
|
23
|
+
const ruleSet = this.options.getRuleSet?.();
|
|
24
|
+
if (!ruleSet)
|
|
25
|
+
return { decision: "ask" };
|
|
26
|
+
return checkPermission(ruleSet, query);
|
|
27
|
+
}
|
|
28
|
+
async request(req) {
|
|
29
|
+
const query = this.requestToQuery(req);
|
|
30
|
+
const ruleResult = this.checkRules(query);
|
|
31
|
+
if (ruleResult.decision === "deny") {
|
|
32
|
+
return {
|
|
33
|
+
action: "reject",
|
|
34
|
+
feedback: `Blocked by deny rule: ${ruleResult.rule?.source ?? "<unknown>"}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const mode = this.options.getMode();
|
|
38
|
+
if (mode === "bypassPermissions" || mode === "dontAsk") {
|
|
39
|
+
return { action: "approve" };
|
|
40
|
+
}
|
|
41
|
+
if (mode === "acceptEdits" && (req.type === "edit" || req.type === "write")) {
|
|
42
|
+
return { action: "approve" };
|
|
43
|
+
}
|
|
44
|
+
if (mode === "plan") {
|
|
45
|
+
return {
|
|
46
|
+
action: "reject",
|
|
47
|
+
feedback: "Plan mode is active. Do not call destructive tools directly — propose your changes via exit_plan_mode and wait for user approval.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (ruleResult.decision === "allow") {
|
|
51
|
+
return { action: "approve" };
|
|
52
|
+
}
|
|
53
|
+
// Session-scoped allowlist: previously-approved bash prefixes skip the prompt.
|
|
54
|
+
if (req.type === "bash" && this.options.bashAllowlist?.matches(req.command)) {
|
|
55
|
+
return { action: "approve" };
|
|
56
|
+
}
|
|
57
|
+
const handler = this.options.handlerRef.current;
|
|
58
|
+
if (!handler) {
|
|
59
|
+
return {
|
|
60
|
+
action: "reject",
|
|
61
|
+
feedback: "No interactive UI is available to approve this tool call.",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return handler(req);
|
|
65
|
+
}
|
|
66
|
+
requestToQuery(req) {
|
|
67
|
+
switch (req.type) {
|
|
68
|
+
case "bash":
|
|
69
|
+
return { tool: "Bash", command: req.command };
|
|
70
|
+
case "write":
|
|
71
|
+
return { tool: "Write", path: req.path, cwd: this.options.cwd };
|
|
72
|
+
case "edit":
|
|
73
|
+
return { tool: "Edit", path: req.path, cwd: this.options.cwd };
|
|
74
|
+
case "lsp":
|
|
75
|
+
return { tool: "Lsp", path: req.path, cwd: this.options.cwd };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic classifier for bash commands that deserve an extra user warning.
|
|
3
|
+
*
|
|
4
|
+
* Conservative by design: false negatives are fine (users are still prompted
|
|
5
|
+
* to approve), false positives are annoying (users get desensitised to the
|
|
6
|
+
* warning). We only flag patterns that are widely recognised as destructive
|
|
7
|
+
* or commonly abused by injected prompts.
|
|
8
|
+
*/
|
|
9
|
+
export interface DangerSignal {
|
|
10
|
+
pattern: string;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function classifyBashDanger(command: string): DangerSignal | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic classifier for bash commands that deserve an extra user warning.
|
|
3
|
+
*
|
|
4
|
+
* Conservative by design: false negatives are fine (users are still prompted
|
|
5
|
+
* to approve), false positives are annoying (users get desensitised to the
|
|
6
|
+
* warning). We only flag patterns that are widely recognised as destructive
|
|
7
|
+
* or commonly abused by injected prompts.
|
|
8
|
+
*/
|
|
9
|
+
export function classifyBashDanger(command) {
|
|
10
|
+
const normalized = command.trim();
|
|
11
|
+
if (!normalized)
|
|
12
|
+
return null;
|
|
13
|
+
// Piping an internet download into a shell — classic supply-chain foot-gun.
|
|
14
|
+
if (/\b(curl|wget|fetch)\b[^|]*\|\s*(bash|sh|zsh|fish)\b/i.test(normalized)) {
|
|
15
|
+
return {
|
|
16
|
+
pattern: "curl | sh",
|
|
17
|
+
message: "This command pipes a downloaded script directly into a shell.",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// Recursive force delete. Match `rm -rf`, `rm -fr`, `rm -Rf`, etc.
|
|
21
|
+
if (/\brm\s+(-[a-zA-Z]*[rR][a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*[rR])\b/.test(normalized)) {
|
|
22
|
+
return {
|
|
23
|
+
pattern: "rm -rf",
|
|
24
|
+
message: "This command recursively deletes files and directories.",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Privilege escalation.
|
|
28
|
+
if (/\bsudo\b/.test(normalized)) {
|
|
29
|
+
return {
|
|
30
|
+
pattern: "sudo",
|
|
31
|
+
message: "This command runs with elevated privileges.",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// chmod 777 and friends.
|
|
35
|
+
if (/\bchmod\s+(-R\s+)?(0?777|a\+rwx|u\+rwx,g\+rwx,o\+rwx)\b/.test(normalized)) {
|
|
36
|
+
return {
|
|
37
|
+
pattern: "chmod 777",
|
|
38
|
+
message: "This command gives world-writable permissions.",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Git force push / reset hard — potentially destructive on shared branches.
|
|
42
|
+
if (/\bgit\s+push\s+(-f\b|--force\b|--force-with-lease\b)/.test(normalized)) {
|
|
43
|
+
return {
|
|
44
|
+
pattern: "git push --force",
|
|
45
|
+
message: "This force-pushes and can overwrite the remote branch history.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (/\bgit\s+reset\s+--hard\b/.test(normalized)) {
|
|
49
|
+
return {
|
|
50
|
+
pattern: "git reset --hard",
|
|
51
|
+
message: "This discards uncommitted changes and rewrites the working tree.",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses unified-diff output (e.g. from `diff`'s `createTwoFilesPatch`) into
|
|
3
|
+
* discrete hunks so the TUI can render each one with its own header and fold
|
|
4
|
+
* gracefully when the total body exceeds the available line budget.
|
|
5
|
+
*/
|
|
6
|
+
export interface DiffHunk {
|
|
7
|
+
/** The `@@ -old,count +new,count @@ ...` line verbatim. */
|
|
8
|
+
header: string;
|
|
9
|
+
/** Body lines, each beginning with '+', '-', or ' '. No trailing newline. */
|
|
10
|
+
lines: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function parseDiffHunks(diff: string): DiffHunk[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses unified-diff output (e.g. from `diff`'s `createTwoFilesPatch`) into
|
|
3
|
+
* discrete hunks so the TUI can render each one with its own header and fold
|
|
4
|
+
* gracefully when the total body exceeds the available line budget.
|
|
5
|
+
*/
|
|
6
|
+
export function parseDiffHunks(diff) {
|
|
7
|
+
const hunks = [];
|
|
8
|
+
const rawLines = diff.split("\n");
|
|
9
|
+
let current = null;
|
|
10
|
+
for (const line of rawLines) {
|
|
11
|
+
if (line.startsWith("===") ||
|
|
12
|
+
line.startsWith("Index:") ||
|
|
13
|
+
line.startsWith("---") ||
|
|
14
|
+
line.startsWith("+++")) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (line.startsWith("@@")) {
|
|
18
|
+
if (current)
|
|
19
|
+
hunks.push(current);
|
|
20
|
+
current = { header: line, lines: [] };
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!current)
|
|
24
|
+
continue;
|
|
25
|
+
if (line.startsWith("\\ No newline"))
|
|
26
|
+
continue;
|
|
27
|
+
current.lines.push(line);
|
|
28
|
+
}
|
|
29
|
+
if (current)
|
|
30
|
+
hunks.push(current);
|
|
31
|
+
return hunks;
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped allowlist of bash command prefixes. Not persisted to disk.
|
|
3
|
+
*
|
|
4
|
+
* Matching rule: a command is allowed if it starts with one of the stored
|
|
5
|
+
* prefixes and the next character is either end-of-string or whitespace.
|
|
6
|
+
* This means the prefix "git status" will match "git status" and
|
|
7
|
+
* "git status -s", but NOT "git statuss" or "git statusbad".
|
|
8
|
+
*
|
|
9
|
+
* For ergonomic parity with Claude Code we also strip a trailing ":*" when
|
|
10
|
+
* storing so that users may type "npm run:*" as a pattern — functionally
|
|
11
|
+
* equivalent to just "npm run" under our simple prefix rule.
|
|
12
|
+
*/
|
|
13
|
+
export declare class BashAllowlist {
|
|
14
|
+
private prefixes;
|
|
15
|
+
add(prefix: string): void;
|
|
16
|
+
remove(prefix: string): boolean;
|
|
17
|
+
clear(): void;
|
|
18
|
+
matches(command: string): boolean;
|
|
19
|
+
list(): string[];
|
|
20
|
+
size(): number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Infers a reasonable "don't ask again" prefix from a bash command. Uses the
|
|
24
|
+
* first two whitespace-separated tokens when the second token looks like a
|
|
25
|
+
* subcommand (no leading `-` or `/`), otherwise falls back to the first token.
|
|
26
|
+
* Examples:
|
|
27
|
+
* "git status -s" → "git status"
|
|
28
|
+
* "git status" → "git status"
|
|
29
|
+
* "git" → "git"
|
|
30
|
+
* "npm run test" → "npm run"
|
|
31
|
+
* "npm test" → "npm test"
|
|
32
|
+
* "rm -rf /tmp/x" → "rm"
|
|
33
|
+
* "./scripts/foo.sh" → "./scripts/foo.sh"
|
|
34
|
+
*/
|
|
35
|
+
export declare function inferBashPrefix(command: string): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped allowlist of bash command prefixes. Not persisted to disk.
|
|
3
|
+
*
|
|
4
|
+
* Matching rule: a command is allowed if it starts with one of the stored
|
|
5
|
+
* prefixes and the next character is either end-of-string or whitespace.
|
|
6
|
+
* This means the prefix "git status" will match "git status" and
|
|
7
|
+
* "git status -s", but NOT "git statuss" or "git statusbad".
|
|
8
|
+
*
|
|
9
|
+
* For ergonomic parity with Claude Code we also strip a trailing ":*" when
|
|
10
|
+
* storing so that users may type "npm run:*" as a pattern — functionally
|
|
11
|
+
* equivalent to just "npm run" under our simple prefix rule.
|
|
12
|
+
*/
|
|
13
|
+
export class BashAllowlist {
|
|
14
|
+
prefixes = new Set();
|
|
15
|
+
add(prefix) {
|
|
16
|
+
const cleaned = prefix.trim().replace(/:\*$/, "").trim();
|
|
17
|
+
if (!cleaned)
|
|
18
|
+
return;
|
|
19
|
+
this.prefixes.add(cleaned);
|
|
20
|
+
}
|
|
21
|
+
remove(prefix) {
|
|
22
|
+
return this.prefixes.delete(prefix.trim());
|
|
23
|
+
}
|
|
24
|
+
clear() {
|
|
25
|
+
this.prefixes.clear();
|
|
26
|
+
}
|
|
27
|
+
matches(command) {
|
|
28
|
+
const trimmed = command.trim();
|
|
29
|
+
for (const prefix of this.prefixes) {
|
|
30
|
+
if (trimmed === prefix)
|
|
31
|
+
return true;
|
|
32
|
+
if (trimmed.startsWith(prefix) && /\s/.test(trimmed.charAt(prefix.length))) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
list() {
|
|
39
|
+
return [...this.prefixes].sort();
|
|
40
|
+
}
|
|
41
|
+
size() {
|
|
42
|
+
return this.prefixes.size;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Infers a reasonable "don't ask again" prefix from a bash command. Uses the
|
|
47
|
+
* first two whitespace-separated tokens when the second token looks like a
|
|
48
|
+
* subcommand (no leading `-` or `/`), otherwise falls back to the first token.
|
|
49
|
+
* Examples:
|
|
50
|
+
* "git status -s" → "git status"
|
|
51
|
+
* "git status" → "git status"
|
|
52
|
+
* "git" → "git"
|
|
53
|
+
* "npm run test" → "npm run"
|
|
54
|
+
* "npm test" → "npm test"
|
|
55
|
+
* "rm -rf /tmp/x" → "rm"
|
|
56
|
+
* "./scripts/foo.sh" → "./scripts/foo.sh"
|
|
57
|
+
*/
|
|
58
|
+
export function inferBashPrefix(command) {
|
|
59
|
+
const tokens = command.trim().split(/\s+/);
|
|
60
|
+
if (tokens.length === 0 || !tokens[0])
|
|
61
|
+
return "";
|
|
62
|
+
const first = tokens[0];
|
|
63
|
+
const second = tokens[1];
|
|
64
|
+
if (second && /^[A-Za-z_]/.test(second)) {
|
|
65
|
+
return `${first} ${second}`;
|
|
66
|
+
}
|
|
67
|
+
return first;
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ToolResult } from "../types.js";
|
|
2
|
+
import type { ApprovalController, ApprovalDecision, ApprovalRequest } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Runs a tool action through the approval controller. Returns either the
|
|
5
|
+
* decision (on approve) or a pre-built rejection ToolResult (on reject or
|
|
6
|
+
* no controller available to the tool).
|
|
7
|
+
*/
|
|
8
|
+
export declare function gateToolAction(approval: ApprovalController | undefined, req: ApprovalRequest): Promise<{
|
|
9
|
+
approved: true;
|
|
10
|
+
decision: ApprovalDecision;
|
|
11
|
+
} | {
|
|
12
|
+
approved: false;
|
|
13
|
+
result: ToolResult;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs a tool action through the approval controller. Returns either the
|
|
3
|
+
* decision (on approve) or a pre-built rejection ToolResult (on reject or
|
|
4
|
+
* no controller available to the tool).
|
|
5
|
+
*/
|
|
6
|
+
export async function gateToolAction(approval, req) {
|
|
7
|
+
if (!approval) {
|
|
8
|
+
return { approved: true, decision: { action: "approve" } };
|
|
9
|
+
}
|
|
10
|
+
const decision = await approval.request(req);
|
|
11
|
+
if (decision.action === "approve") {
|
|
12
|
+
return { approved: true, decision };
|
|
13
|
+
}
|
|
14
|
+
const feedback = decision.feedback?.trim();
|
|
15
|
+
const label = approvalRequestLabel(req);
|
|
16
|
+
const message = feedback
|
|
17
|
+
? `${label} was rejected by the user. User feedback: ${feedback}`
|
|
18
|
+
: `${label} was rejected by the user.`;
|
|
19
|
+
return { approved: false, result: { content: message, isError: true } };
|
|
20
|
+
}
|
|
21
|
+
function approvalRequestLabel(req) {
|
|
22
|
+
switch (req.type) {
|
|
23
|
+
case "edit":
|
|
24
|
+
return `Edit to ${req.path}`;
|
|
25
|
+
case "write":
|
|
26
|
+
return `${req.fileExists ? "Overwrite" : "Write"} to ${req.path}`;
|
|
27
|
+
case "bash":
|
|
28
|
+
return `Bash command \`${req.command}\``;
|
|
29
|
+
case "lsp":
|
|
30
|
+
return `LSP ${req.operation} on ${req.path}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval flow: tools that want a gating UI construct an ApprovalRequest and
|
|
3
|
+
* await an ApprovalDecision from the harness (via ApprovalController).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors Claude Code's per-tool Permission Request components in that each
|
|
6
|
+
* request carries tool-typed data so the UI can render a meaningful preview.
|
|
7
|
+
*/
|
|
8
|
+
import type { PermissionCheckResult, PermissionQuery } from "../permissions/types.js";
|
|
9
|
+
export interface EditApprovalRequest {
|
|
10
|
+
type: "edit";
|
|
11
|
+
path: string;
|
|
12
|
+
/** Unified patch (with context lines) to show the user. */
|
|
13
|
+
diff: string;
|
|
14
|
+
fileExists: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface WriteApprovalRequest {
|
|
17
|
+
type: "write";
|
|
18
|
+
path: string;
|
|
19
|
+
/** Full pending file contents. */
|
|
20
|
+
content: string;
|
|
21
|
+
/** Unified patch from existing contents to pending contents. */
|
|
22
|
+
diff?: string;
|
|
23
|
+
fileExists: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface BashApprovalRequest {
|
|
26
|
+
type: "bash";
|
|
27
|
+
command: string;
|
|
28
|
+
cwd: string;
|
|
29
|
+
}
|
|
30
|
+
export interface LspApprovalRequest {
|
|
31
|
+
type: "lsp";
|
|
32
|
+
path: string;
|
|
33
|
+
operation: string;
|
|
34
|
+
}
|
|
35
|
+
export type ApprovalRequest = EditApprovalRequest | WriteApprovalRequest | BashApprovalRequest | LspApprovalRequest;
|
|
36
|
+
export type ApprovalDecision = {
|
|
37
|
+
action: "approve";
|
|
38
|
+
feedback?: string;
|
|
39
|
+
} | {
|
|
40
|
+
action: "reject";
|
|
41
|
+
feedback?: string;
|
|
42
|
+
};
|
|
43
|
+
export interface ApprovalController {
|
|
44
|
+
/**
|
|
45
|
+
* Decide whether a tool call should proceed. May consult the current
|
|
46
|
+
* permission mode, configured allow/deny rules, session-level allowlists,
|
|
47
|
+
* and — as a final fallback — a user-interactive UI handler.
|
|
48
|
+
*/
|
|
49
|
+
request(req: ApprovalRequest): Promise<ApprovalDecision>;
|
|
50
|
+
/**
|
|
51
|
+
* Pure rule evaluation (no UI, no mode gates). Tools that silently execute
|
|
52
|
+
* unless explicitly denied (e.g. Read, WebFetch) call this to honor
|
|
53
|
+
* user-configured deny rules without changing the UX for the common case.
|
|
54
|
+
*/
|
|
55
|
+
checkRules(query: PermissionQuery): PermissionCheckResult;
|
|
56
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval flow: tools that want a gating UI construct an ApprovalRequest and
|
|
3
|
+
* await an ApprovalDecision from the harness (via ApprovalController).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors Claude Code's per-tool Permission Request components in that each
|
|
6
|
+
* request carries tool-typed data so the UI can render a meaningful preview.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type BubbleEnvironment = "production" | "dev" | "custom";
|
|
2
|
+
export interface BubbleHomeInfo {
|
|
3
|
+
home: string;
|
|
4
|
+
environment: BubbleEnvironment;
|
|
5
|
+
}
|
|
6
|
+
export declare function getBubbleHome(): string;
|
|
7
|
+
export declare function getBubbleHomeInfo(): BubbleHomeInfo;
|
|
8
|
+
export declare function isBubbleDevMode(): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function getBubbleHome() {
|
|
4
|
+
return getBubbleHomeInfo().home;
|
|
5
|
+
}
|
|
6
|
+
export function getBubbleHomeInfo() {
|
|
7
|
+
const explicitHome = process.env.BUBBLE_HOME?.trim();
|
|
8
|
+
if (explicitHome) {
|
|
9
|
+
return { home: explicitHome, environment: "custom" };
|
|
10
|
+
}
|
|
11
|
+
if (isBubbleDevMode()) {
|
|
12
|
+
return { home: join(homedir(), ".bubble-dev"), environment: "dev" };
|
|
13
|
+
}
|
|
14
|
+
return { home: join(homedir(), ".bubble"), environment: "production" };
|
|
15
|
+
}
|
|
16
|
+
export function isBubbleDevMode() {
|
|
17
|
+
const value = process.env.BUBBLE_DEV?.trim().toLowerCase();
|
|
18
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
19
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI argument parsing.
|
|
3
|
+
*/
|
|
4
|
+
import type { PermissionMode, ThinkingLevel } from "./types.js";
|
|
5
|
+
export interface CliArgs {
|
|
6
|
+
model?: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
resume?: boolean;
|
|
10
|
+
sessionName?: string;
|
|
11
|
+
print?: boolean;
|
|
12
|
+
prompt?: string;
|
|
13
|
+
thinkingLevel?: ThinkingLevel;
|
|
14
|
+
mode?: PermissionMode;
|
|
15
|
+
/** When true, --dangerously-skip-permissions was passed; bypassPermissions is reachable via the mode cycle and auto-approves every tool. */
|
|
16
|
+
bypassEnabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function parseArgs(argv: string[]): CliArgs;
|
|
19
|
+
export declare function printHelp(): void;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI argument parsing.
|
|
3
|
+
*/
|
|
4
|
+
import { isThinkingLevel } from "./variant/thinking-level.js";
|
|
5
|
+
export function parseArgs(argv) {
|
|
6
|
+
const args = {
|
|
7
|
+
cwd: process.cwd(),
|
|
8
|
+
};
|
|
9
|
+
for (let i = 0; i < argv.length; i++) {
|
|
10
|
+
const arg = argv[i];
|
|
11
|
+
switch (arg) {
|
|
12
|
+
case "--model":
|
|
13
|
+
case "-m":
|
|
14
|
+
args.model = argv[++i];
|
|
15
|
+
break;
|
|
16
|
+
case "--cwd":
|
|
17
|
+
args.cwd = argv[++i];
|
|
18
|
+
break;
|
|
19
|
+
case "--api-key":
|
|
20
|
+
case "-k":
|
|
21
|
+
args.apiKey = argv[++i];
|
|
22
|
+
break;
|
|
23
|
+
case "--resume":
|
|
24
|
+
case "-r":
|
|
25
|
+
args.resume = true;
|
|
26
|
+
break;
|
|
27
|
+
case "--session":
|
|
28
|
+
args.sessionName = argv[++i];
|
|
29
|
+
break;
|
|
30
|
+
case "--reasoning":
|
|
31
|
+
args.thinkingLevel = "medium";
|
|
32
|
+
break;
|
|
33
|
+
case "--reasoning-effort": {
|
|
34
|
+
const value = argv[++i];
|
|
35
|
+
if (isThinkingLevel(value)) {
|
|
36
|
+
args.thinkingLevel = value;
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case "--print":
|
|
41
|
+
case "-p":
|
|
42
|
+
args.print = true;
|
|
43
|
+
break;
|
|
44
|
+
case "--plan":
|
|
45
|
+
args.mode = "plan";
|
|
46
|
+
break;
|
|
47
|
+
case "--accept-edits":
|
|
48
|
+
args.mode = "acceptEdits";
|
|
49
|
+
break;
|
|
50
|
+
case "--dangerously-skip-permissions":
|
|
51
|
+
args.bypassEnabled = true;
|
|
52
|
+
args.mode = "bypassPermissions";
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
if (!arg.startsWith("-") && !args.prompt) {
|
|
56
|
+
args.prompt = arg;
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return args;
|
|
62
|
+
}
|
|
63
|
+
export function printHelp() {
|
|
64
|
+
console.log(`
|
|
65
|
+
Usage: bubble [options] [prompt]
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
-m, --model <model> Model to use
|
|
69
|
+
--cwd <dir> Working directory (default: current)
|
|
70
|
+
-k, --api-key <key> API key for the active provider
|
|
71
|
+
-r, --resume Resume a previous session (latest by default)
|
|
72
|
+
--session <name> Session name to create or resume
|
|
73
|
+
--reasoning Enable reasoning mode at medium effort
|
|
74
|
+
--reasoning-effort <l> Set reasoning effort: off|minimal|low|medium|high|xhigh|max
|
|
75
|
+
--plan Start in plan mode (read-only investigation; propose before executing)
|
|
76
|
+
--accept-edits Start with edits/writes auto-approved (bash still prompts)
|
|
77
|
+
--dangerously-skip-permissions
|
|
78
|
+
Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
|
|
79
|
+
-p, --print Non-interactive mode (single prompt)
|
|
80
|
+
-h, --help Show this help
|
|
81
|
+
`);
|
|
82
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-level configuration manager.
|
|
3
|
+
*
|
|
4
|
+
* Uses a single JSON file in Bubble home, normally ~/.bubble/config.json.
|
|
5
|
+
*/
|
|
6
|
+
import type { ProviderProfile } from "./provider-registry.js";
|
|
7
|
+
import type { ThinkingLevel } from "./types.js";
|
|
8
|
+
export interface UserConfigData {
|
|
9
|
+
defaultModel?: string;
|
|
10
|
+
defaultThinkingLevel?: ThinkingLevel;
|
|
11
|
+
skillPaths?: string[];
|
|
12
|
+
theme?: Record<string, string>;
|
|
13
|
+
recentModels?: string[];
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
providers?: ProviderProfile[];
|
|
16
|
+
defaultProvider?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class UserConfig {
|
|
19
|
+
private data;
|
|
20
|
+
constructor();
|
|
21
|
+
private load;
|
|
22
|
+
private save;
|
|
23
|
+
getDefaultModel(): string | undefined;
|
|
24
|
+
setDefaultModel(model: string): void;
|
|
25
|
+
getDefaultThinkingLevel(): ThinkingLevel | undefined;
|
|
26
|
+
setDefaultThinkingLevel(level: ThinkingLevel): void;
|
|
27
|
+
getRecentModels(): string[];
|
|
28
|
+
pushRecentModel(model: string): void;
|
|
29
|
+
getApiKey(): string | undefined;
|
|
30
|
+
setApiKey(key: string): void;
|
|
31
|
+
getProviders(): ProviderProfile[];
|
|
32
|
+
setProviders(providers: ProviderProfile[]): void;
|
|
33
|
+
getDefaultProvider(): string | undefined;
|
|
34
|
+
setDefaultProvider(id: string): void;
|
|
35
|
+
getSkillPaths(): string[];
|
|
36
|
+
setSkillPaths(paths: string[]): void;
|
|
37
|
+
getTheme(): Record<string, string>;
|
|
38
|
+
setTheme(theme: Record<string, string>): void;
|
|
39
|
+
}
|
|
40
|
+
/** Mask an API key for safe display. */
|
|
41
|
+
export declare function maskKey(key: string): string;
|