@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,97 @@
1
+ import { QuestionRejectedError } from "./types.js";
2
+ export class QuestionController {
3
+ pending = new Map();
4
+ listeners = new Set();
5
+ counter = 0;
6
+ ask(input) {
7
+ const request = {
8
+ id: this.nextID(),
9
+ sessionID: input.sessionID,
10
+ questions: input.questions.map(normalizePrompt),
11
+ tool: input.tool,
12
+ createdAt: Date.now(),
13
+ };
14
+ const promise = new Promise((resolve, reject) => {
15
+ this.pending.set(request.id, {
16
+ request,
17
+ resolve,
18
+ reject,
19
+ });
20
+ });
21
+ this.emit({ type: "asked", request });
22
+ return promise.finally(() => {
23
+ this.pending.delete(request.id);
24
+ });
25
+ }
26
+ reply(requestID, answers) {
27
+ const pending = this.pending.get(requestID);
28
+ if (!pending)
29
+ return false;
30
+ const normalized = normalizeAnswers(pending.request.questions.length, answers);
31
+ this.pending.delete(requestID);
32
+ this.emit({ type: "replied", request: pending.request, answers: normalized });
33
+ pending.resolve(normalized);
34
+ return true;
35
+ }
36
+ reject(requestID) {
37
+ const pending = this.pending.get(requestID);
38
+ if (!pending)
39
+ return false;
40
+ this.pending.delete(requestID);
41
+ this.emit({ type: "rejected", request: pending.request });
42
+ pending.reject(new QuestionRejectedError());
43
+ return true;
44
+ }
45
+ list(sessionID) {
46
+ const items = Array.from(this.pending.values(), (entry) => entry.request);
47
+ return sessionID === undefined ? items : items.filter((request) => request.sessionID === sessionID);
48
+ }
49
+ subscribe(listener) {
50
+ this.listeners.add(listener);
51
+ return () => {
52
+ this.listeners.delete(listener);
53
+ };
54
+ }
55
+ rejectAll() {
56
+ for (const id of [...this.pending.keys()]) {
57
+ this.reject(id);
58
+ }
59
+ }
60
+ emit(event) {
61
+ for (const listener of [...this.listeners]) {
62
+ try {
63
+ listener(event);
64
+ }
65
+ catch (err) {
66
+ console.error("[question] listener failed", err);
67
+ }
68
+ }
69
+ }
70
+ nextID() {
71
+ this.counter += 1;
72
+ return `question_${Date.now().toString(36)}_${this.counter.toString(36)}`;
73
+ }
74
+ }
75
+ function normalizePrompt(input) {
76
+ return {
77
+ header: String(input.header ?? "").trim(),
78
+ question: String(input.question ?? "").trim(),
79
+ options: Array.isArray(input.options)
80
+ ? input.options.map((option) => ({
81
+ label: String(option?.label ?? "").trim(),
82
+ description: String(option?.description ?? "").trim(),
83
+ }))
84
+ : [],
85
+ multiple: input.multiple === true,
86
+ custom: input.custom === false ? false : undefined,
87
+ };
88
+ }
89
+ function normalizeAnswers(count, answers) {
90
+ return Array.from({ length: count }, (_, index) => {
91
+ const answer = answers[index];
92
+ if (!Array.isArray(answer))
93
+ return [];
94
+ return answer.map((item) => String(item).trim()).filter(Boolean);
95
+ });
96
+ }
97
+ export { QuestionRejectedError };
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./controller.js";
@@ -0,0 +1,2 @@
1
+ export * from "./types.js";
2
+ export * from "./controller.js";
@@ -0,0 +1,42 @@
1
+ export interface QuestionOption {
2
+ label: string;
3
+ description: string;
4
+ }
5
+ export interface QuestionPrompt {
6
+ /** Very short label used in tabs for multi-question prompts. */
7
+ header: string;
8
+ /** Complete user-facing question text. */
9
+ question: string;
10
+ /** Fixed choices shown to the user. */
11
+ options: QuestionOption[];
12
+ /** Allow selecting more than one option. Defaults to false. */
13
+ multiple?: boolean;
14
+ /** Allow a custom free-form answer. Defaults to true. */
15
+ custom?: boolean;
16
+ }
17
+ export type QuestionAnswer = string[];
18
+ export interface QuestionToolRef {
19
+ messageID?: string;
20
+ callID: string;
21
+ }
22
+ export interface QuestionRequest {
23
+ id: string;
24
+ sessionID?: string;
25
+ questions: QuestionPrompt[];
26
+ tool?: QuestionToolRef;
27
+ createdAt: number;
28
+ }
29
+ export type QuestionEvent = {
30
+ type: "asked";
31
+ request: QuestionRequest;
32
+ } | {
33
+ type: "replied";
34
+ request: QuestionRequest;
35
+ answers: QuestionAnswer[];
36
+ } | {
37
+ type: "rejected";
38
+ request: QuestionRequest;
39
+ };
40
+ export declare class QuestionRejectedError extends Error {
41
+ constructor(message?: string);
42
+ }
@@ -0,0 +1,6 @@
1
+ export class QuestionRejectedError extends Error {
2
+ constructor(message = "The user dismissed this question") {
3
+ super(message);
4
+ this.name = "QuestionRejectedError";
5
+ }
6
+ }
@@ -0,0 +1,16 @@
1
+ import type { Message, Todo } from "./types.js";
2
+ import type { SessionLogEntry, SessionMarkerKind, SessionMetadata, SessionSummaryEntry, SessionTodosSnapshotEntry } from "./session-types.js";
3
+ export declare class SessionLog {
4
+ private entries;
5
+ load(lines: string[]): void;
6
+ replace(entries: SessionLogEntry[]): void;
7
+ list(): SessionLogEntry[];
8
+ getMetadata(): SessionMetadata;
9
+ setMetadata(metadata: SessionMetadata): SessionLogEntry[];
10
+ appendMessage(message: Message): SessionLogEntry[];
11
+ appendSummary(summary: string): SessionSummaryEntry;
12
+ appendTodosSnapshot(todos: Todo[]): SessionTodosSnapshotEntry;
13
+ getTodos(): Todo[];
14
+ appendMarker(kind: SessionMarkerKind, value: string): SessionLogEntry;
15
+ toMessages(): Message[];
16
+ }
@@ -0,0 +1,267 @@
1
+ export class SessionLog {
2
+ entries = [];
3
+ load(lines) {
4
+ this.entries = [];
5
+ for (const line of lines) {
6
+ try {
7
+ const raw = JSON.parse(line);
8
+ this.entries.push(...normalizeEntry(raw));
9
+ }
10
+ catch {
11
+ // skip corrupt lines
12
+ }
13
+ }
14
+ }
15
+ replace(entries) {
16
+ this.entries = entries;
17
+ }
18
+ list() {
19
+ return [...this.entries];
20
+ }
21
+ getMetadata() {
22
+ const entry = this.entries.find((item) => item.type === "metadata");
23
+ const metadata = entry?.metadata ?? {};
24
+ return {
25
+ ...metadata,
26
+ thinkingLevel: metadata.thinkingLevel ?? metadata.reasoningEffort,
27
+ };
28
+ }
29
+ setMetadata(metadata) {
30
+ const next = [...this.entries];
31
+ const entry = {
32
+ id: "metadata",
33
+ type: "metadata",
34
+ metadata,
35
+ timestamp: Date.now(),
36
+ };
37
+ const existingIndex = next.findIndex((item) => item.type === "metadata");
38
+ if (existingIndex >= 0) {
39
+ next[existingIndex] = entry;
40
+ }
41
+ else {
42
+ next.unshift(entry);
43
+ }
44
+ this.entries = next;
45
+ return next;
46
+ }
47
+ appendMessage(message) {
48
+ const normalized = normalizeMessageToEntries(message, nextEntryId(this.entries), Date.now());
49
+ this.entries.push(...normalized);
50
+ return normalized;
51
+ }
52
+ appendSummary(summary) {
53
+ const entry = {
54
+ id: nextEntryId(this.entries),
55
+ type: "summary",
56
+ summary,
57
+ timestamp: Date.now(),
58
+ };
59
+ this.entries.push(entry);
60
+ return entry;
61
+ }
62
+ appendTodosSnapshot(todos) {
63
+ const entry = {
64
+ id: nextEntryId(this.entries),
65
+ type: "todos_snapshot",
66
+ todos: todos.map((todo) => ({ ...todo })),
67
+ timestamp: Date.now(),
68
+ };
69
+ this.entries.push(entry);
70
+ return entry;
71
+ }
72
+ getTodos() {
73
+ for (let i = this.entries.length - 1; i >= 0; i--) {
74
+ const entry = this.entries[i];
75
+ if (entry.type === "todos_snapshot") {
76
+ return entry.todos.map((todo) => ({ ...todo }));
77
+ }
78
+ }
79
+ return [];
80
+ }
81
+ appendMarker(kind, value) {
82
+ const entry = {
83
+ id: nextEntryId(this.entries),
84
+ type: "marker",
85
+ kind,
86
+ value,
87
+ timestamp: Date.now(),
88
+ };
89
+ this.entries.push(entry);
90
+ return entry;
91
+ }
92
+ toMessages() {
93
+ const messages = [];
94
+ let latestSummaryIndex = -1;
95
+ for (let index = this.entries.length - 1; index >= 0; index--) {
96
+ if (this.entries[index].type === "summary") {
97
+ latestSummaryIndex = index;
98
+ break;
99
+ }
100
+ }
101
+ if (latestSummaryIndex >= 0) {
102
+ const summary = this.entries[latestSummaryIndex];
103
+ messages.push({
104
+ role: "system",
105
+ content: `Previous conversation summary: ${summary.summary}`,
106
+ });
107
+ }
108
+ const startIndex = latestSummaryIndex >= 0 ? latestSummaryIndex + 1 : 0;
109
+ for (let index = startIndex; index < this.entries.length; index++) {
110
+ const entry = this.entries[index];
111
+ switch (entry.type) {
112
+ case "user_message":
113
+ messages.push(cloneMessage(entry.message));
114
+ break;
115
+ case "assistant_message":
116
+ messages.push({
117
+ ...entry.message,
118
+ role: "assistant",
119
+ });
120
+ break;
121
+ case "tool_call": {
122
+ const last = messages[messages.length - 1];
123
+ if (last?.role === "assistant") {
124
+ const assistant = last;
125
+ assistant.toolCalls = [...(assistant.toolCalls ?? []), { ...entry.toolCall }];
126
+ }
127
+ else {
128
+ messages.push({
129
+ role: "assistant",
130
+ content: "",
131
+ toolCalls: [{ ...entry.toolCall }],
132
+ });
133
+ }
134
+ break;
135
+ }
136
+ case "tool_result":
137
+ messages.push(cloneMessage(entry.message));
138
+ break;
139
+ default:
140
+ break;
141
+ }
142
+ }
143
+ return pruneIncompleteTail(messages);
144
+ }
145
+ }
146
+ function normalizeEntry(raw) {
147
+ if (isSessionLogEntry(raw)) {
148
+ return [raw];
149
+ }
150
+ if (raw.type === "metadata") {
151
+ return [{
152
+ id: raw.id,
153
+ type: "metadata",
154
+ metadata: raw.metadata ?? {},
155
+ timestamp: raw.timestamp,
156
+ }];
157
+ }
158
+ if (raw.type === "compaction") {
159
+ return [{
160
+ id: raw.id,
161
+ type: "summary",
162
+ summary: raw.summary ?? "",
163
+ timestamp: raw.timestamp,
164
+ }];
165
+ }
166
+ if (raw.type === "message" && raw.data) {
167
+ return normalizeMessageToEntries(raw.data, raw.id, raw.timestamp);
168
+ }
169
+ return [];
170
+ }
171
+ function normalizeMessageToEntries(message, id, timestamp) {
172
+ switch (message.role) {
173
+ case "user":
174
+ return [{ id, type: "user_message", message, timestamp }];
175
+ case "assistant": {
176
+ const assistantEntry = {
177
+ id,
178
+ type: "assistant_message",
179
+ message: {
180
+ role: "assistant",
181
+ content: message.content,
182
+ reasoning: message.reasoning,
183
+ },
184
+ timestamp,
185
+ };
186
+ const toolCallEntries = (message.toolCalls ?? []).map((toolCall, index) => ({
187
+ id: `${id}:tool:${index + 1}`,
188
+ type: "tool_call",
189
+ toolCall,
190
+ timestamp,
191
+ }));
192
+ return [assistantEntry, ...toolCallEntries];
193
+ }
194
+ case "tool":
195
+ return [{ id, type: "tool_result", message, timestamp }];
196
+ case "system":
197
+ return [{
198
+ id,
199
+ type: "summary",
200
+ summary: message.content,
201
+ timestamp,
202
+ }];
203
+ }
204
+ }
205
+ function isSessionLogEntry(entry) {
206
+ return [
207
+ "metadata",
208
+ "summary",
209
+ "marker",
210
+ "user_message",
211
+ "assistant_message",
212
+ "tool_call",
213
+ "tool_result",
214
+ "todos_snapshot",
215
+ ].includes(entry.type);
216
+ }
217
+ function nextEntryId(entries) {
218
+ return `${entries.length + 1}`;
219
+ }
220
+ function cloneMessage(message) {
221
+ if (message.role === "assistant") {
222
+ return {
223
+ ...message,
224
+ toolCalls: message.toolCalls?.map((toolCall) => ({ ...toolCall })),
225
+ };
226
+ }
227
+ if (message.role === "user" && Array.isArray(message.content)) {
228
+ return {
229
+ ...message,
230
+ content: message.content.map((part) => ({
231
+ ...part,
232
+ ...(part.type === "image_url" ? { image_url: { ...part.image_url } } : {}),
233
+ })),
234
+ };
235
+ }
236
+ return { ...message };
237
+ }
238
+ function pruneIncompleteTail(messages) {
239
+ let currentTurnStart = -1;
240
+ let hasCompletedAssistant = false;
241
+ let sawNonUserInCurrentTurn = false;
242
+ for (let i = 0; i < messages.length; i++) {
243
+ const message = messages[i];
244
+ if (message.role === "system")
245
+ continue;
246
+ if (message.role === "user") {
247
+ currentTurnStart = i;
248
+ hasCompletedAssistant = false;
249
+ sawNonUserInCurrentTurn = false;
250
+ continue;
251
+ }
252
+ if (currentTurnStart === -1) {
253
+ continue;
254
+ }
255
+ sawNonUserInCurrentTurn = true;
256
+ if (message.role === "assistant") {
257
+ const hasPendingTools = !!message.toolCalls && message.toolCalls.length > 0;
258
+ if (!hasPendingTools) {
259
+ hasCompletedAssistant = true;
260
+ }
261
+ }
262
+ }
263
+ if (currentTurnStart >= 0 && sawNonUserInCurrentTurn && !hasCompletedAssistant) {
264
+ return messages.slice(0, currentTurnStart);
265
+ }
266
+ return messages;
267
+ }
@@ -0,0 +1,55 @@
1
+ import type { AssistantMessage, Message, ThinkingLevel, Todo, ToolCall, ToolMessage, UserMessage } from "./types.js";
2
+ export interface SessionMetadata {
3
+ model?: string;
4
+ thinkingLevel?: ThinkingLevel;
5
+ reasoningEffort?: ThinkingLevel;
6
+ cwd?: string;
7
+ }
8
+ export type SessionMarkerKind = "model_switch" | "provider_switch" | "thinking_level_switch" | "skill_activated" | "mode_switch";
9
+ interface BaseSessionLogEntry {
10
+ id: string;
11
+ timestamp: number;
12
+ }
13
+ export interface SessionMetadataEntry extends BaseSessionLogEntry {
14
+ type: "metadata";
15
+ metadata: SessionMetadata;
16
+ }
17
+ export interface SessionSummaryEntry extends BaseSessionLogEntry {
18
+ type: "summary";
19
+ summary: string;
20
+ }
21
+ export interface SessionMarkerEntry extends BaseSessionLogEntry {
22
+ type: "marker";
23
+ kind: SessionMarkerKind;
24
+ value: string;
25
+ }
26
+ export interface SessionUserMessageEntry extends BaseSessionLogEntry {
27
+ type: "user_message";
28
+ message: UserMessage;
29
+ }
30
+ export interface SessionAssistantMessageEntry extends BaseSessionLogEntry {
31
+ type: "assistant_message";
32
+ message: Omit<AssistantMessage, "toolCalls">;
33
+ }
34
+ export interface SessionToolCallEntry extends BaseSessionLogEntry {
35
+ type: "tool_call";
36
+ toolCall: ToolCall;
37
+ }
38
+ export interface SessionToolResultEntry extends BaseSessionLogEntry {
39
+ type: "tool_result";
40
+ message: ToolMessage;
41
+ }
42
+ export interface SessionTodosSnapshotEntry extends BaseSessionLogEntry {
43
+ type: "todos_snapshot";
44
+ todos: Todo[];
45
+ }
46
+ export type SessionLogEntry = SessionMetadataEntry | SessionSummaryEntry | SessionMarkerEntry | SessionUserMessageEntry | SessionAssistantMessageEntry | SessionToolCallEntry | SessionToolResultEntry | SessionTodosSnapshotEntry;
47
+ export interface LegacySessionEntry {
48
+ id: string;
49
+ type: "metadata" | "message" | "compaction";
50
+ data?: Message;
51
+ summary?: string;
52
+ metadata?: SessionMetadata;
53
+ timestamp: number;
54
+ }
55
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Session Manager - Append-only JSONL persistence over a structured session log.
3
+ */
4
+ import { type CompactOptions, type CompactResult } from "./context/compact.js";
5
+ import type { Message, Todo } from "./types.js";
6
+ import type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
7
+ export type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
8
+ export declare class SessionManager {
9
+ private sessionFile;
10
+ private log;
11
+ constructor(sessionFile: string);
12
+ static create(cwd: string, sessionName?: string): SessionManager;
13
+ static resume(cwd: string, sessionName?: string): SessionManager | undefined;
14
+ static createFresh(cwd: string): SessionManager;
15
+ static listSessions(cwd: string): string[];
16
+ private load;
17
+ private persist;
18
+ private rewrite;
19
+ getMetadata(): SessionMetadata;
20
+ setMetadata(metadata: SessionMetadata): void;
21
+ appendMessage(message: Message): void;
22
+ appendCompaction(summary: string): void;
23
+ appendMarker(kind: SessionMarkerKind, value: string): void;
24
+ appendTodosSnapshot(todos: Todo[]): void;
25
+ getTodos(): Todo[];
26
+ compact(options?: CompactOptions): CompactResult;
27
+ getMessages(): Message[];
28
+ getEntries(): SessionLogEntry[];
29
+ getSessionFile(): string;
30
+ private maybeAutoCompact;
31
+ }
32
+ export declare function getSessionsDir(cwd: string): string;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Session Manager - Append-only JSONL persistence over a structured session log.
3
+ */
4
+ import { mkdirSync, appendFileSync, existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { getBubbleHome } from "./bubble-home.js";
7
+ import { compactSessionEntries } from "./context/compact.js";
8
+ import { SessionLog } from "./session-log.js";
9
+ const AUTO_COMPACT_ENTRY_THRESHOLD = 180;
10
+ const AUTO_COMPACT_KEEP_RECENT_TURNS = 3;
11
+ export class SessionManager {
12
+ sessionFile;
13
+ log = new SessionLog();
14
+ constructor(sessionFile) {
15
+ this.sessionFile = sessionFile;
16
+ if (existsSync(sessionFile)) {
17
+ this.load();
18
+ }
19
+ }
20
+ static create(cwd, sessionName) {
21
+ const file = resolveSessionFile(cwd, sessionName || `${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`);
22
+ return new SessionManager(file);
23
+ }
24
+ static resume(cwd, sessionName) {
25
+ if (sessionName) {
26
+ const file = resolveSessionFile(cwd, sessionName);
27
+ return existsSync(file) ? new SessionManager(file) : undefined;
28
+ }
29
+ const latest = this.listSessions(cwd).sort().at(-1);
30
+ if (!latest) {
31
+ return undefined;
32
+ }
33
+ return new SessionManager(resolveSessionFile(cwd, latest));
34
+ }
35
+ static createFresh(cwd) {
36
+ const file = resolveSessionFile(cwd, `${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`);
37
+ return new SessionManager(file);
38
+ }
39
+ static listSessions(cwd) {
40
+ const sessionsDir = getSessionsDir(cwd);
41
+ if (!existsSync(sessionsDir))
42
+ return [];
43
+ return readdirSync(sessionsDir).filter((file) => file.endsWith(".jsonl"));
44
+ }
45
+ load() {
46
+ const content = readFileSync(this.sessionFile, "utf-8");
47
+ const lines = content.split("\n").filter((line) => line.trim() !== "");
48
+ this.log.load(lines);
49
+ }
50
+ persist(entry) {
51
+ const dir = dirname(this.sessionFile);
52
+ if (!existsSync(dir)) {
53
+ mkdirSync(dir, { recursive: true });
54
+ }
55
+ const entries = Array.isArray(entry) ? entry : [entry];
56
+ if (entries.length === 0) {
57
+ return;
58
+ }
59
+ appendFileSync(this.sessionFile, entries.map((item) => JSON.stringify(item)).join("\n") + "\n");
60
+ }
61
+ rewrite(entries) {
62
+ const dir = dirname(this.sessionFile);
63
+ if (!existsSync(dir)) {
64
+ mkdirSync(dir, { recursive: true });
65
+ }
66
+ writeFileSync(this.sessionFile, entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n");
67
+ this.log.replace(entries);
68
+ }
69
+ getMetadata() {
70
+ return this.log.getMetadata();
71
+ }
72
+ setMetadata(metadata) {
73
+ const nextEntries = this.log.setMetadata(metadata);
74
+ this.rewrite(nextEntries);
75
+ }
76
+ appendMessage(message) {
77
+ const entries = this.log.appendMessage(message);
78
+ this.persist(entries);
79
+ this.maybeAutoCompact();
80
+ }
81
+ appendCompaction(summary) {
82
+ const entry = this.log.appendSummary(summary);
83
+ this.persist(entry);
84
+ }
85
+ appendMarker(kind, value) {
86
+ const entry = this.log.appendMarker(kind, value);
87
+ this.persist(entry);
88
+ }
89
+ appendTodosSnapshot(todos) {
90
+ const entry = this.log.appendTodosSnapshot(todos);
91
+ this.persist(entry);
92
+ this.maybeAutoCompact();
93
+ }
94
+ getTodos() {
95
+ return this.log.getTodos();
96
+ }
97
+ compact(options) {
98
+ const result = compactSessionEntries(this.log.list(), options);
99
+ if (result.compacted && result.entries) {
100
+ this.rewrite(result.entries);
101
+ }
102
+ return result;
103
+ }
104
+ getMessages() {
105
+ return this.log.toMessages();
106
+ }
107
+ getEntries() {
108
+ return this.log.list();
109
+ }
110
+ getSessionFile() {
111
+ return this.sessionFile;
112
+ }
113
+ maybeAutoCompact() {
114
+ const entries = this.log.list();
115
+ if (entries.length < AUTO_COMPACT_ENTRY_THRESHOLD) {
116
+ return;
117
+ }
118
+ const result = compactSessionEntries(entries, {
119
+ keepRecentTurns: AUTO_COMPACT_KEEP_RECENT_TURNS,
120
+ });
121
+ if (result.compacted && result.entries) {
122
+ this.rewrite(result.entries);
123
+ }
124
+ }
125
+ }
126
+ export function getSessionsDir(cwd) {
127
+ const agentDir = getBubbleHome();
128
+ const safeCwd = cwd.replace(/[/\\:]/g, "_");
129
+ const sessionsDir = join(agentDir, "sessions", safeCwd);
130
+ mkdirSync(sessionsDir, { recursive: true });
131
+ return sessionsDir;
132
+ }
133
+ function resolveSessionFile(cwd, sessionName) {
134
+ return join(getSessionsDir(cwd), sessionName);
135
+ }
@@ -0,0 +1,12 @@
1
+ import type { SkillDiagnostic, SkillRecord } from "./types.js";
2
+ export interface DiscoverSkillsOptions {
3
+ roots: Array<{
4
+ path: string;
5
+ source: SkillRecord["source"];
6
+ }>;
7
+ }
8
+ export interface DiscoverSkillsResult {
9
+ skills: SkillRecord[];
10
+ diagnostics: SkillDiagnostic[];
11
+ }
12
+ export declare function discoverSkills(options: DiscoverSkillsOptions): DiscoverSkillsResult;