@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.
- package/CHANGELOG.md +10 -0
- package/dist/agent/agent-loop.d.ts +1 -8
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +44 -159
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/claude-cli-wrapper.d.ts +6 -0
- package/dist/agent/claude-cli-wrapper.d.ts.map +1 -1
- package/dist/agent/claude-cli-wrapper.js +6 -0
- package/dist/agent/claude-cli-wrapper.js.map +1 -1
- package/dist/agent/codex-mcp-process.d.ts +85 -0
- package/dist/agent/codex-mcp-process.d.ts.map +1 -0
- package/dist/agent/codex-mcp-process.js +357 -0
- package/dist/agent/codex-mcp-process.js.map +1 -0
- package/dist/agent/session-pool.d.ts +17 -2
- package/dist/agent/session-pool.d.ts.map +1 -1
- package/dist/agent/session-pool.js +51 -26
- package/dist/agent/session-pool.js.map +1 -1
- package/dist/agent/types.d.ts +9 -24
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/api/graph-api.d.ts.map +1 -1
- package/dist/api/graph-api.js +133 -45
- package/dist/api/graph-api.js.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +14 -25
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +3 -10
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +143 -54
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +2 -7
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/config/config-manager.d.ts.map +1 -1
- package/dist/cli/config/config-manager.js +9 -17
- package/dist/cli/config/config-manager.js.map +1 -1
- package/dist/cli/config/types.d.ts +19 -25
- package/dist/cli/config/types.d.ts.map +1 -1
- package/dist/cli/config/types.js.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/gateways/context-injector.d.ts.map +1 -1
- package/dist/gateways/context-injector.js +6 -3
- package/dist/gateways/context-injector.js.map +1 -1
- package/dist/gateways/discord.d.ts +4 -0
- package/dist/gateways/discord.d.ts.map +1 -1
- package/dist/gateways/discord.js +39 -16
- package/dist/gateways/discord.js.map +1 -1
- package/dist/gateways/message-router.d.ts +6 -1
- package/dist/gateways/message-router.d.ts.map +1 -1
- package/dist/gateways/message-router.js +92 -7
- package/dist/gateways/message-router.js.map +1 -1
- package/dist/multi-agent/agent-process-manager.d.ts.map +1 -1
- package/dist/multi-agent/agent-process-manager.js +36 -9
- package/dist/multi-agent/agent-process-manager.js.map +1 -1
- package/dist/multi-agent/runtime-process.d.ts +4 -4
- package/dist/multi-agent/runtime-process.d.ts.map +1 -1
- package/dist/multi-agent/runtime-process.js +9 -20
- package/dist/multi-agent/runtime-process.js.map +1 -1
- package/dist/multi-agent/types.d.ts +13 -8
- package/dist/multi-agent/types.d.ts.map +1 -1
- package/dist/multi-agent/types.js.map +1 -1
- package/dist/setup/setup-prompt.d.ts +1 -1
- package/dist/setup/setup-prompt.d.ts.map +1 -1
- package/dist/setup/setup-prompt.js +19 -0
- package/dist/setup/setup-prompt.js.map +1 -1
- package/dist/setup/setup-server.d.ts.map +1 -1
- package/dist/setup/setup-server.js +39 -16
- package/dist/setup/setup-server.js.map +1 -1
- package/dist/skills/skill-registry.d.ts.map +1 -1
- package/dist/skills/skill-registry.js +5 -2
- package/dist/skills/skill-registry.js.map +1 -1
- package/package.json +5 -3
- package/public/setup.html +12 -1
- package/public/viewer/js/modules/chat.js +1760 -1976
- package/public/viewer/js/modules/dashboard.js +613 -695
- package/public/viewer/js/modules/graph.js +857 -970
- package/public/viewer/js/modules/memory.js +357 -312
- package/public/viewer/js/modules/settings.js +1009 -1026
- package/public/viewer/js/modules/skills.js +336 -355
- package/public/viewer/js/utils/api.js +255 -255
- package/public/viewer/js/utils/debug-logger.js +20 -26
- package/public/viewer/js/utils/dom.js +73 -60
- package/public/viewer/js/utils/format.js +182 -228
- package/public/viewer/js/utils/markdown.js +40 -0
- package/public/viewer/src/modules/chat.ts +2258 -0
- package/public/viewer/src/modules/dashboard.ts +1052 -0
- package/public/viewer/src/modules/graph.ts +1080 -0
- package/public/viewer/src/modules/memory.ts +453 -0
- package/public/viewer/src/modules/settings.ts +1398 -0
- package/public/viewer/src/modules/skills.ts +457 -0
- package/public/viewer/src/types/global.d.ts +168 -0
- package/public/viewer/src/utils/api.ts +650 -0
- package/public/viewer/src/utils/debug-logger.ts +36 -0
- package/public/viewer/src/utils/dom.ts +138 -0
- package/public/viewer/src/utils/format.ts +331 -0
- package/public/viewer/src/utils/markdown.ts +46 -0
- package/public/viewer/tsconfig.viewer.json +18 -0
- package/public/viewer/viewer.html +214 -311
- package/dist/agent/codex-cli-wrapper.d.ts +0 -85
- package/dist/agent/codex-cli-wrapper.d.ts.map +0 -1
- package/dist/agent/codex-cli-wrapper.js +0 -295
- 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, '"');
|
|
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:  ā 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 >)
|
|
246
|
+
// Match patterns: A) text, **A)** text, > A) text, etc.
|
|
247
|
+
formatted = formatted.replace(
|
|
248
|
+
/^(?:>\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
|
+
}
|