@jungjaehoon/mama-os 0.8.3 → 0.9.0

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 (106) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agent/agent-loop.d.ts +1 -8
  3. package/dist/agent/agent-loop.d.ts.map +1 -1
  4. package/dist/agent/agent-loop.js +44 -159
  5. package/dist/agent/agent-loop.js.map +1 -1
  6. package/dist/agent/claude-cli-wrapper.d.ts +6 -0
  7. package/dist/agent/claude-cli-wrapper.d.ts.map +1 -1
  8. package/dist/agent/claude-cli-wrapper.js +6 -0
  9. package/dist/agent/claude-cli-wrapper.js.map +1 -1
  10. package/dist/agent/codex-mcp-process.d.ts +85 -0
  11. package/dist/agent/codex-mcp-process.d.ts.map +1 -0
  12. package/dist/agent/codex-mcp-process.js +357 -0
  13. package/dist/agent/codex-mcp-process.js.map +1 -0
  14. package/dist/agent/session-pool.d.ts +17 -2
  15. package/dist/agent/session-pool.d.ts.map +1 -1
  16. package/dist/agent/session-pool.js +51 -26
  17. package/dist/agent/session-pool.js.map +1 -1
  18. package/dist/agent/types.d.ts +9 -24
  19. package/dist/agent/types.d.ts.map +1 -1
  20. package/dist/agent/types.js.map +1 -1
  21. package/dist/api/graph-api.d.ts.map +1 -1
  22. package/dist/api/graph-api.js +133 -45
  23. package/dist/api/graph-api.js.map +1 -1
  24. package/dist/cli/commands/init.d.ts +1 -1
  25. package/dist/cli/commands/init.d.ts.map +1 -1
  26. package/dist/cli/commands/init.js +14 -25
  27. package/dist/cli/commands/init.js.map +1 -1
  28. package/dist/cli/commands/run.d.ts.map +1 -1
  29. package/dist/cli/commands/run.js +3 -10
  30. package/dist/cli/commands/run.js.map +1 -1
  31. package/dist/cli/commands/start.d.ts.map +1 -1
  32. package/dist/cli/commands/start.js +143 -54
  33. package/dist/cli/commands/start.js.map +1 -1
  34. package/dist/cli/commands/status.d.ts.map +1 -1
  35. package/dist/cli/commands/status.js +2 -7
  36. package/dist/cli/commands/status.js.map +1 -1
  37. package/dist/cli/config/config-manager.d.ts.map +1 -1
  38. package/dist/cli/config/config-manager.js +9 -17
  39. package/dist/cli/config/config-manager.js.map +1 -1
  40. package/dist/cli/config/types.d.ts +19 -25
  41. package/dist/cli/config/types.d.ts.map +1 -1
  42. package/dist/cli/config/types.js.map +1 -1
  43. package/dist/cli/index.js +2 -2
  44. package/dist/cli/index.js.map +1 -1
  45. package/dist/gateways/context-injector.d.ts.map +1 -1
  46. package/dist/gateways/context-injector.js +6 -3
  47. package/dist/gateways/context-injector.js.map +1 -1
  48. package/dist/gateways/discord.d.ts +4 -0
  49. package/dist/gateways/discord.d.ts.map +1 -1
  50. package/dist/gateways/discord.js +39 -16
  51. package/dist/gateways/discord.js.map +1 -1
  52. package/dist/gateways/message-router.d.ts +6 -1
  53. package/dist/gateways/message-router.d.ts.map +1 -1
  54. package/dist/gateways/message-router.js +92 -7
  55. package/dist/gateways/message-router.js.map +1 -1
  56. package/dist/multi-agent/agent-process-manager.d.ts.map +1 -1
  57. package/dist/multi-agent/agent-process-manager.js +36 -9
  58. package/dist/multi-agent/agent-process-manager.js.map +1 -1
  59. package/dist/multi-agent/runtime-process.d.ts +4 -4
  60. package/dist/multi-agent/runtime-process.d.ts.map +1 -1
  61. package/dist/multi-agent/runtime-process.js +9 -20
  62. package/dist/multi-agent/runtime-process.js.map +1 -1
  63. package/dist/multi-agent/types.d.ts +13 -8
  64. package/dist/multi-agent/types.d.ts.map +1 -1
  65. package/dist/multi-agent/types.js.map +1 -1
  66. package/dist/setup/setup-prompt.d.ts +1 -1
  67. package/dist/setup/setup-prompt.d.ts.map +1 -1
  68. package/dist/setup/setup-prompt.js +19 -0
  69. package/dist/setup/setup-prompt.js.map +1 -1
  70. package/dist/setup/setup-server.d.ts.map +1 -1
  71. package/dist/setup/setup-server.js +39 -16
  72. package/dist/setup/setup-server.js.map +1 -1
  73. package/dist/skills/skill-registry.d.ts.map +1 -1
  74. package/dist/skills/skill-registry.js +5 -2
  75. package/dist/skills/skill-registry.js.map +1 -1
  76. package/package.json +5 -3
  77. package/public/setup.html +12 -1
  78. package/public/viewer/js/modules/chat.js +1760 -1976
  79. package/public/viewer/js/modules/dashboard.js +613 -695
  80. package/public/viewer/js/modules/graph.js +857 -970
  81. package/public/viewer/js/modules/memory.js +357 -312
  82. package/public/viewer/js/modules/settings.js +1009 -1026
  83. package/public/viewer/js/modules/skills.js +336 -355
  84. package/public/viewer/js/utils/api.js +255 -255
  85. package/public/viewer/js/utils/debug-logger.js +20 -26
  86. package/public/viewer/js/utils/dom.js +73 -60
  87. package/public/viewer/js/utils/format.js +182 -228
  88. package/public/viewer/js/utils/markdown.js +40 -0
  89. package/public/viewer/src/modules/chat.ts +2258 -0
  90. package/public/viewer/src/modules/dashboard.ts +1052 -0
  91. package/public/viewer/src/modules/graph.ts +1080 -0
  92. package/public/viewer/src/modules/memory.ts +453 -0
  93. package/public/viewer/src/modules/settings.ts +1398 -0
  94. package/public/viewer/src/modules/skills.ts +457 -0
  95. package/public/viewer/src/types/global.d.ts +168 -0
  96. package/public/viewer/src/utils/api.ts +650 -0
  97. package/public/viewer/src/utils/debug-logger.ts +36 -0
  98. package/public/viewer/src/utils/dom.ts +138 -0
  99. package/public/viewer/src/utils/format.ts +331 -0
  100. package/public/viewer/src/utils/markdown.ts +46 -0
  101. package/public/viewer/tsconfig.viewer.json +18 -0
  102. package/public/viewer/viewer.html +214 -311
  103. package/dist/agent/codex-cli-wrapper.d.ts +0 -85
  104. package/dist/agent/codex-cli-wrapper.d.ts.map +0 -1
  105. package/dist/agent/codex-cli-wrapper.js +0 -295
  106. package/dist/agent/codex-cli-wrapper.js.map +0 -1
@@ -0,0 +1,138 @@
1
+ /**
2
+ * DOM Utility Functions
3
+ * @module utils/dom
4
+ * @version 1.0.0
5
+ */
6
+
7
+ /* eslint-env browser */
8
+
9
+ /**
10
+ * Escape HTML to prevent XSS
11
+ * @param {string} text - Text to escape
12
+ * @returns {string} Escaped HTML
13
+ */
14
+ export function escapeHtml(text: string | null | undefined): string {
15
+ if (!text) {
16
+ return '';
17
+ }
18
+ const div = document.createElement('div');
19
+ div.textContent = text;
20
+ return div.innerHTML;
21
+ }
22
+
23
+ /**
24
+ * Escape HTML for use in attribute values (also escapes quotes)
25
+ * @param {string} text - Text to escape
26
+ * @returns {string} Escaped text safe for HTML attributes
27
+ */
28
+ export function escapeAttr(text: string | null | undefined): string {
29
+ if (!text) {
30
+ return '';
31
+ }
32
+ return escapeHtml(text).replace(/"/g, '"').replace(/'/g, ''');
33
+ }
34
+
35
+ /**
36
+ * Get DOM element by id with typed generic casting
37
+ * @param {string} id - Element id
38
+ * @returns {T | null} Matched element or null
39
+ */
40
+ export function getElementByIdOrNull<T extends Element>(id: string): T | null {
41
+ const element = document.getElementById(id);
42
+ return element ? (element as unknown as T) : null;
43
+ }
44
+
45
+ /**
46
+ * Normalize error values to a safe message string
47
+ * @param error - Unknown thrown value
48
+ * @returns {string} Error message
49
+ */
50
+ export function getErrorMessage(error: unknown): string {
51
+ if (error instanceof Error) {
52
+ return error.message;
53
+ }
54
+ return String(error);
55
+ }
56
+
57
+ /**
58
+ * Debounce function calls
59
+ * @param {Function} func - Function to debounce
60
+ * @param {number} wait - Wait time in milliseconds
61
+ * @returns {Function} Debounced function
62
+ */
63
+ export function debounce<T extends (...args: unknown[]) => unknown>(
64
+ func: T,
65
+ wait: number
66
+ ): (...args: Parameters<T>) => void {
67
+ let timeout: ReturnType<typeof setTimeout> | undefined;
68
+ return function executedFunction(...args) {
69
+ const later = () => {
70
+ timeout = undefined;
71
+ func(...args);
72
+ };
73
+ if (timeout) {
74
+ clearTimeout(timeout);
75
+ }
76
+ timeout = setTimeout(later, wait);
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Show toast notification
82
+ * @param {string} message - Message to display
83
+ * @param {number} duration - Duration in milliseconds
84
+ */
85
+ export function showToast(message: string, duration = 3000): void {
86
+ // Remove existing toast
87
+ const existingToast = document.querySelector('.toast-notification');
88
+ if (existingToast) {
89
+ existingToast.remove();
90
+ }
91
+
92
+ const toast = document.createElement('div');
93
+ toast.className = 'toast-notification';
94
+ toast.textContent = message;
95
+ document.body.appendChild(toast);
96
+
97
+ // Trigger animation
98
+ requestAnimationFrame(() => {
99
+ toast.classList.add('visible');
100
+ });
101
+
102
+ // Auto-remove
103
+ setTimeout(() => {
104
+ toast.classList.remove('visible');
105
+ setTimeout(() => toast.remove(), 300);
106
+ }, duration);
107
+ }
108
+
109
+ /**
110
+ * Scroll element to bottom
111
+ * @param {HTMLElement} container - Container to scroll
112
+ */
113
+ export function scrollToBottom(container: HTMLElement): void {
114
+ // Use setTimeout to ensure DOM has updated before scrolling
115
+ const doScroll = () => {
116
+ container.scrollTop = container.scrollHeight;
117
+ if (container.scrollTo) {
118
+ container.scrollTo({ top: container.scrollHeight, behavior: 'auto' });
119
+ }
120
+ };
121
+ setTimeout(doScroll, 50);
122
+ requestAnimationFrame(doScroll);
123
+ }
124
+
125
+ /**
126
+ * Auto-resize textarea to fit content
127
+ * @param {HTMLTextAreaElement} textarea - Textarea element
128
+ * @param {number} maxRows - Maximum number of rows (default: 5)
129
+ */
130
+ export function autoResizeTextarea(textarea: HTMLTextAreaElement, maxRows = 5): void {
131
+ textarea.style.height = 'auto';
132
+ const computedLineHeight = Number.parseFloat(getComputedStyle(textarea).lineHeight);
133
+ const lineHeight =
134
+ Number.isFinite(computedLineHeight) && computedLineHeight > 0 ? computedLineHeight : 20;
135
+ const maxHeight = lineHeight * maxRows;
136
+ const newHeight = Math.min(textarea.scrollHeight, maxHeight);
137
+ textarea.style.height = newHeight + 'px';
138
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Formatting Utility Functions
3
+ * @module utils/format
4
+ * @version 1.1.0
5
+ */
6
+
7
+ /* eslint-env browser */
8
+
9
+ /**
10
+ * Known Claude model name mappings
11
+ * https://platform.claude.com/docs/en/about-claude/models/overview
12
+ */
13
+ const MODEL_NAMES: Record<string, string> = {
14
+ // Latest models
15
+ 'claude-opus-4-6': 'Claude Opus 4.6',
16
+ 'claude-sonnet-4-5-20250929': 'Claude Sonnet 4.5',
17
+ 'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
18
+ // Legacy models
19
+ 'claude-opus-4-5-20251101': 'Claude Opus 4.5',
20
+ 'claude-sonnet-4-20250514': 'Claude Sonnet 4',
21
+ 'claude-opus-4-20250514': 'Claude Opus 4',
22
+ 'claude-3-7-sonnet-20250219': 'Claude Sonnet 3.7',
23
+ 'claude-3-haiku-20240307': 'Claude Haiku 3',
24
+ // GPT models
25
+ 'gpt-5.3-codex': 'GPT-5.3 Codex',
26
+ 'gpt-5.2': 'GPT-5.2',
27
+ 'gpt-5.1': 'GPT-5.1',
28
+ 'gpt-4.1': 'GPT-4.1',
29
+ 'gpt-4o': 'GPT-4o',
30
+ 'gpt-4o-mini': 'GPT-4o Mini',
31
+ 'o1': 'o1',
32
+ 'o1-mini': 'o1 Mini',
33
+ 'o3-mini': 'o3 Mini',
34
+ };
35
+
36
+ /**
37
+ * Get human-friendly model name from model ID
38
+ * @param {string} model - Model ID (e.g., 'claude-sonnet-4-20250514')
39
+ * @returns {string} Human-friendly name (e.g., 'Claude 4 Sonnet')
40
+ */
41
+ export function formatModelName(model: string | null | undefined): string {
42
+ if (!model || model === 'default') {
43
+ return 'Default';
44
+ }
45
+
46
+ // Check known model mappings
47
+ if (MODEL_NAMES[model]) {
48
+ return MODEL_NAMES[model];
49
+ }
50
+
51
+ // Try to extract friendly name from model string
52
+ if (model.includes('opus')) {
53
+ return 'Claude Opus';
54
+ }
55
+ if (model.includes('sonnet')) {
56
+ return 'Claude Sonnet';
57
+ }
58
+ if (model.includes('haiku')) {
59
+ return 'Claude Haiku';
60
+ }
61
+
62
+ return model;
63
+ }
64
+
65
+ /**
66
+ * Format message timestamp
67
+ * @param {Date} date - Date object
68
+ * @returns {string} Formatted time (HH:MM)
69
+ */
70
+ export function formatMessageTime(date: Date): string {
71
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
72
+ }
73
+
74
+ /**
75
+ * Format checkpoint timestamp
76
+ * @param {string|Date} timestamp - Timestamp
77
+ * @returns {string} Formatted relative time
78
+ */
79
+ export function formatCheckpointTime(timestamp: string | Date): string {
80
+ const date = new Date(timestamp);
81
+ const now = new Date();
82
+ const diff = now.getTime() - date.getTime();
83
+
84
+ if (diff < 3600000) {
85
+ const mins = Math.floor(diff / 60000);
86
+ return `${mins}m ago`;
87
+ }
88
+ if (diff < 86400000) {
89
+ const hours = Math.floor(diff / 3600000);
90
+ return `${hours}h ago`;
91
+ }
92
+
93
+ return (
94
+ date.toLocaleDateString() +
95
+ ' ' +
96
+ date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Format relative time
102
+ * @param {string|Date} timestamp - Timestamp
103
+ * @returns {string} Relative time string
104
+ */
105
+ export function formatRelativeTime(timestamp: string | number | Date | null | undefined): string {
106
+ if (!timestamp) {
107
+ return 'Never';
108
+ }
109
+
110
+ const date = new Date(timestamp);
111
+ if (Number.isNaN(date.getTime())) {
112
+ return 'Never';
113
+ }
114
+
115
+ const now = new Date();
116
+ const diff = now.getTime() - date.getTime();
117
+
118
+ if (diff < 60000) {
119
+ return 'Just now';
120
+ }
121
+ if (diff < 3600000) {
122
+ const mins = Math.floor(diff / 60000);
123
+ return `${mins}m ago`;
124
+ }
125
+ if (diff < 86400000) {
126
+ const hours = Math.floor(diff / 3600000);
127
+ return `${hours}h ago`;
128
+ }
129
+ if (diff < 604800000) {
130
+ const days = Math.floor(diff / 86400000);
131
+ return `${days}d ago`;
132
+ }
133
+
134
+ return date.toLocaleDateString();
135
+ }
136
+
137
+ /**
138
+ * Truncate text with ellipsis
139
+ * @param {string} text - Text to truncate
140
+ * @param {number} maxLength - Maximum length
141
+ * @returns {string} Truncated text
142
+ */
143
+ export function truncateText(text: string | null | undefined, maxLength: number): string {
144
+ if (!text) {
145
+ return '';
146
+ }
147
+ if (text.length <= maxLength) {
148
+ return text;
149
+ }
150
+ return text.substring(0, maxLength) + '...';
151
+ }
152
+
153
+ /**
154
+ * Extract first meaningful line from text
155
+ * @param {string} text - Text to extract from
156
+ * @returns {string} First meaningful line
157
+ */
158
+ export function extractFirstLine(text: string | null | undefined): string {
159
+ if (!text) {
160
+ return 'No summary';
161
+ }
162
+ const lines = text.split('\n').filter((l) => l.trim() && !l.startsWith('**'));
163
+ return lines[0] || text.substring(0, 100);
164
+ }
165
+
166
+ /**
167
+ * Format assistant message with markdown support
168
+ * @param {string} text - Text to format
169
+ * @returns {string} Formatted HTML
170
+ */
171
+ export function formatAssistantMessage(text: string | null | undefined): string {
172
+ if (!text) {
173
+ return '';
174
+ }
175
+
176
+ // First escape HTML to prevent XSS
177
+ let formatted = escapeHtmlForMarkdown(text);
178
+
179
+ // Detect and wrap checkpoint/context sections in collapsible
180
+ formatted = wrapCheckpointSections(formatted);
181
+
182
+ // Code blocks with optional language (```js ... ```)
183
+ formatted = formatted.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, lang, code) => {
184
+ const langClass = lang ? ` class="language-${lang}"` : '';
185
+ return `<pre class="code-block"><code${langClass}>${code.trim()}</code></pre>`;
186
+ });
187
+
188
+ // Inline code
189
+ formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>');
190
+
191
+ // Bold
192
+ formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
193
+
194
+ // Italic (avoiding conflicts with bold) - Safari-compatible without lookbehind
195
+ // Process after bold, match single asterisks not part of ** sequences
196
+ formatted = formatted.replace(/([^*]|^)\*([^*]+)\*([^*]|$)/g, '$1<em>$2</em>$3');
197
+
198
+ // Helper: build safe media HTML from captured filename
199
+ // Note: filename may contain HTML entities from prior escaping, so decode first
200
+ const buildMediaHtml = (filename: string): string => {
201
+ const decodedName = decodeHtmlEntities(filename);
202
+ const safeName = encodeURIComponent(decodedName);
203
+ const safeAlt = escapeHtmlForMarkdown(decodedName).replace(/"/g, '&quot;');
204
+ const ext = decodedName.split('.').pop()?.toLowerCase() || '';
205
+ const imgExts = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
206
+ if (imgExts.includes(ext)) {
207
+ return `<div class="media-inline"><img src="/api/media/${safeName}" class="max-w-[300px] rounded-lg my-1 cursor-pointer" data-lightbox="/api/media/${safeName}" alt="${safeAlt}"/><a href="/api/media/download/${safeName}" class="text-xs text-blue-500 hover:underline block">Download ${safeAlt}</a></div>`;
208
+ }
209
+ return `<a href="/api/media/download/${safeName}" class="text-blue-500 hover:underline">Download ${safeAlt}</a>`;
210
+ };
211
+
212
+ // Markdown images: ![alt](media-path) — render as inline images
213
+ // Note: exclude /api/media/download/ paths (they're already download links)
214
+ formatted = formatted.replace(
215
+ /!\[([^\]]*)\]\((?:~\/\.mama\/workspace\/media\/(?:outbound|inbound)\/|\/home\/[^/]+\/\.mama\/workspace\/media\/(?:outbound|inbound)\/|\/api\/media\/(?!download\/))([^)]+)\)/gi,
216
+ (_match, _alt, filename) => buildMediaHtml(filename)
217
+ );
218
+
219
+ // First: strip any <a> wrappers around media paths (from markdown link handler)
220
+ formatted = formatted.replace(
221
+ /<a\s+href="(?:~\/\.mama\/workspace\/media\/(?:outbound|inbound)\/|\/home\/[^/]+\/\.mama\/workspace\/media\/(?:outbound|inbound)\/)([^"]+)"[^>]*>[^<]*<\/a>/gi,
222
+ (_match, filename) => buildMediaHtml(filename)
223
+ );
224
+ // Then: handle bare media paths not already inside HTML tags
225
+ // Safari-compatible: use capture group instead of lookbehind
226
+ formatted = formatted.replace(
227
+ /(^|[^"'])(?:~\/\.mama\/workspace\/media\/(?:outbound|inbound)\/|\/home\/[^/]+\/\.mama\/workspace\/media\/(?:outbound|inbound)\/)([^\s<"']+\.(png|jpg|jpeg|gif|webp|svg|pdf))/gi,
228
+ (_, prefix, filename) => prefix + buildMediaHtml(filename)
229
+ );
230
+
231
+ // Headers (## and ###)
232
+ formatted = formatted.replace(
233
+ /^### (.+)$/gm,
234
+ '<h4 class="text-sm font-semibold mt-2 mb-1">$1</h4>'
235
+ );
236
+ formatted = formatted.replace(
237
+ /^## (.+)$/gm,
238
+ '<h3 class="text-base font-semibold mt-3 mb-1">$1</h3>'
239
+ );
240
+
241
+ // Bullet lists (- item)
242
+ formatted = formatted.replace(/^- (.+)$/gm, '<li class="ml-4">• $1</li>');
243
+
244
+ // Quiz choices as buttons - patterns like **A)** text or A) text
245
+ // Also handles blockquote prefix (> or &gt;)
246
+ // Match patterns: A) text, **A)** text, > A) text, etc.
247
+ formatted = formatted.replace(
248
+ /^(?:&gt;\s*)?(?:<strong>)?([A-D])\)(?:<\/strong>)?\s*(.+)$/gim,
249
+ (match, letter, text) => {
250
+ const upperLetter = letter.toUpperCase();
251
+ return `<button class="quiz-choice-btn" type="button" data-choice="${upperLetter}">${upperLetter}) ${text.trim()}</button>`;
252
+ }
253
+ );
254
+
255
+ // Line breaks
256
+ formatted = formatted.replace(/\n/g, '<br>');
257
+
258
+ // Clean up multiple <br> in lists
259
+ formatted = formatted.replace(/<\/li><br><li/g, '</li><li');
260
+
261
+ // Clean up <br> before/after quiz buttons
262
+ formatted = formatted.replace(/<br>(<button class="quiz-choice-btn")/g, '$1');
263
+ formatted = formatted.replace(/(<\/button>)<br>/g, '$1');
264
+
265
+ return formatted;
266
+ }
267
+
268
+ /**
269
+ * Escape HTML for markdown processing
270
+ * @param {string} text - Text to escape
271
+ * @returns {string} Escaped text
272
+ */
273
+ function escapeHtmlForMarkdown(text: string): string {
274
+ const div = document.createElement('div');
275
+ div.textContent = text;
276
+ return div.innerHTML;
277
+ }
278
+
279
+ /**
280
+ * Decode HTML entities back to plain text
281
+ * Used to fix double-encoding when building URLs from already-escaped content
282
+ * @param {string} text - Text with HTML entities
283
+ * @returns {string} Decoded plain text
284
+ */
285
+ function decodeHtmlEntities(text: string): string {
286
+ const div = document.createElement('div');
287
+ div.innerHTML = text;
288
+ return div.textContent || div.innerText || '';
289
+ }
290
+
291
+ /**
292
+ * Wrap checkpoint/context sections in collapsible elements
293
+ * Detects patterns like "šŸ“ Summary", "šŸŽÆ Goal", "Recent decisions", etc.
294
+ * @param {string} text - Text to process
295
+ * @returns {string} Text with collapsible sections
296
+ */
297
+ function wrapCheckpointSections(text: string): string {
298
+ // Pattern to detect checkpoint section start
299
+ // Matches: "šŸ“ Summary", "šŸŽÆ Goal", "šŸ“ **Last Checkpoint**", etc.
300
+ const checkpointStartPatterns = [
301
+ /^(šŸ“\s*\*?\*?Summary of past work|šŸ“\s*\*?\*?Last Checkpoint)/m,
302
+ /^(šŸŽÆ\s*Goal)/m,
303
+ ];
304
+
305
+ // Check if this message contains a checkpoint section
306
+ const hasCheckpoint = checkpointStartPatterns.some((p) => p.test(text));
307
+ if (!hasCheckpoint) {
308
+ return text;
309
+ }
310
+
311
+ // Find the checkpoint section boundaries
312
+ // It typically starts with "šŸ“" and ends before "---" or end of message
313
+ const checkpointMatch = text.match(/(šŸ“[\s\S]*?)(?=\n---\nšŸš€|\n---\n\nšŸš€|---\n\nšŸš€|$)/);
314
+
315
+ if (!checkpointMatch) {
316
+ return text;
317
+ }
318
+
319
+ const checkpointContent = checkpointMatch[1];
320
+ const uniqueId = 'cp-' + Math.random().toString(36).substring(2, 11);
321
+
322
+ // Create collapsible wrapper - summary must be first child (no newlines before it)
323
+ const collapsibleHtml =
324
+ `<details class="checkpoint-collapse" id="${uniqueId}">` +
325
+ `<summary class="checkpoint-summary">šŸ“ Session Context <span class="collapse-hint">(tap to expand)</span></summary>` +
326
+ `<div class="checkpoint-content">${checkpointContent}</div>` +
327
+ `</details>`;
328
+
329
+ // Replace the checkpoint content with collapsible version
330
+ return text.replace(checkpointContent, collapsibleHtml);
331
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Safe markdown rendering utilities.
3
+ */
4
+
5
+ /* eslint-env browser */
6
+
7
+ import { escapeHtml } from './dom.js';
8
+
9
+ const MARKDOWN_PARSE_OPTIONS = {} as const;
10
+
11
+ /**
12
+ * Render markdown string to HTML and sanitize.
13
+ *
14
+ * - Uses `marked.parse` when available.
15
+ * - Falls back to plain text rendering when markdown parser is unavailable.
16
+ * - Applies DOMPurify when available; otherwise returns parser output as-is.
17
+ */
18
+ export function renderSafeMarkdown(markdown: string): string {
19
+ if (!markdown) {
20
+ return '';
21
+ }
22
+
23
+ const markdownText = String(markdown);
24
+
25
+ let html: string;
26
+ try {
27
+ if (typeof marked !== 'undefined' && typeof marked.parse === 'function') {
28
+ html = marked.parse(markdownText, MARKDOWN_PARSE_OPTIONS);
29
+ } else {
30
+ throw new Error('marked not available');
31
+ }
32
+ } catch {
33
+ const fallback = escapeHtml(markdownText);
34
+ html = fallback.replace(/\n/g, '<br/>');
35
+ }
36
+
37
+ if (typeof DOMPurify !== 'undefined' && typeof DOMPurify.sanitize === 'function') {
38
+ return DOMPurify.sanitize(html);
39
+ }
40
+
41
+ if (typeof console !== 'undefined') {
42
+ console.warn('[SafeMarkdown] DOMPurify is unavailable. Rendering escaped content as fallback.');
43
+ }
44
+ const safeFallback = escapeHtml(markdownText).replace(/\\n/g, '<br/>');
45
+ return safeFallback;
46
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "strict": false,
8
+ "noUnusedLocals": false,
9
+ "noUnusedParameters": false,
10
+ "noEmitOnError": false,
11
+ "outDir": "./js",
12
+ "rootDir": "./src",
13
+ "skipLibCheck": true,
14
+ "esModuleInterop": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }