@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,96 @@
|
|
|
1
|
+
import { classifyTask } from "../agent/task-classifier.js";
|
|
2
|
+
import { EvidenceTracker } from "../agent/evidence-tracker.js";
|
|
3
|
+
import { ExecutionGovernor } from "../agent/execution-governor.js";
|
|
4
|
+
import { arbitrateToolCall } from "../agent/tool-arbiter.js";
|
|
5
|
+
import { buildTaskSummaryReminder, buildWorkflowPhaseReminder } from "../prompt/reminders.js";
|
|
6
|
+
import { formatCoverageSummary, resolveWorkflowPhase } from "./workflow.js";
|
|
7
|
+
export function createDefaultHooks() {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
beforeTurn(ctx) {
|
|
11
|
+
const taskType = classifyTask(ctx.input);
|
|
12
|
+
ctx.state.taskType = taskType;
|
|
13
|
+
ctx.state.governor = new ExecutionGovernor(taskType);
|
|
14
|
+
if (taskType === "security_investigation") {
|
|
15
|
+
ctx.state.evidenceTracker = new EvidenceTracker();
|
|
16
|
+
ctx.state.workflowPhase = "investigate";
|
|
17
|
+
ctx.state.workflowKey = "";
|
|
18
|
+
}
|
|
19
|
+
for (const reminder of ctx.state.governor.consumePendingReminders()) {
|
|
20
|
+
ctx.queueReminder(reminder);
|
|
21
|
+
}
|
|
22
|
+
ctx.agent.compactResidentHistory();
|
|
23
|
+
},
|
|
24
|
+
beforeModelCall(ctx) {
|
|
25
|
+
ctx.agent.compactResidentHistory();
|
|
26
|
+
if (ctx.state.governor) {
|
|
27
|
+
ctx.toolEntries = ctx.state.governor.filterToolDefinitions(ctx.toolEntries);
|
|
28
|
+
}
|
|
29
|
+
if (ctx.state.taskType === "security_investigation" && ctx.state.evidenceTracker && ctx.state.governor) {
|
|
30
|
+
const coverage = ctx.state.evidenceTracker.snapshot();
|
|
31
|
+
const phase = resolveWorkflowPhase({
|
|
32
|
+
coreCoverageComplete: ctx.state.evidenceTracker.isCoreCoverageComplete(),
|
|
33
|
+
searchFrozen: ctx.state.governor.snapshot().searchFrozen,
|
|
34
|
+
});
|
|
35
|
+
ctx.state.workflowPhase = phase;
|
|
36
|
+
const summary = formatCoverageSummary(coverage);
|
|
37
|
+
const key = `${phase}:${ctx.state.evidenceTracker.key()}:${ctx.state.governor.snapshot().searchFrozen ? "1" : "0"}`;
|
|
38
|
+
if (ctx.state.workflowKey !== key) {
|
|
39
|
+
ctx.state.workflowKey = key;
|
|
40
|
+
ctx.queueReminder(buildWorkflowPhaseReminder({
|
|
41
|
+
phase,
|
|
42
|
+
covered: summary.covered,
|
|
43
|
+
pending: summary.pending,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
afterTurn(ctx) {
|
|
49
|
+
ctx.agent.compactResidentHistory();
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
beforeToolCall(ctx) {
|
|
54
|
+
const arbitration = arbitrateToolCall(ctx.toolCall);
|
|
55
|
+
ctx.replaceToolCall({ ...arbitration.toolCall, ...(arbitration.note ? { arbiterNote: arbitration.note } : {}) });
|
|
56
|
+
const decision = ctx.state.governor?.beforeToolCall(ctx.toolCall);
|
|
57
|
+
if (decision?.blockedResult) {
|
|
58
|
+
ctx.blockToolCall(decision.blockedResult);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
afterToolCall(ctx) {
|
|
62
|
+
if (ctx.toolCall.arbiterNote) {
|
|
63
|
+
ctx.replaceResult({
|
|
64
|
+
...ctx.result,
|
|
65
|
+
metadata: {
|
|
66
|
+
...ctx.result.metadata,
|
|
67
|
+
arbiterNote: ctx.toolCall.arbiterNote,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
ctx.state.evidenceTracker?.observe(ctx.toolCall, ctx.result);
|
|
72
|
+
ctx.state.governor?.afterToolResult(ctx.toolCall, ctx.result);
|
|
73
|
+
if (ctx.toolCall.name === "task") {
|
|
74
|
+
ctx.queueReminder(buildTaskSummaryReminder());
|
|
75
|
+
}
|
|
76
|
+
if (ctx.state.governor) {
|
|
77
|
+
for (const reminder of ctx.state.governor.consumePendingReminders()) {
|
|
78
|
+
ctx.queueReminder(reminder);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
beforeContinuation(ctx) {
|
|
83
|
+
if (ctx.state.taskType === "security_investigation" && ctx.state.evidenceTracker?.isCoreCoverageComplete()) {
|
|
84
|
+
ctx.requestTextOnlyTurn("Core security investigation evidence has been collected. Summarize the findings instead of continuing with more tool calls.");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const allSearchResultsWereLowSignal = ctx.toolCalls.length > 0
|
|
88
|
+
&& ctx.toolCalls.every((toolCall) => ["glob", "grep", "bash", "web_search", "web_fetch"].includes(toolCall.name))
|
|
89
|
+
&& ctx.toolResults.every((result) => result.status === "no_match" || result.status === "blocked");
|
|
90
|
+
if (ctx.state.governor?.snapshot().searchFrozen && allSearchResultsWereLowSignal) {
|
|
91
|
+
ctx.requestTextOnlyTurn("Search continuation has become low-yield. Summarize the strongest evidence already collected instead of continuing broad exploration.");
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Agent } from "../agent.js";
|
|
2
|
+
import type { ContentPart, ParsedToolCall, ToolRegistryEntry, ToolResult } from "../types.js";
|
|
3
|
+
import type { TaskType } from "../agent/task-classifier.js";
|
|
4
|
+
import type { ExecutionGovernor } from "../agent/execution-governor.js";
|
|
5
|
+
import type { EvidenceTracker } from "../agent/evidence-tracker.js";
|
|
6
|
+
import type { WorkflowPhase } from "./workflow.js";
|
|
7
|
+
export interface TurnHookState {
|
|
8
|
+
taskType?: TaskType;
|
|
9
|
+
governor?: ExecutionGovernor;
|
|
10
|
+
evidenceTracker?: EvidenceTracker;
|
|
11
|
+
workflowPhase?: WorkflowPhase;
|
|
12
|
+
workflowKey?: string;
|
|
13
|
+
turnCount?: number;
|
|
14
|
+
forceTextOnlyReason?: string;
|
|
15
|
+
taskBudget?: {
|
|
16
|
+
total: number;
|
|
17
|
+
spent: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface TurnHookContext {
|
|
21
|
+
agent: Agent;
|
|
22
|
+
cwd: string;
|
|
23
|
+
input: string | ContentPart[];
|
|
24
|
+
state: TurnHookState;
|
|
25
|
+
queueReminder: (reminder: string) => void;
|
|
26
|
+
flushReminders: () => void;
|
|
27
|
+
}
|
|
28
|
+
export interface BeforeModelCallHookContext extends TurnHookContext {
|
|
29
|
+
toolEntries: ToolRegistryEntry[];
|
|
30
|
+
disableTools: (reason: string) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface BeforeToolCallHookContext extends TurnHookContext {
|
|
33
|
+
toolCall: ParsedToolCall & {
|
|
34
|
+
arbiterNote?: string;
|
|
35
|
+
};
|
|
36
|
+
blockedResult?: ToolResult;
|
|
37
|
+
replaceToolCall: (toolCall: ParsedToolCall & {
|
|
38
|
+
arbiterNote?: string;
|
|
39
|
+
}) => void;
|
|
40
|
+
blockToolCall: (result: ToolResult) => void;
|
|
41
|
+
}
|
|
42
|
+
export interface AfterToolCallHookContext extends TurnHookContext {
|
|
43
|
+
toolCall: ParsedToolCall & {
|
|
44
|
+
arbiterNote?: string;
|
|
45
|
+
};
|
|
46
|
+
result: ToolResult;
|
|
47
|
+
replaceResult: (result: ToolResult) => void;
|
|
48
|
+
}
|
|
49
|
+
export interface BeforeContinuationHookContext extends TurnHookContext {
|
|
50
|
+
toolCalls: Array<ParsedToolCall & {
|
|
51
|
+
arbiterNote?: string;
|
|
52
|
+
}>;
|
|
53
|
+
toolResults: ToolResult[];
|
|
54
|
+
requestTextOnlyTurn: (reason: string) => void;
|
|
55
|
+
}
|
|
56
|
+
export interface TurnHooks {
|
|
57
|
+
beforeTurn?: (ctx: TurnHookContext) => void | Promise<void>;
|
|
58
|
+
beforeModelCall?: (ctx: BeforeModelCallHookContext) => void | Promise<void>;
|
|
59
|
+
beforeToolCall?: (ctx: BeforeToolCallHookContext) => void | Promise<void>;
|
|
60
|
+
afterToolCall?: (ctx: AfterToolCallHookContext) => void | Promise<void>;
|
|
61
|
+
beforeContinuation?: (ctx: BeforeContinuationHookContext) => void | Promise<void>;
|
|
62
|
+
afterTurn?: (ctx: TurnHookContext) => void | Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
export declare class HookBus {
|
|
65
|
+
private beforeTurnHooks;
|
|
66
|
+
private beforeModelCallHooks;
|
|
67
|
+
private beforeToolCallHooks;
|
|
68
|
+
private afterToolCallHooks;
|
|
69
|
+
private beforeContinuationHooks;
|
|
70
|
+
private afterTurnHooks;
|
|
71
|
+
register(hooks: TurnHooks): void;
|
|
72
|
+
runBeforeTurn(ctx: TurnHookContext): Promise<void>;
|
|
73
|
+
runBeforeModelCall(ctx: BeforeModelCallHookContext): Promise<void>;
|
|
74
|
+
runBeforeToolCall(ctx: BeforeToolCallHookContext): Promise<void>;
|
|
75
|
+
runAfterToolCall(ctx: AfterToolCallHookContext): Promise<void>;
|
|
76
|
+
runBeforeContinuation(ctx: BeforeContinuationHookContext): Promise<void>;
|
|
77
|
+
runAfterTurn(ctx: TurnHookContext): Promise<void>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class HookBus {
|
|
2
|
+
beforeTurnHooks = [];
|
|
3
|
+
beforeModelCallHooks = [];
|
|
4
|
+
beforeToolCallHooks = [];
|
|
5
|
+
afterToolCallHooks = [];
|
|
6
|
+
beforeContinuationHooks = [];
|
|
7
|
+
afterTurnHooks = [];
|
|
8
|
+
register(hooks) {
|
|
9
|
+
if (hooks.beforeTurn)
|
|
10
|
+
this.beforeTurnHooks.push(hooks.beforeTurn);
|
|
11
|
+
if (hooks.beforeModelCall)
|
|
12
|
+
this.beforeModelCallHooks.push(hooks.beforeModelCall);
|
|
13
|
+
if (hooks.beforeToolCall)
|
|
14
|
+
this.beforeToolCallHooks.push(hooks.beforeToolCall);
|
|
15
|
+
if (hooks.afterToolCall)
|
|
16
|
+
this.afterToolCallHooks.push(hooks.afterToolCall);
|
|
17
|
+
if (hooks.beforeContinuation)
|
|
18
|
+
this.beforeContinuationHooks.push(hooks.beforeContinuation);
|
|
19
|
+
if (hooks.afterTurn)
|
|
20
|
+
this.afterTurnHooks.push(hooks.afterTurn);
|
|
21
|
+
}
|
|
22
|
+
async runBeforeTurn(ctx) {
|
|
23
|
+
for (const hook of this.beforeTurnHooks) {
|
|
24
|
+
await hook(ctx);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async runBeforeModelCall(ctx) {
|
|
28
|
+
for (const hook of this.beforeModelCallHooks) {
|
|
29
|
+
await hook(ctx);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async runBeforeToolCall(ctx) {
|
|
33
|
+
for (const hook of this.beforeToolCallHooks) {
|
|
34
|
+
await hook(ctx);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async runAfterToolCall(ctx) {
|
|
38
|
+
for (const hook of this.afterToolCallHooks) {
|
|
39
|
+
await hook(ctx);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async runBeforeContinuation(ctx) {
|
|
43
|
+
for (const hook of this.beforeContinuationHooks) {
|
|
44
|
+
await hook(ctx);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async runAfterTurn(ctx) {
|
|
48
|
+
for (const hook of this.afterTurnHooks) {
|
|
49
|
+
await hook(ctx);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { InvestigationCoverage } from "../agent/evidence-tracker.js";
|
|
2
|
+
export type WorkflowPhase = "investigate" | "correlate" | "conclude";
|
|
3
|
+
export declare function resolveWorkflowPhase(input: {
|
|
4
|
+
coreCoverageComplete: boolean;
|
|
5
|
+
searchFrozen: boolean;
|
|
6
|
+
}): WorkflowPhase;
|
|
7
|
+
export declare function formatCoverageSummary(coverage: InvestigationCoverage): {
|
|
8
|
+
covered: string[];
|
|
9
|
+
pending: string[];
|
|
10
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function resolveWorkflowPhase(input) {
|
|
2
|
+
if (input.coreCoverageComplete) {
|
|
3
|
+
return "conclude";
|
|
4
|
+
}
|
|
5
|
+
if (input.searchFrozen) {
|
|
6
|
+
return "correlate";
|
|
7
|
+
}
|
|
8
|
+
return "investigate";
|
|
9
|
+
}
|
|
10
|
+
export function formatCoverageSummary(coverage) {
|
|
11
|
+
const items = [
|
|
12
|
+
{ label: "config load paths", done: coverage.configLoadPaths },
|
|
13
|
+
{ label: "environment variable reads", done: coverage.envReads },
|
|
14
|
+
{ label: "persistence paths", done: coverage.persistencePaths },
|
|
15
|
+
{ label: "exposure paths", done: coverage.exposurePaths },
|
|
16
|
+
{ label: "masking or redaction signals", done: coverage.maskingSignals },
|
|
17
|
+
];
|
|
18
|
+
return {
|
|
19
|
+
covered: items.filter((item) => item.done).map((item) => item.label),
|
|
20
|
+
pending: items.filter((item) => !item.done).map((item) => item.label),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PermissionMode } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Display metadata for each permission mode. Mirrors Claude Code's
|
|
4
|
+
* PERMISSION_MODE_CONFIG — kept narrow to what the TUI needs today.
|
|
5
|
+
*/
|
|
6
|
+
export interface PermissionModeInfo {
|
|
7
|
+
title: string;
|
|
8
|
+
shortTitle: string;
|
|
9
|
+
/** Single/double-char prefix shown in the footer badge. Empty = no badge. */
|
|
10
|
+
symbol: string;
|
|
11
|
+
/** Theme colour key. */
|
|
12
|
+
color: "muted" | "accent" | "success" | "warning" | "error";
|
|
13
|
+
}
|
|
14
|
+
export declare const PERMISSION_MODE_INFO: Record<PermissionMode, PermissionModeInfo>;
|
|
15
|
+
/**
|
|
16
|
+
* Cycle order for the interactive mode keybind. This intentionally mirrors
|
|
17
|
+
* opencode's primary-agent switch: the keybind toggles Build and Plan only.
|
|
18
|
+
* Permission presets like acceptEdits/bypassPermissions remain opt-in through
|
|
19
|
+
* flags or commands and are never reached accidentally from Tab.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getNextPermissionMode(current: PermissionMode, _options?: {
|
|
22
|
+
bypassEnabled?: boolean;
|
|
23
|
+
}): PermissionMode;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const PERMISSION_MODE_INFO = {
|
|
2
|
+
default: { title: "Default", shortTitle: "default", symbol: "", color: "muted" },
|
|
3
|
+
acceptEdits: { title: "Accept edits", shortTitle: "accept edits", symbol: "⏵⏵", color: "success" },
|
|
4
|
+
plan: { title: "Plan", shortTitle: "plan", symbol: "⏸", color: "accent" },
|
|
5
|
+
bypassPermissions: { title: "Bypass permissions", shortTitle: "bypass", symbol: "⏵⏵", color: "error" },
|
|
6
|
+
dontAsk: { title: "Do not ask", shortTitle: "silent", symbol: "·", color: "warning" },
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Cycle order for the interactive mode keybind. This intentionally mirrors
|
|
10
|
+
* opencode's primary-agent switch: the keybind toggles Build and Plan only.
|
|
11
|
+
* Permission presets like acceptEdits/bypassPermissions remain opt-in through
|
|
12
|
+
* flags or commands and are never reached accidentally from Tab.
|
|
13
|
+
*/
|
|
14
|
+
export function getNextPermissionMode(current, _options = {}) {
|
|
15
|
+
if (current === "plan")
|
|
16
|
+
return "default";
|
|
17
|
+
if (current === "bypassPermissions" || current === "dontAsk")
|
|
18
|
+
return "default";
|
|
19
|
+
return "plan";
|
|
20
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ParsedRule, PermissionCheckResult, PermissionDecision, PermissionQuery, PermissionRule, PermissionRuleSet } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a single rule string, e.g. `Bash(git status:*)`.
|
|
4
|
+
*
|
|
5
|
+
* Whitespace around the whole expression is trimmed; whitespace inside the
|
|
6
|
+
* pattern is preserved (Bash patterns are tokenized later, paths may legitimately
|
|
7
|
+
* contain spaces inside globs).
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseRule(input: string): ParsedRule;
|
|
10
|
+
/**
|
|
11
|
+
* Parse a list of rule strings. Invalid entries are reported in `errors`; valid
|
|
12
|
+
* ones populate `rules`. Callers decide whether to surface errors or ignore.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseRules(inputs: string[]): {
|
|
15
|
+
rules: PermissionRule[];
|
|
16
|
+
errors: {
|
|
17
|
+
source: string;
|
|
18
|
+
message: string;
|
|
19
|
+
}[];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Does `rule` match `query`?
|
|
23
|
+
*
|
|
24
|
+
* Tool name must match (or rule.tool is "*"). If the rule has no pattern, it
|
|
25
|
+
* matches any use of that tool. Otherwise the tool-specific matcher decides.
|
|
26
|
+
*/
|
|
27
|
+
export declare function matchRule(rule: PermissionRule, query: PermissionQuery): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Evaluate a rule set against a query. Deny wins; otherwise first allow wins;
|
|
30
|
+
* otherwise "ask".
|
|
31
|
+
*/
|
|
32
|
+
export declare function checkPermission(rules: PermissionRuleSet, query: PermissionQuery): PermissionCheckResult;
|
|
33
|
+
/**
|
|
34
|
+
* Convenience: given raw `allow`/`deny` string arrays from settings, produce a
|
|
35
|
+
* ready-to-use rule set (silently dropping parse errors — callers that want
|
|
36
|
+
* diagnostics should call parseRules directly).
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildRuleSet(allow: string[], deny: string[]): PermissionRuleSet;
|
|
39
|
+
export type { PermissionDecision };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
import picomatch from "picomatch";
|
|
4
|
+
const KNOWN_TOOLS = new Set([
|
|
5
|
+
"Bash",
|
|
6
|
+
"Read",
|
|
7
|
+
"Write",
|
|
8
|
+
"Edit",
|
|
9
|
+
"Lsp",
|
|
10
|
+
"WebFetch",
|
|
11
|
+
"WebSearch",
|
|
12
|
+
"*",
|
|
13
|
+
]);
|
|
14
|
+
const RULE_SHAPE = /^([A-Za-z_*][A-Za-z0-9_]*)(?:\(([^)]*)\))?$/;
|
|
15
|
+
/**
|
|
16
|
+
* Parse a single rule string, e.g. `Bash(git status:*)`.
|
|
17
|
+
*
|
|
18
|
+
* Whitespace around the whole expression is trimmed; whitespace inside the
|
|
19
|
+
* pattern is preserved (Bash patterns are tokenized later, paths may legitimately
|
|
20
|
+
* contain spaces inside globs).
|
|
21
|
+
*/
|
|
22
|
+
export function parseRule(input) {
|
|
23
|
+
const source = input;
|
|
24
|
+
const trimmed = input.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
error: { source, message: "Rule is empty." },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const match = RULE_SHAPE.exec(trimmed);
|
|
32
|
+
if (!match) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
error: {
|
|
36
|
+
source,
|
|
37
|
+
message: `Rule must look like "Tool" or "Tool(pattern)"; got: ${trimmed}`,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const tool = match[1];
|
|
42
|
+
const rawPattern = match[2];
|
|
43
|
+
if (tool !== "*" && !KNOWN_TOOLS.has(tool)) {
|
|
44
|
+
// Allow unknown tool names through — future MCP tools etc. can reuse this
|
|
45
|
+
// parser. But empty bodies with no paren and unknown names are fine too.
|
|
46
|
+
}
|
|
47
|
+
const pattern = rawPattern !== undefined ? rawPattern.trim() : undefined;
|
|
48
|
+
if (pattern !== undefined && pattern.length === 0) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: {
|
|
52
|
+
source,
|
|
53
|
+
message: `Rule has empty parentheses; drop the parens to match all uses of ${tool}.`,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
rule: { tool, pattern, source },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parse a list of rule strings. Invalid entries are reported in `errors`; valid
|
|
64
|
+
* ones populate `rules`. Callers decide whether to surface errors or ignore.
|
|
65
|
+
*/
|
|
66
|
+
export function parseRules(inputs) {
|
|
67
|
+
const rules = [];
|
|
68
|
+
const errors = [];
|
|
69
|
+
for (const input of inputs) {
|
|
70
|
+
const parsed = parseRule(input);
|
|
71
|
+
if (parsed.ok) {
|
|
72
|
+
rules.push(parsed.rule);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
errors.push(parsed.error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { rules, errors };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Does `rule` match `query`?
|
|
82
|
+
*
|
|
83
|
+
* Tool name must match (or rule.tool is "*"). If the rule has no pattern, it
|
|
84
|
+
* matches any use of that tool. Otherwise the tool-specific matcher decides.
|
|
85
|
+
*/
|
|
86
|
+
export function matchRule(rule, query) {
|
|
87
|
+
if (rule.tool !== "*" && rule.tool !== query.tool) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (rule.pattern === undefined) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
const toolKey = rule.tool === "*" ? query.tool : rule.tool;
|
|
94
|
+
switch (toolKey) {
|
|
95
|
+
case "Bash":
|
|
96
|
+
if (!("command" in query) || typeof query.command !== "string")
|
|
97
|
+
return false;
|
|
98
|
+
return matchBash(rule.pattern, query.command);
|
|
99
|
+
case "Read":
|
|
100
|
+
case "Write":
|
|
101
|
+
case "Edit":
|
|
102
|
+
case "Lsp":
|
|
103
|
+
if (!("path" in query) || !("cwd" in query))
|
|
104
|
+
return false;
|
|
105
|
+
return matchPath(rule.pattern, query.path, query.cwd);
|
|
106
|
+
case "WebFetch":
|
|
107
|
+
if (!("url" in query) || typeof query.url !== "string")
|
|
108
|
+
return false;
|
|
109
|
+
return matchDomain(rule.pattern, query.url);
|
|
110
|
+
case "WebSearch":
|
|
111
|
+
// WebSearch rules only support tool-level match for v1.
|
|
112
|
+
return false;
|
|
113
|
+
default:
|
|
114
|
+
// Unknown tool with a pattern: be conservative, don't match.
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Evaluate a rule set against a query. Deny wins; otherwise first allow wins;
|
|
120
|
+
* otherwise "ask".
|
|
121
|
+
*/
|
|
122
|
+
export function checkPermission(rules, query) {
|
|
123
|
+
for (const rule of rules.deny) {
|
|
124
|
+
if (matchRule(rule, query)) {
|
|
125
|
+
return { decision: "deny", rule };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const rule of rules.allow) {
|
|
129
|
+
if (matchRule(rule, query)) {
|
|
130
|
+
return { decision: "allow", rule };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { decision: "ask" };
|
|
134
|
+
}
|
|
135
|
+
// --- per-tool matchers ---------------------------------------------------
|
|
136
|
+
/**
|
|
137
|
+
* Bash pattern matching.
|
|
138
|
+
*
|
|
139
|
+
* - `git status` → command tokens equal ["git","status"] exactly
|
|
140
|
+
* - `git status:*` → command tokens start with ["git","status"]
|
|
141
|
+
*
|
|
142
|
+
* Tokenization splits on whitespace. Shell control tokens (`&&`, `|`, `;`) are
|
|
143
|
+
* treated as plain tokens; a rule that includes them would have to match them
|
|
144
|
+
* literally. v1 does not try to parse shell grammar.
|
|
145
|
+
*/
|
|
146
|
+
function matchBash(pattern, command) {
|
|
147
|
+
const cmdTokens = tokenize(command);
|
|
148
|
+
let ruleTokens = tokenize(pattern);
|
|
149
|
+
let prefixMatch = false;
|
|
150
|
+
if (ruleTokens.length > 0) {
|
|
151
|
+
const last = ruleTokens[ruleTokens.length - 1];
|
|
152
|
+
if (last === ":*") {
|
|
153
|
+
prefixMatch = true;
|
|
154
|
+
ruleTokens = ruleTokens.slice(0, -1);
|
|
155
|
+
}
|
|
156
|
+
else if (last.endsWith(":*")) {
|
|
157
|
+
prefixMatch = true;
|
|
158
|
+
ruleTokens = ruleTokens.slice(0, -1).concat([last.slice(0, -2)]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (ruleTokens.length === 0) {
|
|
162
|
+
return prefixMatch; // `Bash(:*)` would match anything; keep behavior defined
|
|
163
|
+
}
|
|
164
|
+
if (prefixMatch) {
|
|
165
|
+
if (ruleTokens.length > cmdTokens.length)
|
|
166
|
+
return false;
|
|
167
|
+
return ruleTokens.every((tok, i) => tok === cmdTokens[i]);
|
|
168
|
+
}
|
|
169
|
+
if (ruleTokens.length !== cmdTokens.length)
|
|
170
|
+
return false;
|
|
171
|
+
return ruleTokens.every((tok, i) => tok === cmdTokens[i]);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Path pattern matching via picomatch (glob). The rule pattern is expanded for
|
|
175
|
+
* `~` (home) and resolved relative to cwd if not absolute. The query path is
|
|
176
|
+
* resolved the same way to compare canonical absolute paths.
|
|
177
|
+
*/
|
|
178
|
+
function matchPath(pattern, queryPath, cwd) {
|
|
179
|
+
const expandedPattern = expandHome(pattern);
|
|
180
|
+
const absolutePattern = isAbsolute(expandedPattern)
|
|
181
|
+
? expandedPattern
|
|
182
|
+
: resolve(cwd, expandedPattern);
|
|
183
|
+
const absoluteQuery = isAbsolute(queryPath)
|
|
184
|
+
? queryPath
|
|
185
|
+
: resolve(cwd, queryPath);
|
|
186
|
+
const matcher = picomatch(absolutePattern, { dot: true });
|
|
187
|
+
return matcher(absoluteQuery);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* WebFetch domain matching. Pattern must be `domain:<host>`. Match is true if
|
|
191
|
+
* the URL host equals `<host>` or is a subdomain of it.
|
|
192
|
+
*
|
|
193
|
+
* domain:github.com → matches https://github.com/... and https://api.github.com/...
|
|
194
|
+
*/
|
|
195
|
+
function matchDomain(pattern, url) {
|
|
196
|
+
const prefix = "domain:";
|
|
197
|
+
if (!pattern.startsWith(prefix))
|
|
198
|
+
return false;
|
|
199
|
+
const expectedHost = pattern.slice(prefix.length).trim().toLowerCase();
|
|
200
|
+
if (!expectedHost)
|
|
201
|
+
return false;
|
|
202
|
+
let host;
|
|
203
|
+
try {
|
|
204
|
+
host = new URL(url).hostname.toLowerCase();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (host === expectedHost)
|
|
210
|
+
return true;
|
|
211
|
+
return host.endsWith("." + expectedHost);
|
|
212
|
+
}
|
|
213
|
+
// --- small helpers -------------------------------------------------------
|
|
214
|
+
function tokenize(input) {
|
|
215
|
+
return input.trim().split(/\s+/).filter(Boolean);
|
|
216
|
+
}
|
|
217
|
+
function expandHome(p) {
|
|
218
|
+
if (p === "~")
|
|
219
|
+
return homedir();
|
|
220
|
+
if (p.startsWith("~/"))
|
|
221
|
+
return resolve(homedir(), p.slice(2));
|
|
222
|
+
return p;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Convenience: given raw `allow`/`deny` string arrays from settings, produce a
|
|
226
|
+
* ready-to-use rule set (silently dropping parse errors — callers that want
|
|
227
|
+
* diagnostics should call parseRules directly).
|
|
228
|
+
*/
|
|
229
|
+
export function buildRuleSet(allow, deny) {
|
|
230
|
+
return {
|
|
231
|
+
allow: parseRules(allow).rules,
|
|
232
|
+
deny: parseRules(deny).rules,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent permission settings.
|
|
3
|
+
*
|
|
4
|
+
* Three scopes, lowest to highest precedence:
|
|
5
|
+
*
|
|
6
|
+
* user — ~/.bubble/settings.json (cross-project preferences)
|
|
7
|
+
* project — <cwd>/.bubble/settings.json (team-shared, check into git)
|
|
8
|
+
* local — <cwd>/.bubble/settings.local.json (personal, gitignore)
|
|
9
|
+
*
|
|
10
|
+
* `defaultMode` uses last-wins precedence (local beats project beats user).
|
|
11
|
+
* `allow` / `deny` arrays are concatenated across scopes (with the rule text
|
|
12
|
+
* itself carrying provenance via `PermissionRule.source`).
|
|
13
|
+
*
|
|
14
|
+
* Parse errors do not fail the load; they collect into `diagnostics` so callers
|
|
15
|
+
* can surface them in /permissions or on startup without taking the agent down.
|
|
16
|
+
*/
|
|
17
|
+
import type { PermissionMode } from "../types.js";
|
|
18
|
+
import { type LspConfig } from "../lsp/config.js";
|
|
19
|
+
import type { PermissionRuleSet } from "./types.js";
|
|
20
|
+
export type SettingsScope = "user" | "project" | "local";
|
|
21
|
+
export type RuleList = "allow" | "deny";
|
|
22
|
+
export interface RawSettings {
|
|
23
|
+
lsp?: unknown;
|
|
24
|
+
permissions?: {
|
|
25
|
+
defaultMode?: string;
|
|
26
|
+
allow?: string[];
|
|
27
|
+
deny?: string[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface SettingsDiagnostic {
|
|
31
|
+
scope: SettingsScope;
|
|
32
|
+
path: string;
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
export interface MergedSettings {
|
|
36
|
+
defaultMode?: PermissionMode;
|
|
37
|
+
lsp?: LspConfig;
|
|
38
|
+
ruleSet: PermissionRuleSet;
|
|
39
|
+
diagnostics: SettingsDiagnostic[];
|
|
40
|
+
}
|
|
41
|
+
export interface SettingsManagerOptions {
|
|
42
|
+
/** Override for Bubble home. Respects BUBBLE_HOME/BUBBLE_DEV env vars by default. */
|
|
43
|
+
bubbleHome?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare class SettingsManager {
|
|
46
|
+
private readonly cwd;
|
|
47
|
+
private readonly paths;
|
|
48
|
+
private raw;
|
|
49
|
+
private fileDiagnostics;
|
|
50
|
+
constructor(cwd: string, options?: SettingsManagerOptions);
|
|
51
|
+
/** Re-read all three files from disk. */
|
|
52
|
+
reload(): void;
|
|
53
|
+
getPath(scope: SettingsScope): string;
|
|
54
|
+
/** Merged view for runtime consumption. Does not hit disk — call `reload()` first if stale. */
|
|
55
|
+
getMerged(): MergedSettings;
|
|
56
|
+
/**
|
|
57
|
+
* Add a rule to the specified list in the specified scope. Creates the file
|
|
58
|
+
* and parent directories if needed. Silently skips if the exact string is
|
|
59
|
+
* already present.
|
|
60
|
+
*
|
|
61
|
+
* Returns true if the file was written.
|
|
62
|
+
*/
|
|
63
|
+
addRule(scope: SettingsScope, list: RuleList, rule: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Remove the first matching rule (by exact string) from the specified list
|
|
66
|
+
* in the specified scope. Returns true if a rule was removed.
|
|
67
|
+
*/
|
|
68
|
+
removeRule(scope: SettingsScope, list: RuleList, rule: string): boolean;
|
|
69
|
+
private readFile;
|
|
70
|
+
private writeFile;
|
|
71
|
+
}
|