@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.
Files changed (248) hide show
  1. package/README.md +70 -0
  2. package/dist/agent/evidence-tracker.d.ts +15 -0
  3. package/dist/agent/evidence-tracker.js +93 -0
  4. package/dist/agent/execution-governor.d.ts +30 -0
  5. package/dist/agent/execution-governor.js +169 -0
  6. package/dist/agent/subtask-policy.d.ts +14 -0
  7. package/dist/agent/subtask-policy.js +60 -0
  8. package/dist/agent/task-classifier.d.ts +3 -0
  9. package/dist/agent/task-classifier.js +36 -0
  10. package/dist/agent/tool-arbiter.d.ts +7 -0
  11. package/dist/agent/tool-arbiter.js +33 -0
  12. package/dist/agent/tool-intent.d.ts +20 -0
  13. package/dist/agent/tool-intent.js +176 -0
  14. package/dist/agent.d.ts +95 -0
  15. package/dist/agent.js +672 -0
  16. package/dist/approval/controller.d.ts +48 -0
  17. package/dist/approval/controller.js +78 -0
  18. package/dist/approval/danger.d.ts +13 -0
  19. package/dist/approval/danger.js +55 -0
  20. package/dist/approval/diff-hunks.d.ts +12 -0
  21. package/dist/approval/diff-hunks.js +32 -0
  22. package/dist/approval/session-cache.d.ts +35 -0
  23. package/dist/approval/session-cache.js +68 -0
  24. package/dist/approval/tool-helper.d.ts +14 -0
  25. package/dist/approval/tool-helper.js +32 -0
  26. package/dist/approval/types.d.ts +56 -0
  27. package/dist/approval/types.js +8 -0
  28. package/dist/bubble-home.d.ts +8 -0
  29. package/dist/bubble-home.js +19 -0
  30. package/dist/cli.d.ts +19 -0
  31. package/dist/cli.js +82 -0
  32. package/dist/config.d.ts +41 -0
  33. package/dist/config.js +144 -0
  34. package/dist/context/budget.d.ts +21 -0
  35. package/dist/context/budget.js +72 -0
  36. package/dist/context/compact-llm.d.ts +16 -0
  37. package/dist/context/compact-llm.js +132 -0
  38. package/dist/context/compact.d.ts +15 -0
  39. package/dist/context/compact.js +251 -0
  40. package/dist/context/overflow.d.ts +9 -0
  41. package/dist/context/overflow.js +46 -0
  42. package/dist/context/projector.d.ts +26 -0
  43. package/dist/context/projector.js +150 -0
  44. package/dist/context/prune.d.ts +9 -0
  45. package/dist/context/prune.js +111 -0
  46. package/dist/lsp/config.d.ts +18 -0
  47. package/dist/lsp/config.js +58 -0
  48. package/dist/lsp/diagnostics.d.ts +24 -0
  49. package/dist/lsp/diagnostics.js +103 -0
  50. package/dist/lsp/index.d.ts +3 -0
  51. package/dist/lsp/index.js +3 -0
  52. package/dist/lsp/service.d.ts +85 -0
  53. package/dist/lsp/service.js +695 -0
  54. package/dist/main.d.ts +5 -0
  55. package/dist/main.js +352 -0
  56. package/dist/mcp/client.d.ts +68 -0
  57. package/dist/mcp/client.js +163 -0
  58. package/dist/mcp/config.d.ts +26 -0
  59. package/dist/mcp/config.js +127 -0
  60. package/dist/mcp/manager.d.ts +55 -0
  61. package/dist/mcp/manager.js +296 -0
  62. package/dist/mcp/name.d.ts +26 -0
  63. package/dist/mcp/name.js +40 -0
  64. package/dist/mcp/transports.d.ts +53 -0
  65. package/dist/mcp/transports.js +248 -0
  66. package/dist/mcp/types.d.ts +111 -0
  67. package/dist/mcp/types.js +14 -0
  68. package/dist/memory/db.d.ts +62 -0
  69. package/dist/memory/db.js +313 -0
  70. package/dist/memory/index.d.ts +9 -0
  71. package/dist/memory/index.js +9 -0
  72. package/dist/memory/paths.d.ts +18 -0
  73. package/dist/memory/paths.js +38 -0
  74. package/dist/memory/phase1.d.ts +23 -0
  75. package/dist/memory/phase1.js +172 -0
  76. package/dist/memory/phase2.d.ts +19 -0
  77. package/dist/memory/phase2.js +100 -0
  78. package/dist/memory/prompts.d.ts +19 -0
  79. package/dist/memory/prompts.js +99 -0
  80. package/dist/memory/reset.d.ts +1 -0
  81. package/dist/memory/reset.js +13 -0
  82. package/dist/memory/start.d.ts +24 -0
  83. package/dist/memory/start.js +50 -0
  84. package/dist/memory/storage.d.ts +10 -0
  85. package/dist/memory/storage.js +82 -0
  86. package/dist/memory/store.d.ts +43 -0
  87. package/dist/memory/store.js +193 -0
  88. package/dist/memory/usage.d.ts +1 -0
  89. package/dist/memory/usage.js +38 -0
  90. package/dist/model-catalog.d.ts +20 -0
  91. package/dist/model-catalog.js +99 -0
  92. package/dist/model-config.d.ts +32 -0
  93. package/dist/model-config.js +59 -0
  94. package/dist/model-pricing.d.ts +23 -0
  95. package/dist/model-pricing.js +46 -0
  96. package/dist/oauth/index.d.ts +3 -0
  97. package/dist/oauth/index.js +2 -0
  98. package/dist/oauth/openai-codex.d.ts +9 -0
  99. package/dist/oauth/openai-codex.js +173 -0
  100. package/dist/oauth/storage.d.ts +18 -0
  101. package/dist/oauth/storage.js +60 -0
  102. package/dist/oauth/types.d.ts +15 -0
  103. package/dist/oauth/types.js +1 -0
  104. package/dist/orchestrator/default-hooks.d.ts +2 -0
  105. package/dist/orchestrator/default-hooks.js +96 -0
  106. package/dist/orchestrator/hooks.d.ts +78 -0
  107. package/dist/orchestrator/hooks.js +52 -0
  108. package/dist/orchestrator/workflow.d.ts +10 -0
  109. package/dist/orchestrator/workflow.js +22 -0
  110. package/dist/permission/mode.d.ts +23 -0
  111. package/dist/permission/mode.js +20 -0
  112. package/dist/permissions/rule.d.ts +39 -0
  113. package/dist/permissions/rule.js +234 -0
  114. package/dist/permissions/settings.d.ts +71 -0
  115. package/dist/permissions/settings.js +202 -0
  116. package/dist/permissions/types.d.ts +61 -0
  117. package/dist/permissions/types.js +14 -0
  118. package/dist/prompt/compose.d.ts +12 -0
  119. package/dist/prompt/compose.js +67 -0
  120. package/dist/prompt/environment.d.ts +12 -0
  121. package/dist/prompt/environment.js +38 -0
  122. package/dist/prompt/provider-prompts/anthropic.d.ts +1 -0
  123. package/dist/prompt/provider-prompts/anthropic.js +5 -0
  124. package/dist/prompt/provider-prompts/codex.d.ts +1 -0
  125. package/dist/prompt/provider-prompts/codex.js +5 -0
  126. package/dist/prompt/provider-prompts/default.d.ts +1 -0
  127. package/dist/prompt/provider-prompts/default.js +6 -0
  128. package/dist/prompt/provider-prompts/gemini.d.ts +1 -0
  129. package/dist/prompt/provider-prompts/gemini.js +5 -0
  130. package/dist/prompt/provider-prompts/gpt.d.ts +1 -0
  131. package/dist/prompt/provider-prompts/gpt.js +5 -0
  132. package/dist/prompt/reminders.d.ts +30 -0
  133. package/dist/prompt/reminders.js +164 -0
  134. package/dist/prompt/runtime.d.ts +12 -0
  135. package/dist/prompt/runtime.js +31 -0
  136. package/dist/prompt/skills.d.ts +2 -0
  137. package/dist/prompt/skills.js +4 -0
  138. package/dist/provider-openai-codex.d.ts +14 -0
  139. package/dist/provider-openai-codex.js +409 -0
  140. package/dist/provider-registry.d.ts +56 -0
  141. package/dist/provider-registry.js +244 -0
  142. package/dist/provider-transform.d.ts +10 -0
  143. package/dist/provider-transform.js +69 -0
  144. package/dist/provider.d.ts +31 -0
  145. package/dist/provider.js +269 -0
  146. package/dist/question/controller.d.ts +22 -0
  147. package/dist/question/controller.js +97 -0
  148. package/dist/question/index.d.ts +2 -0
  149. package/dist/question/index.js +2 -0
  150. package/dist/question/types.d.ts +42 -0
  151. package/dist/question/types.js +6 -0
  152. package/dist/session-log.d.ts +16 -0
  153. package/dist/session-log.js +267 -0
  154. package/dist/session-types.d.ts +55 -0
  155. package/dist/session-types.js +1 -0
  156. package/dist/session.d.ts +32 -0
  157. package/dist/session.js +135 -0
  158. package/dist/skills/discovery.d.ts +12 -0
  159. package/dist/skills/discovery.js +148 -0
  160. package/dist/skills/format.d.ts +2 -0
  161. package/dist/skills/format.js +47 -0
  162. package/dist/skills/frontmatter.d.ts +5 -0
  163. package/dist/skills/frontmatter.js +60 -0
  164. package/dist/skills/invocation.d.ts +8 -0
  165. package/dist/skills/invocation.js +51 -0
  166. package/dist/skills/registry.d.ts +17 -0
  167. package/dist/skills/registry.js +42 -0
  168. package/dist/skills/types.d.ts +32 -0
  169. package/dist/skills/types.js +1 -0
  170. package/dist/slash-commands/commands.d.ts +7 -0
  171. package/dist/slash-commands/commands.js +779 -0
  172. package/dist/slash-commands/index.d.ts +4 -0
  173. package/dist/slash-commands/index.js +8 -0
  174. package/dist/slash-commands/registry.d.ts +31 -0
  175. package/dist/slash-commands/registry.js +70 -0
  176. package/dist/slash-commands/types.d.ts +44 -0
  177. package/dist/slash-commands/types.js +1 -0
  178. package/dist/slash-commands/unified.d.ts +38 -0
  179. package/dist/slash-commands/unified.js +38 -0
  180. package/dist/system-prompt.d.ts +34 -0
  181. package/dist/system-prompt.js +7 -0
  182. package/dist/tools/bash.d.ts +6 -0
  183. package/dist/tools/bash.js +135 -0
  184. package/dist/tools/edit.d.ts +16 -0
  185. package/dist/tools/edit.js +95 -0
  186. package/dist/tools/exa-mcp.d.ts +3 -0
  187. package/dist/tools/exa-mcp.js +74 -0
  188. package/dist/tools/exit-plan-mode.d.ts +17 -0
  189. package/dist/tools/exit-plan-mode.js +68 -0
  190. package/dist/tools/glob.d.ts +5 -0
  191. package/dist/tools/glob.js +129 -0
  192. package/dist/tools/grep.d.ts +5 -0
  193. package/dist/tools/grep.js +111 -0
  194. package/dist/tools/index.d.ts +36 -0
  195. package/dist/tools/index.js +59 -0
  196. package/dist/tools/lsp.d.ts +4 -0
  197. package/dist/tools/lsp.js +92 -0
  198. package/dist/tools/memory.d.ts +3 -0
  199. package/dist/tools/memory.js +90 -0
  200. package/dist/tools/question.d.ts +3 -0
  201. package/dist/tools/question.js +174 -0
  202. package/dist/tools/read.d.ts +7 -0
  203. package/dist/tools/read.js +83 -0
  204. package/dist/tools/sensitive-paths.d.ts +3 -0
  205. package/dist/tools/sensitive-paths.js +24 -0
  206. package/dist/tools/skill.d.ts +5 -0
  207. package/dist/tools/skill.js +51 -0
  208. package/dist/tools/task.d.ts +2 -0
  209. package/dist/tools/task.js +57 -0
  210. package/dist/tools/todo.d.ts +12 -0
  211. package/dist/tools/todo.js +151 -0
  212. package/dist/tools/tool-search.d.ts +23 -0
  213. package/dist/tools/tool-search.js +124 -0
  214. package/dist/tools/web-fetch.d.ts +6 -0
  215. package/dist/tools/web-fetch.js +75 -0
  216. package/dist/tools/web-search.d.ts +5 -0
  217. package/dist/tools/web-search.js +49 -0
  218. package/dist/tools/write.d.ts +11 -0
  219. package/dist/tools/write.js +77 -0
  220. package/dist/tui/display-history.d.ts +35 -0
  221. package/dist/tui/display-history.js +243 -0
  222. package/dist/tui/file-mentions.d.ts +29 -0
  223. package/dist/tui/file-mentions.js +174 -0
  224. package/dist/tui/image-paste.d.ts +54 -0
  225. package/dist/tui/image-paste.js +288 -0
  226. package/dist/tui/markdown-theme-rules.d.ts +23 -0
  227. package/dist/tui/markdown-theme-rules.js +164 -0
  228. package/dist/tui/markdown-theme.d.ts +5 -0
  229. package/dist/tui/markdown-theme.js +27 -0
  230. package/dist/tui/opencode-spinner.d.ts +21 -0
  231. package/dist/tui/opencode-spinner.js +216 -0
  232. package/dist/tui/prompt-keybindings.d.ts +41 -0
  233. package/dist/tui/prompt-keybindings.js +28 -0
  234. package/dist/tui/recent-activity.d.ts +8 -0
  235. package/dist/tui/recent-activity.js +71 -0
  236. package/dist/tui/run.d.ts +39 -0
  237. package/dist/tui/run.js +5696 -0
  238. package/dist/tui/sidebar-mcp.d.ts +31 -0
  239. package/dist/tui/sidebar-mcp.js +62 -0
  240. package/dist/tui/sidebar-state.d.ts +12 -0
  241. package/dist/tui/sidebar-state.js +69 -0
  242. package/dist/types.d.ts +219 -0
  243. package/dist/types.js +4 -0
  244. package/dist/variant/thinking-level.d.ts +5 -0
  245. package/dist/variant/thinking-level.js +25 -0
  246. package/dist/variant/variant-resolver.d.ts +4 -0
  247. package/dist/variant/variant-resolver.js +12 -0
  248. package/package.json +47 -0
@@ -0,0 +1,779 @@
1
+ import { UserConfig, maskKey } from "../config.js";
2
+ import { formatDiagnostics } from "../lsp/index.js";
3
+ import { normalizeNameForMCP } from "../mcp/name.js";
4
+ import { parseRule } from "../permissions/rule.js";
5
+ import { encodeModel, decodeModel, displayModel, BUILTIN_PROVIDERS, isUserVisibleProvider } from "../provider-registry.js";
6
+ import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
7
+ import { buildSystemPrompt } from "../system-prompt.js";
8
+ import { formatLoadedSkill } from "../tools/skill.js";
9
+ import { isThinkingLevel } from "../variant/thinking-level.js";
10
+ import { buildMemoryPrompt, getMemoryStatus, isMemoryDisabled, resetMemory, searchMemory, } from "../memory/index.js";
11
+ const VALID_SCOPES = ["user", "project", "local"];
12
+ const VALID_LISTS = ["allow", "deny"];
13
+ function isScope(value) {
14
+ return VALID_SCOPES.includes(value);
15
+ }
16
+ function isList(value) {
17
+ return VALID_LISTS.includes(value);
18
+ }
19
+ function handlePermissionsMutation(sub, tokens, ctx) {
20
+ if (!ctx.settingsManager) {
21
+ return "No settings manager is attached to this session.";
22
+ }
23
+ const [scope, list, ...ruleParts] = tokens;
24
+ if (!scope || !list || ruleParts.length === 0) {
25
+ return `Usage: /permissions ${sub} <user|project|local> <allow|deny> <rule>\n`
26
+ + `Example: /permissions ${sub} local allow Bash(git status)`;
27
+ }
28
+ if (!isScope(scope)) {
29
+ return `Unknown scope "${scope}". Use one of: ${VALID_SCOPES.join(", ")}.`;
30
+ }
31
+ if (!isList(list)) {
32
+ return `Unknown list "${list}". Use allow or deny.`;
33
+ }
34
+ const rule = ruleParts.join(" ");
35
+ const parsed = parseRule(rule);
36
+ if (!parsed.ok) {
37
+ return `Invalid rule: ${parsed.error.message}`;
38
+ }
39
+ if (sub === "add") {
40
+ const added = ctx.settingsManager.addRule(scope, list, rule);
41
+ if (!added)
42
+ return `Rule already present in ${scope} ${list}: ${rule}`;
43
+ return `Added to ${scope} ${list}: ${rule}\n → ${ctx.settingsManager.getPath(scope)}`;
44
+ }
45
+ const removed = ctx.settingsManager.removeRule(scope, list, rule);
46
+ if (!removed)
47
+ return `Rule not found in ${scope} ${list}: ${rule}`;
48
+ return `Removed from ${scope} ${list}: ${rule}`;
49
+ }
50
+ function persistSelectedModel(model, ctx) {
51
+ const userConfig = new UserConfig();
52
+ userConfig.setDefaultModel(model);
53
+ userConfig.setDefaultThinkingLevel(ctx.agent.thinking);
54
+ userConfig.pushRecentModel(model);
55
+ if (ctx.sessionManager) {
56
+ ctx.sessionManager.setMetadata({ model, thinkingLevel: ctx.agent.thinking, reasoningEffort: ctx.agent.thinking });
57
+ ctx.sessionManager.appendMarker("model_switch", model);
58
+ }
59
+ }
60
+ function syncSystemPrompt(ctx, model) {
61
+ const { providerId, modelId } = decodeModel(model);
62
+ ctx.agent.setSystemPrompt(buildSystemPrompt({
63
+ agentName: "Bubble",
64
+ configuredProvider: providerId,
65
+ configuredModel: displayModel(model),
66
+ configuredModelId: model,
67
+ thinkingLevel: ctx.agent.thinking,
68
+ workingDir: ctx.cwd,
69
+ skills: ctx.skillRegistry.summaries(),
70
+ memoryPrompt: buildMemoryPrompt(ctx.cwd),
71
+ }));
72
+ }
73
+ function switchToProviderModel(providerId, modelId, ctx, thinkingLevel) {
74
+ const provider = ctx.registry.getConfigured().find((item) => item.id === providerId);
75
+ if (!provider?.apiKey) {
76
+ return false;
77
+ }
78
+ ctx.agent.thinking = normalizeThinkingLevel(thinkingLevel ?? ctx.agent.thinking, getAvailableThinkingLevels(providerId, modelId));
79
+ ctx.agent.setProvider(ctx.createProvider(providerId, provider.apiKey, provider.baseURL));
80
+ ctx.agent.providerId = providerId;
81
+ ctx.agent.model = encodeModel(providerId, modelId);
82
+ syncSystemPrompt(ctx, ctx.agent.model);
83
+ persistSelectedModel(ctx.agent.model, ctx);
84
+ return true;
85
+ }
86
+ function parseModelArgs(args) {
87
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
88
+ let model;
89
+ let thinkingLevel;
90
+ for (let index = 0; index < tokens.length; index++) {
91
+ const token = tokens[index];
92
+ if (token === "--reasoning-effort" || token === "--thinking") {
93
+ const value = tokens[++index];
94
+ if (!isThinkingLevel(value)) {
95
+ return { error: `Invalid reasoning effort "${value ?? ""}".` };
96
+ }
97
+ thinkingLevel = value;
98
+ continue;
99
+ }
100
+ if (!model) {
101
+ model = token;
102
+ continue;
103
+ }
104
+ return { error: `Unexpected model argument "${token}".` };
105
+ }
106
+ return { model, thinkingLevel };
107
+ }
108
+ function displaySelectedModel(model, thinkingLevel) {
109
+ const label = displayModel(model);
110
+ return thinkingLevel === "off" ? label : `${label} (${thinkingLevel})`;
111
+ }
112
+ function parseMemoryScopeArgs(args) {
113
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
114
+ let scope = "project";
115
+ const rest = [];
116
+ for (const token of tokens) {
117
+ if (token === "--global") {
118
+ scope = "global";
119
+ continue;
120
+ }
121
+ if (token === "--project") {
122
+ scope = "project";
123
+ continue;
124
+ }
125
+ rest.push(token);
126
+ }
127
+ return { scope, rest: rest.join(" ") };
128
+ }
129
+ async function handleMemoryCommand(args, ctx) {
130
+ const trimmed = args.trim();
131
+ const [sub = "status", ...rest] = trimmed.split(/\s+/);
132
+ if (!trimmed || sub === "status") {
133
+ const status = getMemoryStatus(ctx.cwd);
134
+ const lines = [
135
+ "Memory status:",
136
+ ` environment: ${status.environment}`,
137
+ ` bubble home: ${status.bubbleHome}`,
138
+ ` project root: ${status.paths.projectRoot}`,
139
+ ` global root: ${status.paths.globalRoot}`,
140
+ ` startup pipeline: ${isMemoryDisabled() ? "disabled" : "enabled"}`,
141
+ "",
142
+ "Files:",
143
+ ];
144
+ for (const file of status.files) {
145
+ lines.push(` ${file.exists ? "present" : "missing"} ${file.label} (${file.bytes} bytes)`);
146
+ lines.push(` ${file.path}`);
147
+ }
148
+ lines.push("", "SQLite state:");
149
+ lines.push(` path: ${status.database.path}`);
150
+ lines.push(` stage1Outputs: ${status.database.stage1Outputs}`);
151
+ lines.push(` disabledThreads: ${status.database.disabledThreads}`);
152
+ for (const job of status.database.jobs) {
153
+ lines.push(` job ${job.kind}/${job.jobKey}: ${job.status}`);
154
+ if (job.lastError)
155
+ lines.push(` lastError: ${job.lastError}`);
156
+ }
157
+ return lines.join("\n");
158
+ }
159
+ if (sub === "add") {
160
+ return "Manual memory writes are disabled. Bubble now follows the Codex-style automatic startup memory pipeline.";
161
+ }
162
+ if (sub === "search") {
163
+ const query = rest.join(" ").trim();
164
+ if (!query) {
165
+ return "Usage: /memory search <query>";
166
+ }
167
+ const results = searchMemory(ctx.cwd, query);
168
+ if (results.length === 0) {
169
+ return `No memory matches for "${query}".`;
170
+ }
171
+ const lines = [`Memory search results for "${query}":`];
172
+ for (const result of results) {
173
+ lines.push(` ${result.scope} ${result.path}:${result.line}`);
174
+ lines.push(` ${result.text}`);
175
+ }
176
+ return lines.join("\n");
177
+ }
178
+ if (sub === "compact") {
179
+ if (!ctx.runMemoryCompaction) {
180
+ return "Memory compaction is not attached to this session.";
181
+ }
182
+ return await ctx.runMemoryCompaction();
183
+ }
184
+ if (sub === "summarize") {
185
+ if (!ctx.runMemorySummary) {
186
+ return "Memory summary is not attached to this session.";
187
+ }
188
+ const parsed = parseMemoryScopeArgs(rest.join(" "));
189
+ if (parsed.rest)
190
+ return "Usage: /memory summarize [--project|--global]";
191
+ const result = await ctx.runMemorySummary(parsed.scope);
192
+ if (ctx.agent.model)
193
+ syncSystemPrompt(ctx, ctx.agent.model);
194
+ return result;
195
+ }
196
+ if (sub === "refresh") {
197
+ if (!ctx.runMemoryRefresh) {
198
+ return "Memory refresh is not attached to this session.";
199
+ }
200
+ const parsed = parseMemoryScopeArgs(rest.join(" "));
201
+ if (parsed.rest)
202
+ return "Usage: /memory refresh [--project|--global]";
203
+ const result = await ctx.runMemoryRefresh(parsed.scope);
204
+ if (ctx.agent.model)
205
+ syncSystemPrompt(ctx, ctx.agent.model);
206
+ return result;
207
+ }
208
+ if (sub === "reset") {
209
+ const result = resetMemory(ctx.cwd);
210
+ if (ctx.agent.model)
211
+ syncSystemPrompt(ctx, ctx.agent.model);
212
+ return result;
213
+ }
214
+ return "Usage: /memory [status|search|compact|summarize|refresh|reset]";
215
+ }
216
+ function parseKeyArgs(args, ctx) {
217
+ const trimmed = args.trim();
218
+ const [first, ...rest] = trimmed.split(/\s+/);
219
+ const explicitProvider = first
220
+ ? ctx.registry.getConfigured().find((provider) => provider.id === first)
221
+ : undefined;
222
+ if (explicitProvider) {
223
+ return { provider: explicitProvider, apiKey: rest.join(" ") };
224
+ }
225
+ return { provider: ctx.registry.getDefault(), apiKey: trimmed };
226
+ }
227
+ const builtinSlashCommandEntries = [
228
+ {
229
+ name: "skills",
230
+ description: "List available skills and any skill diagnostics",
231
+ async handler(args, ctx) {
232
+ const skills = ctx.skillRegistry.summaries();
233
+ const diagnostics = ctx.skillRegistry.getDiagnostics();
234
+ const lines = [];
235
+ if (skills.length === 0) {
236
+ lines.push("No skills available.");
237
+ }
238
+ else {
239
+ lines.push("Available skills:");
240
+ for (const skill of skills) {
241
+ const tagSuffix = skill.tags && skill.tags.length > 0 ? ` [tags: ${skill.tags.join(", ")}]` : "";
242
+ lines.push(`- ${skill.name}: ${skill.description}${tagSuffix}`);
243
+ }
244
+ }
245
+ if (diagnostics.length > 0) {
246
+ lines.push("", "Skill diagnostics:");
247
+ for (const diagnostic of diagnostics) {
248
+ const prefix = diagnostic.level === "error" ? "ERROR" : "WARN";
249
+ const target = diagnostic.skillName ?? diagnostic.filePath ?? "skills";
250
+ lines.push(`- ${prefix} ${target}: ${diagnostic.message}`);
251
+ }
252
+ }
253
+ return lines.join("\n");
254
+ },
255
+ },
256
+ {
257
+ name: "skill",
258
+ description: "Load a skill explicitly. Usage: /skill <name>",
259
+ async handler(args, ctx) {
260
+ const name = args.trim();
261
+ if (!name) {
262
+ return "Usage: /skill <name>";
263
+ }
264
+ const skill = ctx.skillRegistry.get(name);
265
+ if (!skill) {
266
+ const available = ctx.skillRegistry.summaries().map((item) => item.name).join(", ");
267
+ return available
268
+ ? `Unknown skill "${name}". Available skills: ${available}`
269
+ : `Unknown skill "${name}". No skills are currently available.`;
270
+ }
271
+ ctx.sessionManager?.appendMarker("skill_activated", skill.meta.name);
272
+ return formatLoadedSkill(skill);
273
+ },
274
+ },
275
+ {
276
+ name: "help",
277
+ description: "Show available slash commands",
278
+ async handler(args, ctx) {
279
+ const { registry } = await import("./index.js");
280
+ const lines = ["Available commands:"];
281
+ for (const cmd of registry.list()) {
282
+ lines.push(` /${cmd.name} - ${cmd.description}`);
283
+ }
284
+ return lines.join("\n");
285
+ },
286
+ },
287
+ {
288
+ name: "memory",
289
+ description: "Inspect and maintain Codex-style automatic persistent memory. Usage: /memory [status|search|compact|summarize|refresh|reset]",
290
+ async handler(args, ctx) {
291
+ return handleMemoryCommand(args, ctx);
292
+ },
293
+ },
294
+ {
295
+ name: "quit",
296
+ description: "Exit the application",
297
+ async handler(args, ctx) {
298
+ // Shut MCP stdio children down first; their stdout/stderr listeners
299
+ // otherwise hold the Node event loop open even after ink unmounts.
300
+ try {
301
+ await ctx.mcpManager?.shutdown();
302
+ }
303
+ catch {
304
+ // ignore — we're quitting anyway
305
+ }
306
+ try {
307
+ await ctx.flushMemory?.();
308
+ }
309
+ catch {
310
+ // memory shutdown hooks are best-effort during exit
311
+ }
312
+ ctx.exit();
313
+ // Belt-and-braces: if anything else (raw-mode tty handle, pending
314
+ // timer, etc.) still holds the loop, force-exit shortly after.
315
+ setTimeout(() => process.exit(0), 100).unref();
316
+ },
317
+ },
318
+ {
319
+ name: "clear",
320
+ description: "Clear the current conversation history",
321
+ async handler(args, ctx) {
322
+ ctx.clearMessages();
323
+ ctx.agent.messages = ctx.agent.messages.filter((m) => m.role === "system");
324
+ return "Conversation cleared.";
325
+ },
326
+ },
327
+ {
328
+ name: "session",
329
+ description: "Show current session information",
330
+ async handler(args, ctx) {
331
+ return `Session info not implemented yet.`;
332
+ },
333
+ },
334
+ {
335
+ name: "provider",
336
+ description: "Manage providers. /provider to switch, /provider --add [id] to add, /provider --remove <id>, /provider --set <id>",
337
+ async handler(args, ctx) {
338
+ if (!args) {
339
+ ctx.openPicker("provider");
340
+ return;
341
+ }
342
+ const parts = args.trim().split(/\s+/);
343
+ const flag = parts[0];
344
+ const value = parts[1];
345
+ if (flag === "--add") {
346
+ if (!value) {
347
+ ctx.openPicker("provider-add");
348
+ return;
349
+ }
350
+ const builtin = BUILTIN_PROVIDERS.find((p) => p.id === value && isUserVisibleProvider(p.id));
351
+ if (!builtin) {
352
+ const ids = BUILTIN_PROVIDERS.filter((p) => isUserVisibleProvider(p.id)).map((p) => p.id).join(", ");
353
+ return `Unknown provider "${value}". Supported: ${ids}`;
354
+ }
355
+ ctx.registry.addProvider(value, "");
356
+ ctx.registry.setDefault(value);
357
+ ctx.openPicker("key", value);
358
+ return;
359
+ }
360
+ if (flag === "--remove" && value) {
361
+ if (ctx.registry.getModelConfig().hasProvider(value)) {
362
+ return `Provider ${value} is defined in ~/.bubble/models.json. Please edit that file directly.`;
363
+ }
364
+ ctx.registry.removeProvider(value);
365
+ return `Provider ${value} removed.`;
366
+ }
367
+ if (flag === "--set" && value) {
368
+ const providers = ctx.registry.getConfigured();
369
+ const p = providers.find((x) => x.id === value);
370
+ if (!p)
371
+ return `Provider ${value} is not configured.`;
372
+ ctx.registry.setDefault(value);
373
+ if (ctx.registry.getModelConfig().hasProvider(value)) {
374
+ return `Default provider set to ${p.name}. Note: config is managed via ~/.bubble/models.json.`;
375
+ }
376
+ return `Default provider set to ${p.name}.`;
377
+ }
378
+ if (flag === "--list") {
379
+ const providers = ctx.registry.getConfigured();
380
+ const lines = ["Configured providers:"];
381
+ for (const p of providers) {
382
+ const marker = p.id === ctx.registry.getDefault()?.id ? "* " : " ";
383
+ const source = ctx.registry.getModelConfig().hasProvider(p.id) ? " [models.json]" : "";
384
+ const oauth = ctx.registry.getAuthStorage().has(p.id) ? " [oauth]" : "";
385
+ lines.push(`${marker}${p.name} (${p.id}) ${p.enabled ? "" : "[disabled]"}${oauth}${source}`);
386
+ }
387
+ if (ctx.registry.getModelConfig().getLoadError()) {
388
+ lines.push(`Warning: failed to load models.json: ${ctx.registry.getModelConfig().getLoadError()}`);
389
+ }
390
+ return lines.join("\n");
391
+ }
392
+ return `Usage: /provider [--add|--remove|--set|--list] <id>`;
393
+ },
394
+ },
395
+ {
396
+ name: "login",
397
+ description: "OAuth login for supported providers. Usage: /login [openai]",
398
+ async handler(args, ctx) {
399
+ const providerId = args?.trim() || "openai";
400
+ if (!providerId) {
401
+ ctx.openPicker("login");
402
+ return;
403
+ }
404
+ if (!ctx.registry.supportsOAuth(providerId)) {
405
+ return `Unsupported OAuth provider: ${providerId}. Currently only 'openai' is supported.`;
406
+ }
407
+ const { loginOpenAICodex } = await import("../oauth/openai-codex.js");
408
+ const tokens = await loginOpenAICodex({
409
+ onStatus: (msg) => ctx.addMessage("assistant", msg),
410
+ });
411
+ ctx.registry.getAuthStorage().set(providerId, {
412
+ type: "oauth",
413
+ accessToken: tokens.accessToken,
414
+ refreshToken: tokens.refreshToken,
415
+ expiresAt: tokens.expiresAt,
416
+ idToken: tokens.idToken,
417
+ accountId: tokens.accountId,
418
+ });
419
+ await ctx.registry.prepareProvider(providerId);
420
+ ctx.registry.setDefault(providerId);
421
+ const provider = ctx.registry.getConfigured().find((item) => item.id === providerId);
422
+ const discoveredModels = provider ? await ctx.registry.listModels(provider) : [];
423
+ const defaultModel = discoveredModels[0]?.id || ctx.registry.getDefaultModel(providerId, "oauth");
424
+ if (!defaultModel) {
425
+ return `OpenAI Codex OAuth login succeeded, but no default model is configured for ${providerId}.`;
426
+ }
427
+ const switched = switchToProviderModel(providerId, defaultModel, ctx);
428
+ if (!switched) {
429
+ return `OpenAI Codex OAuth login succeeded, but the provider could not be activated. Tokens saved to ${ctx.registry.getAuthStorage().getPath()}`;
430
+ }
431
+ return `OpenAI Codex OAuth login successful. Switched to ${displayModel(ctx.agent.model)}. Account: ${tokens.accountId || "unknown"}. Tokens saved to ${ctx.registry.getAuthStorage().getPath()}`;
432
+ },
433
+ },
434
+ {
435
+ name: "model",
436
+ description: "Switch model. Use /model <id> [--reasoning-effort <level>] or just /model to open picker.",
437
+ async handler(args, ctx) {
438
+ if (!args) {
439
+ if (ctx.registry.getEnabled().length === 0) {
440
+ ctx.openPicker("model");
441
+ return;
442
+ }
443
+ ctx.openPicker("model");
444
+ return;
445
+ }
446
+ const parsed = parseModelArgs(args);
447
+ if (parsed.error) {
448
+ return parsed.error;
449
+ }
450
+ if (!parsed.model) {
451
+ ctx.openPicker("model");
452
+ return;
453
+ }
454
+ const defaultProvider = ctx.registry.getDefault()?.id || "openai";
455
+ const next = parsed.model.includes(":") ? parsed.model : encodeModel(defaultProvider, parsed.model);
456
+ const { providerId, modelId } = decodeModel(next);
457
+ const targetProviderId = providerId || defaultProvider;
458
+ await ctx.registry.prepareProvider(targetProviderId);
459
+ const switched = switchToProviderModel(targetProviderId, modelId, ctx, parsed.thinkingLevel);
460
+ if (!switched) {
461
+ return `Provider ${targetProviderId} is not configured or has no active credentials.`;
462
+ }
463
+ return `Model switched to ${displaySelectedModel(next, ctx.agent.thinking)}.`;
464
+ },
465
+ },
466
+ {
467
+ name: "key",
468
+ description: "Set API key for the current or a specific provider. Usage: /key [provider-id] <key>",
469
+ async handler(args, ctx) {
470
+ if (!args) {
471
+ ctx.openPicker("key");
472
+ return;
473
+ }
474
+ const { provider, apiKey } = parseKeyArgs(args, ctx);
475
+ if (!provider) {
476
+ return "No provider configured. Use /provider --add <id> first.";
477
+ }
478
+ if (!apiKey) {
479
+ return `Usage: /key ${provider.id} <key>`;
480
+ }
481
+ if (ctx.registry.getModelConfig().hasProvider(provider.id)) {
482
+ return `API key for ${provider.name} is managed in ~/.bubble/models.json. Please edit that file directly.`;
483
+ }
484
+ ctx.registry.updateProviderKey(provider.id, apiKey);
485
+ ctx.registry.setDefault(provider.id);
486
+ ctx.agent.setProvider(ctx.createProvider(provider.id, apiKey, provider.baseURL));
487
+ ctx.agent.providerId = provider.id;
488
+ return `API key updated for ${provider.name} to ${maskKey(apiKey)}.`;
489
+ },
490
+ },
491
+ {
492
+ name: "logout",
493
+ description: "Remove OAuth credentials for a provider. Usage: /logout [openai]",
494
+ async handler(args, ctx) {
495
+ const providerId = args?.trim() || "openai";
496
+ if (!providerId) {
497
+ ctx.openPicker("logout");
498
+ return;
499
+ }
500
+ if (!ctx.registry.getAuthStorage().has(providerId)) {
501
+ return `No OAuth credentials found for ${providerId}.`;
502
+ }
503
+ ctx.registry.getAuthStorage().remove(providerId);
504
+ const fallback = ctx.registry.getDefault();
505
+ if (fallback?.apiKey) {
506
+ const fallbackModel = ctx.registry.getDefaultModel(fallback.id);
507
+ if (fallbackModel) {
508
+ switchToProviderModel(fallback.id, fallbackModel, ctx);
509
+ return `OAuth credentials for ${providerId} removed. Switched to ${fallback.name}.`;
510
+ }
511
+ ctx.agent.setProvider(ctx.createProvider(fallback.id, fallback.apiKey, fallback.baseURL));
512
+ ctx.agent.providerId = fallback.id;
513
+ }
514
+ else if (ctx.agent.providerId === providerId) {
515
+ ctx.agent.providerId = "";
516
+ }
517
+ return `OAuth credentials for ${providerId} removed.`;
518
+ },
519
+ },
520
+ {
521
+ name: "plan",
522
+ description: "Toggle plan mode on/off (Tab switches Build/Plan)",
523
+ async handler(args, ctx) {
524
+ const next = ctx.agent.mode === "plan" ? "default" : "plan";
525
+ ctx.agent.setMode(next);
526
+ return next === "plan"
527
+ ? "Entered plan mode. The assistant will investigate and propose a plan before making changes."
528
+ : "Exited plan mode.";
529
+ },
530
+ },
531
+ {
532
+ name: "todos",
533
+ description: "Show the current todo list. Use /todos clear to reset it.",
534
+ async handler(args, ctx) {
535
+ const sub = args.trim();
536
+ if (sub === "clear") {
537
+ const previous = ctx.agent.getTodos().length;
538
+ if (previous === 0) {
539
+ return "Todo list is already empty.";
540
+ }
541
+ ctx.agent.setTodos([]);
542
+ return `Cleared ${previous} todo item${previous === 1 ? "" : "s"}.`;
543
+ }
544
+ const todos = ctx.agent.getTodos();
545
+ if (todos.length === 0) {
546
+ return "No todos yet. The assistant will create some when working on multi-step tasks.";
547
+ }
548
+ const glyph = (status) => status === "completed" ? "✔" : status === "in_progress" ? "▶" : "○";
549
+ const lines = ["Todos:"];
550
+ for (const todo of todos) {
551
+ const label = todo.status === "in_progress" ? (todo.activeForm || todo.content) : todo.content;
552
+ lines.push(` ${glyph(todo.status)} ${label}`);
553
+ }
554
+ return lines.join("\n");
555
+ },
556
+ },
557
+ {
558
+ name: "permissions",
559
+ description: "Inspect or edit allow/deny rules. Subcommands: add <scope> <list> <rule>, remove <scope> <list> <rule>, clear (session allowlist), reload.",
560
+ async handler(args, ctx) {
561
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
562
+ const sub = tokens[0] ?? "";
563
+ if (sub === "add" || sub === "remove") {
564
+ return handlePermissionsMutation(sub, tokens.slice(1), ctx);
565
+ }
566
+ if (sub === "clear") {
567
+ if (!ctx.bashAllowlist)
568
+ return "No approval controller is attached to this session.";
569
+ const size = ctx.bashAllowlist.size();
570
+ if (size === 0)
571
+ return "Bash allowlist is already empty.";
572
+ ctx.bashAllowlist.clear();
573
+ return `Cleared ${size} bash prefix${size === 1 ? "" : "es"} from the session allowlist.`;
574
+ }
575
+ if (sub === "reload") {
576
+ if (!ctx.settingsManager)
577
+ return "No settings manager is attached to this session.";
578
+ ctx.settingsManager.reload();
579
+ ctx.lspService?.updateConfig(ctx.settingsManager.getMerged().lsp);
580
+ ctx.lspService?.restart();
581
+ return "Reloaded settings from disk.";
582
+ }
583
+ const lines = [];
584
+ if (ctx.settingsManager) {
585
+ const merged = ctx.settingsManager.getMerged();
586
+ lines.push("Settings files:");
587
+ lines.push(` user: ${ctx.settingsManager.getPath("user")}`);
588
+ lines.push(` project: ${ctx.settingsManager.getPath("project")}`);
589
+ lines.push(` local: ${ctx.settingsManager.getPath("local")}`);
590
+ if (merged.defaultMode) {
591
+ lines.push("", `defaultMode: ${merged.defaultMode}`);
592
+ }
593
+ lines.push("", `Allow rules (${merged.ruleSet.allow.length}):`);
594
+ if (merged.ruleSet.allow.length === 0) {
595
+ lines.push(" (none)");
596
+ }
597
+ else {
598
+ for (const r of merged.ruleSet.allow)
599
+ lines.push(` ${r.source}`);
600
+ }
601
+ lines.push("", `Deny rules (${merged.ruleSet.deny.length}):`);
602
+ if (merged.ruleSet.deny.length === 0) {
603
+ lines.push(" (none)");
604
+ }
605
+ else {
606
+ for (const r of merged.ruleSet.deny)
607
+ lines.push(` ${r.source}`);
608
+ }
609
+ if (merged.diagnostics.length > 0) {
610
+ lines.push("", "Diagnostics:");
611
+ for (const d of merged.diagnostics) {
612
+ lines.push(` [${d.scope}] ${d.message}`);
613
+ }
614
+ }
615
+ }
616
+ if (ctx.bashAllowlist) {
617
+ const entries = ctx.bashAllowlist.list();
618
+ if (lines.length > 0)
619
+ lines.push("");
620
+ lines.push(`Session bash allowlist (${entries.length}):`);
621
+ if (entries.length === 0) {
622
+ lines.push(' (none) — approving "Yes, and don\'t ask again for <prefix>" adds entries here');
623
+ }
624
+ else {
625
+ for (const prefix of entries)
626
+ lines.push(` ${prefix}`);
627
+ }
628
+ }
629
+ if (lines.length === 0) {
630
+ return "Permissions system not attached to this session.";
631
+ }
632
+ return lines.join("\n");
633
+ },
634
+ },
635
+ {
636
+ name: "lsp",
637
+ description: "Inspect or restart language servers. Usage: /lsp [status|diagnostics|restart]",
638
+ async handler(args, ctx) {
639
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
640
+ const sub = tokens[0] ?? "status";
641
+ const lsp = ctx.lspService;
642
+ if (!lsp)
643
+ return "LSP is not initialized for this session.";
644
+ if (sub === "restart") {
645
+ ctx.settingsManager?.reload();
646
+ ctx.lspService?.updateConfig(ctx.settingsManager?.getMerged().lsp);
647
+ await lsp.restart();
648
+ return "Restarted LSP servers.";
649
+ }
650
+ if (sub === "diagnostics") {
651
+ const diagnostics = lsp.diagnostics();
652
+ const entries = Object.entries(diagnostics).filter(([, issues]) => issues.length > 0);
653
+ if (entries.length === 0)
654
+ return "No LSP diagnostics.";
655
+ const lines = ["LSP diagnostics:"];
656
+ for (const [file, issues] of entries.slice(0, 10)) {
657
+ lines.push(formatDiagnostics(file, issues, ctx.cwd));
658
+ }
659
+ if (entries.length > 10)
660
+ lines.push(`... ${entries.length - 10} more file(s) with diagnostics`);
661
+ return lines.join("\n");
662
+ }
663
+ if (sub !== "status" && sub !== "list" && sub !== "") {
664
+ return `Unknown /lsp subcommand "${sub}". Use /lsp status, /lsp diagnostics, or /lsp restart.`;
665
+ }
666
+ if (lsp.isDisabled()) {
667
+ return "LSPs have been disabled in settings.";
668
+ }
669
+ const statuses = lsp.status();
670
+ if (statuses.length === 0) {
671
+ return "LSPs will activate as files are read.";
672
+ }
673
+ const lines = ["LSP servers:"];
674
+ for (const status of statuses) {
675
+ const marker = status.status === "connected" ? "*" : status.status === "starting" ? "~" : "!";
676
+ const suffix = status.message ? ` — ${status.message}` : "";
677
+ lines.push(` ${marker} ${status.id} ${status.root}${suffix}`);
678
+ }
679
+ return lines.join("\n");
680
+ },
681
+ },
682
+ {
683
+ name: "mcp",
684
+ description: "Manage MCP servers. Usage: /mcp [list|reconnect <name>]",
685
+ async handler(args, ctx) {
686
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
687
+ const sub = tokens[0] ?? "list";
688
+ if (!ctx.mcpManager) {
689
+ return "MCP is not initialized for this session.";
690
+ }
691
+ if (sub === "reconnect") {
692
+ const name = tokens[1];
693
+ if (!name)
694
+ return "Usage: /mcp reconnect <server-name>";
695
+ const state = await ctx.mcpManager.reconnect(name);
696
+ if (!state)
697
+ return `Unknown MCP server: ${name}`;
698
+ if (state.status.kind === "connected") {
699
+ return `Reconnected ${name}. ${state.status.tools.length} tool${state.status.tools.length === 1 ? "" : "s"} available.`;
700
+ }
701
+ if (state.status.kind === "failed") {
702
+ return `Failed to connect ${name}: ${state.status.error}`;
703
+ }
704
+ return `${name}: ${state.status.kind}`;
705
+ }
706
+ if (sub !== "list" && sub !== "") {
707
+ return `Unknown /mcp subcommand "${sub}". Use /mcp list or /mcp reconnect <name>.`;
708
+ }
709
+ const states = ctx.mcpManager.getStates();
710
+ if (states.length === 0) {
711
+ return "No MCP servers configured. Add entries under `mcpServers` in ~/.bubble/settings.json or <cwd>/.bubble/settings.json.";
712
+ }
713
+ const lines = ["MCP servers:"];
714
+ for (const state of states) {
715
+ const transport = state.config.type;
716
+ const scope = state.scope;
717
+ if (state.status.kind === "connected") {
718
+ const info = state.status.serverInfo ? ` ${state.status.serverInfo.name}@${state.status.serverInfo.version}` : "";
719
+ const tn = state.status.tools.length;
720
+ const pn = state.status.prompts.length;
721
+ const counts = [`${tn} tool${tn === 1 ? "" : "s"}`];
722
+ if (pn > 0)
723
+ counts.push(`${pn} prompt${pn === 1 ? "" : "s"}`);
724
+ lines.push(` ✔ ${state.name} [${scope}/${transport}]${info} — ${counts.join(", ")}`);
725
+ for (const tool of state.status.tools) {
726
+ lines.push(` · ${tool.name}${tool.description ? ` — ${tool.description.replace(/\s+/g, " ").slice(0, 80)}` : ""}`);
727
+ }
728
+ if (pn > 0) {
729
+ lines.push(` prompts (invoke as /<name>):`);
730
+ for (const p of state.status.prompts) {
731
+ const argSig = p.arguments?.length
732
+ ? ` <${p.arguments.map((a) => (a.required ? a.name : `${a.name}?`)).join("> <")}>`
733
+ : "";
734
+ const cmdName = normalizeNameForMCP(p.name);
735
+ lines.push(` · /${cmdName}${argSig}${p.description ? ` — ${p.description.replace(/\s+/g, " ").slice(0, 70)}` : ""}`);
736
+ }
737
+ }
738
+ }
739
+ else if (state.status.kind === "failed") {
740
+ lines.push(` ✘ ${state.name} [${scope}/${transport}] — ${state.status.error}`);
741
+ }
742
+ else {
743
+ lines.push(` ○ ${state.name} [${scope}/${transport}] — disabled`);
744
+ }
745
+ }
746
+ return lines.join("\n");
747
+ },
748
+ },
749
+ {
750
+ name: "compact",
751
+ description: "Manually compact the current session context",
752
+ async handler(args, ctx) {
753
+ if (!ctx.sessionManager) {
754
+ return "Compaction requires session persistence. Start an interactive session first.";
755
+ }
756
+ const result = ctx.sessionManager.compact();
757
+ if (!result.compacted) {
758
+ return "Session is already compact enough.";
759
+ }
760
+ const systemMessage = ctx.agent.messages.find((message) => message.role === "system");
761
+ ctx.clearMessages();
762
+ ctx.agent.messages = [
763
+ ...(systemMessage ? [systemMessage] : []),
764
+ ...ctx.sessionManager.getMessages(),
765
+ ];
766
+ const dropped = result.droppedEntries ?? 0;
767
+ return `✓ Compaction complete · ${dropped} log entr${dropped === 1 ? "y" : "ies"} summarized`;
768
+ },
769
+ },
770
+ ];
771
+ /**
772
+ * Public export — built-in commands tagged with `source: "builtin"` so the
773
+ * registry and TUI can group them uniformly alongside MCP-derived commands.
774
+ * Kept as a mapped projection to avoid adding the field to every object literal.
775
+ */
776
+ export const builtinSlashCommands = builtinSlashCommandEntries.map((cmd) => ({
777
+ ...cmd,
778
+ source: "builtin",
779
+ }));