@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
@@ -6,138 +6,143 @@
6
6
  * Manages skill browsing, installation, and configuration
7
7
  * across MAMA, Cowork, and OpenClaw sources.
8
8
  */
9
-
10
9
  /* eslint-env browser */
11
- /* global marked */
12
-
13
10
  import { API } from '../utils/api.js';
14
11
  import { DebugLogger } from '../utils/debug-logger.js';
15
-
12
+ import { getElementByIdOrNull } from '../utils/dom.js';
13
+ import { renderSafeMarkdown } from '../utils/markdown.js';
16
14
  const logger = new DebugLogger('Skills');
17
-
18
15
  /**
19
16
  * Skills marketplace module
20
17
  */
21
18
  export const SkillsModule = {
22
- /** All skills (installed + catalog) */
23
- installed: [],
24
- catalog: [],
25
- /** Current filter */
26
- currentFilter: 'all',
27
- /** Search query */
28
- searchQuery: '',
29
- /** Whether initialized */
30
- _initialized: false,
31
- /** Debounce timer */
32
- _searchTimer: null,
33
-
34
- /**
35
- * Initialize the skills tab
36
- */
37
- async init() {
38
- if (!this._initialized) {
39
- this._bindEvents();
40
- this._initialized = true;
41
- }
42
- await this.loadSkills();
43
- this.render();
44
- },
45
-
46
- /**
47
- * Load installed + catalog skills
48
- */
49
- async loadSkills() {
50
- try {
51
- const [installedRes, catalogRes] = await Promise.all([
52
- API.getSkills().catch(() => ({ skills: [] })),
53
- API.getSkillCatalog('all').catch(() => ({ skills: [] })),
54
- ]);
55
-
56
- this.installed = installedRes.skills || [];
57
- this.catalog = (catalogRes.skills || []).filter(
58
- (s) => !this.installed.some((i) => i.id === s.id && i.source === s.source)
59
- );
60
- } catch (error) {
61
- logger.error('Failed to load:', error);
62
- }
63
- },
64
-
65
- /**
66
- * Bind UI events
67
- */
68
- _bindEvents() {
69
- // Search input
70
- const searchInput = document.getElementById('skills-search');
71
- if (searchInput) {
72
- searchInput.addEventListener('input', (e) => {
73
- clearTimeout(this._searchTimer);
74
- this._searchTimer = setTimeout(() => {
75
- this.searchQuery = e.target.value.trim();
76
- this.render();
77
- }, 300);
78
- });
79
- }
80
-
81
- // URL install
82
- const urlBtn = document.getElementById('skills-url-install-btn');
83
- if (urlBtn) {
84
- urlBtn.addEventListener('click', () => {
85
- const input = document.getElementById('skills-url-input');
86
- const url = input?.value?.trim();
87
- if (url) {
88
- this.installFromUrl(url);
19
+ /** All skills (installed + catalog) */
20
+ installed: [],
21
+ catalog: [],
22
+ /** Current filter */
23
+ currentFilter: 'all',
24
+ /** Search query */
25
+ searchQuery: '',
26
+ /** Whether initialized */
27
+ _initialized: false,
28
+ /** Debounce timer */
29
+ _searchTimer: null,
30
+ /**
31
+ * Initialize the skills tab
32
+ */
33
+ async init() {
34
+ if (!this._initialized) {
35
+ this._bindEvents();
36
+ this._initialized = true;
89
37
  }
90
- });
91
- }
92
- const urlInput = document.getElementById('skills-url-input');
93
- if (urlInput) {
94
- urlInput.addEventListener('keydown', (e) => {
95
- if (e.key === 'Enter') {
96
- const url = e.target.value.trim();
97
- if (url) {
98
- this.installFromUrl(url);
99
- }
38
+ await this.loadSkills();
39
+ this.render();
40
+ },
41
+ /**
42
+ * Load installed + catalog skills
43
+ */
44
+ async loadSkills() {
45
+ try {
46
+ const [installedResponse, catalogResponse] = await Promise.all([
47
+ API.getSkills(),
48
+ API.getSkillCatalog('all'),
49
+ ]);
50
+ this.installed = installedResponse.skills || [];
51
+ const catalogItems = catalogResponse.skills || [];
52
+ const installedMap = new Set(this.installed.map((item) => `${item.source}::${item.id}`));
53
+ this.catalog = catalogItems.filter((s) => !installedMap.has(`${s.source}::${s.id}`));
100
54
  }
101
- });
102
- }
103
-
104
- // Filter buttons
105
- const filterBar = document.getElementById('skills-filter-bar');
106
- if (filterBar) {
107
- filterBar.addEventListener('click', (e) => {
108
- const btn = e.target.closest('[data-filter]');
109
- if (!btn) {
110
- return;
55
+ catch (error) {
56
+ const message = error instanceof Error ? error.message : String(error);
57
+ logger.error('Unexpected error while loading skills:', message);
58
+ // Initialize with empty arrays on failure
59
+ this.installed = [];
60
+ this.catalog = [];
111
61
  }
112
- this.currentFilter = btn.dataset.filter;
113
- // Update active state
114
- filterBar.querySelectorAll('[data-filter]').forEach((b) => {
115
- b.classList.toggle('bg-yellow-400', b.dataset.filter === this.currentFilter);
116
- b.classList.toggle('text-gray-900', b.dataset.filter === this.currentFilter);
117
- b.classList.toggle('bg-gray-700', b.dataset.filter !== this.currentFilter);
118
- b.classList.toggle('text-gray-300', b.dataset.filter !== this.currentFilter);
119
- });
120
- this.render();
121
- });
122
- }
123
- },
124
-
125
- /**
126
- * Render the full skills view
127
- */
128
- render() {
129
- const container = document.getElementById('skills-content');
130
- if (!container) {
131
- return;
132
- }
133
-
134
- const installed = this._filterSkills(this.installed);
135
- const available = this._filterSkills(this.catalog);
136
-
137
- container.innerHTML = `
138
- ${
139
- installed.length > 0
140
- ? `
62
+ },
63
+ /**
64
+ * Bind UI events
65
+ */
66
+ _bindEvents() {
67
+ // Search input
68
+ const searchInput = getElementByIdOrNull('skills-search');
69
+ if (searchInput) {
70
+ searchInput.addEventListener('input', (e) => {
71
+ clearTimeout(this._searchTimer);
72
+ this._searchTimer = setTimeout(() => {
73
+ const target = e.target;
74
+ if (!(target instanceof HTMLInputElement)) {
75
+ return;
76
+ }
77
+ this.searchQuery = target.value.trim();
78
+ this.render();
79
+ }, 300);
80
+ });
81
+ }
82
+ // URL install
83
+ const urlBtn = getElementByIdOrNull('skills-url-install-btn');
84
+ if (urlBtn) {
85
+ urlBtn.addEventListener('click', () => {
86
+ const input = getElementByIdOrNull('skills-url-input');
87
+ const url = input?.value?.trim();
88
+ if (url) {
89
+ this.installFromUrl(url);
90
+ }
91
+ });
92
+ }
93
+ const urlInput = getElementByIdOrNull('skills-url-input');
94
+ if (urlInput) {
95
+ urlInput.addEventListener('keydown', (e) => {
96
+ if (e.key === 'Enter') {
97
+ const target = e.target;
98
+ if (!(target instanceof HTMLInputElement)) {
99
+ return;
100
+ }
101
+ const url = target.value.trim();
102
+ if (url) {
103
+ this.installFromUrl(url);
104
+ }
105
+ }
106
+ });
107
+ }
108
+ // Filter buttons
109
+ const filterBar = getElementByIdOrNull('skills-filter-bar');
110
+ if (filterBar) {
111
+ filterBar.addEventListener('click', (e) => {
112
+ const target = e.target;
113
+ const btn = target instanceof HTMLElement ? target.closest('[data-filter]') : null;
114
+ if (!btn) {
115
+ return;
116
+ }
117
+ const filter = btn.dataset.filter;
118
+ if (!filter) {
119
+ return;
120
+ }
121
+ this.currentFilter = filter;
122
+ // Update active state
123
+ filterBar.querySelectorAll('[data-filter]').forEach((b) => {
124
+ b.classList.toggle('bg-yellow-400', b.dataset.filter === this.currentFilter);
125
+ b.classList.toggle('text-gray-900', b.dataset.filter === this.currentFilter);
126
+ b.classList.toggle('bg-gray-700', b.dataset.filter !== this.currentFilter);
127
+ b.classList.toggle('text-gray-300', b.dataset.filter !== this.currentFilter);
128
+ });
129
+ this.render();
130
+ });
131
+ }
132
+ },
133
+ /**
134
+ * Render the full skills view
135
+ */
136
+ render() {
137
+ const container = getElementByIdOrNull('skills-content');
138
+ if (!container) {
139
+ return;
140
+ }
141
+ const installed = this._filterSkills(this.installed);
142
+ const available = this._filterSkills(this.catalog);
143
+ container.innerHTML = `
144
+ ${installed.length > 0
145
+ ? `
141
146
  <div class="mb-6">
142
147
  <h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">
143
148
  Installed (${installed.length})
@@ -147,15 +152,13 @@ export const SkillsModule = {
147
152
  </div>
148
153
  </div>
149
154
  `
150
- : ''
151
- }
155
+ : ''}
152
156
 
153
157
  <div class="mb-6">
154
158
  <h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">
155
159
  Available (${available.length})
156
160
  </h3>
157
- ${
158
- available.length > 0
161
+ ${available.length > 0
159
162
  ? `
160
163
  <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
161
164
  ${available.map((s) => this._renderCard(s, false)).join('')}
@@ -165,74 +168,72 @@ export const SkillsModule = {
165
168
  <p class="text-gray-500 text-sm">
166
169
  ${this.searchQuery ? 'No skills match your search.' : 'Loading catalog...'}
167
170
  </p>
168
- `
169
- }
171
+ `}
170
172
  </div>
171
173
  `;
172
-
173
- // Bind card actions
174
- container.querySelectorAll('[data-action]').forEach((btn) => {
175
- btn.addEventListener('click', (e) => {
176
- e.stopPropagation();
177
- const action = btn.dataset.action;
178
- const id = btn.dataset.id;
179
- const source = btn.dataset.source;
180
- if (action === 'install') {
181
- this.install(source, id);
182
- } else if (action === 'uninstall') {
183
- this.uninstall(source, id);
184
- } else if (action === 'toggle') {
185
- this.toggle(source, id, btn.dataset.enabled !== 'true');
174
+ // Bind card actions
175
+ container.querySelectorAll('[data-action]').forEach((btn) => {
176
+ btn.addEventListener('click', (e) => {
177
+ e.stopPropagation();
178
+ const action = btn.dataset.action;
179
+ const id = btn.dataset.id;
180
+ const source = btn.dataset.source;
181
+ if (!action || !id || !source) {
182
+ return;
183
+ }
184
+ if (action === 'install') {
185
+ this.install(source, id);
186
+ }
187
+ else if (action === 'uninstall') {
188
+ this.uninstall(source, id);
189
+ }
190
+ else if (action === 'toggle') {
191
+ this.toggle(source, id, btn.dataset.enabled !== 'true');
192
+ }
193
+ });
194
+ });
195
+ // Bind card click for detail
196
+ container.querySelectorAll('[data-skill-card]').forEach((card) => {
197
+ card.addEventListener('click', () => {
198
+ const source = card.dataset.source;
199
+ const id = card.dataset.id;
200
+ if (!source || !id) {
201
+ return;
202
+ }
203
+ this.showDetail(source, id);
204
+ });
205
+ });
206
+ },
207
+ /**
208
+ * Filter skills by current filter + search query
209
+ */
210
+ _filterSkills(skills) {
211
+ let filtered = skills;
212
+ // Source filter
213
+ if (this.currentFilter !== 'all' && this.currentFilter !== 'installed') {
214
+ filtered = filtered.filter((s) => s.source === this.currentFilter);
186
215
  }
187
- });
188
- });
189
-
190
- // Bind card click for detail
191
- container.querySelectorAll('[data-skill-card]').forEach((card) => {
192
- card.addEventListener('click', () => {
193
- this.showDetail(card.dataset.source, card.dataset.id);
194
- });
195
- });
196
- },
197
-
198
- /**
199
- * Filter skills by current filter + search query
200
- */
201
- _filterSkills(skills) {
202
- let filtered = skills;
203
-
204
- // Source filter
205
- if (this.currentFilter !== 'all' && this.currentFilter !== 'installed') {
206
- filtered = filtered.filter((s) => s.source === this.currentFilter);
207
- }
208
-
209
- // Search filter
210
- if (this.searchQuery) {
211
- const q = this.searchQuery.toLowerCase();
212
- filtered = filtered.filter(
213
- (s) =>
214
- s.name.toLowerCase().includes(q) ||
215
- s.description.toLowerCase().includes(q) ||
216
- s.id.toLowerCase().includes(q)
217
- );
218
- }
219
-
220
- return filtered;
221
- },
222
-
223
- /**
224
- * Render a single skill card
225
- */
226
- _renderCard(skill, isInstalled) {
227
- const sourceColors = {
228
- mama: 'bg-yellow-900/30 text-yellow-400',
229
- cowork: 'bg-blue-900/30 text-blue-400',
230
- external: 'bg-purple-900/30 text-purple-400',
231
- };
232
- const badgeClass = sourceColors[skill.source] || 'bg-gray-700 text-gray-400';
233
- const enabledClass = skill.enabled !== false ? 'border-green-500/30' : 'border-gray-700';
234
-
235
- return `
216
+ // Search filter
217
+ if (this.searchQuery) {
218
+ const q = this.searchQuery.toLowerCase();
219
+ filtered = filtered.filter((s) => s.name.toLowerCase().includes(q) ||
220
+ (s.description || '').toLowerCase().includes(q) ||
221
+ s.id.toLowerCase().includes(q));
222
+ }
223
+ return filtered;
224
+ },
225
+ /**
226
+ * Render a single skill card
227
+ */
228
+ _renderCard(skill, isInstalled) {
229
+ const sourceColors = {
230
+ mama: 'bg-yellow-900/30 text-yellow-400',
231
+ cowork: 'bg-blue-900/30 text-blue-400',
232
+ external: 'bg-purple-900/30 text-purple-400',
233
+ };
234
+ const badgeClass = sourceColors[skill.source] || 'bg-gray-700 text-gray-400';
235
+ const enabledClass = skill.enabled !== false ? 'border-green-500/30' : 'border-gray-700';
236
+ return `
236
237
  <div class="bg-gray-800 rounded-lg border ${enabledClass} p-3 cursor-pointer
237
238
  hover:border-yellow-500/50 transition-colors"
238
239
  data-skill-card data-id="${this._escapeHtml(skill.id)}" data-source="${this._escapeHtml(skill.source)}">
@@ -242,11 +243,10 @@ export const SkillsModule = {
242
243
  ${this._escapeHtml(skill.source)}
243
244
  </span>
244
245
  </div>
245
- <p class="text-xs text-gray-400 line-clamp-2 mb-3">${this._escapeHtml(skill.description)}</p>
246
+ <p class="text-xs text-gray-400 line-clamp-2 mb-3">${this._escapeHtml(skill.description || '')}</p>
246
247
  <div class="flex items-center justify-between">
247
- ${
248
- isInstalled
249
- ? `
248
+ ${isInstalled
249
+ ? `
250
250
  <button data-action="toggle" data-id="${this._escapeHtml(skill.id)}" data-source="${this._escapeHtml(skill.source)}"
251
251
  data-enabled="${skill.enabled !== false}"
252
252
  class="text-xs px-2 py-1 rounded ${skill.enabled !== false ? 'bg-green-900/30 text-green-400' : 'bg-gray-700 text-gray-400'}">
@@ -257,119 +257,115 @@ export const SkillsModule = {
257
257
  Remove
258
258
  </button>
259
259
  `
260
- : `
260
+ : `
261
261
  <span></span>
262
262
  <button data-action="install" data-id="${this._escapeHtml(skill.id)}" data-source="${this._escapeHtml(skill.source)}"
263
263
  class="text-xs px-2 py-1 rounded bg-yellow-900/30 text-yellow-400 hover:bg-yellow-900/50">
264
264
  Install
265
265
  </button>
266
- `
267
- }
266
+ `}
268
267
  </div>
269
268
  </div>
270
269
  `;
271
- },
272
-
273
- /**
274
- * Install a skill
275
- */
276
- async install(source, name) {
277
- try {
278
- const btn = document.querySelector(
279
- `[data-action="install"][data-id="${name}"][data-source="${source}"]`
280
- );
281
- if (btn) {
282
- btn.textContent = 'Installing...';
283
- btn.disabled = true;
284
- }
285
- await API.installSkill(source, name);
286
- await this.loadSkills();
287
- this.render();
288
- } catch (error) {
289
- logger.error('Install failed:', error);
290
- alert(`Failed to install ${name}: ${error.message}`);
291
- this.render();
292
- }
293
- },
294
-
295
- /**
296
- * Install from GitHub URL
297
- */
298
- async installFromUrl(url) {
299
- const btn = document.getElementById('skills-url-install-btn');
300
- const input = document.getElementById('skills-url-input');
301
- try {
302
- if (btn) {
303
- btn.textContent = 'Installing...';
304
- btn.disabled = true;
305
- }
306
- await API.installSkillFromUrl(url);
307
- if (input) {
308
- input.value = '';
309
- }
310
- await this.loadSkills();
311
- this.render();
312
- } catch (error) {
313
- logger.error('URL install failed:', error);
314
- alert(`Failed to install from URL: ${error.message}`);
315
- } finally {
316
- if (btn) {
317
- btn.textContent = 'Install URL';
318
- btn.disabled = false;
319
- }
320
- }
321
- },
322
-
323
- /**
324
- * Uninstall a skill
325
- */
326
- async uninstall(source, name) {
327
- if (!confirm(`Remove skill "${name}"?`)) {
328
- return;
329
- }
330
-
331
- try {
332
- await API.uninstallSkill(name, source);
333
- await this.loadSkills();
334
- this.render();
335
- } catch (error) {
336
- logger.error('Uninstall failed:', error);
337
- }
338
- },
339
-
340
- /**
341
- * Toggle skill enabled/disabled
342
- */
343
- async toggle(source, name, enabled) {
344
- try {
345
- await API.toggleSkill(name, enabled, source);
346
- // Update local state
347
- const skill = this.installed.find((s) => s.id === name && s.source === source);
348
- if (skill) {
349
- skill.enabled = enabled;
350
- }
351
- this.render();
352
- } catch (error) {
353
- logger.error('Toggle failed:', error);
354
- }
355
- },
356
-
357
- /**
358
- * Show skill detail modal
359
- */
360
- async showDetail(source, name) {
361
- const modal = document.getElementById('skill-detail-modal');
362
- const modalContent = document.getElementById('skill-detail-content');
363
- if (!modal || !modalContent) {
364
- return;
365
- }
366
-
367
- modalContent.innerHTML = '<p class="text-gray-400">Loading...</p>';
368
- modal.classList.remove('hidden');
369
-
370
- try {
371
- const { content } = await API.getSkillContent(name, source);
372
- modalContent.innerHTML = `
270
+ },
271
+ /**
272
+ * Install a skill
273
+ */
274
+ async install(source, name) {
275
+ try {
276
+ const btn = document.querySelector(`[data-action="install"][data-id="${CSS.escape(name)}"][data-source="${CSS.escape(source)}"]`);
277
+ if (btn) {
278
+ btn.textContent = 'Installing...';
279
+ btn.disabled = true;
280
+ }
281
+ await API.installSkill(source, name);
282
+ await this.loadSkills();
283
+ this.render();
284
+ }
285
+ catch (error) {
286
+ const message = error instanceof Error ? error.message : String(error);
287
+ logger.error('Install failed:', message);
288
+ alert(`Failed to install ${name}: ${message}`);
289
+ this.render();
290
+ }
291
+ },
292
+ /**
293
+ * Install from GitHub URL
294
+ */
295
+ async installFromUrl(url) {
296
+ const btn = getElementByIdOrNull('skills-url-install-btn');
297
+ const input = getElementByIdOrNull('skills-url-input');
298
+ try {
299
+ if (btn) {
300
+ btn.textContent = 'Installing...';
301
+ btn.disabled = true;
302
+ }
303
+ await API.installSkillFromUrl(url);
304
+ if (input) {
305
+ input.value = '';
306
+ }
307
+ await this.loadSkills();
308
+ this.render();
309
+ }
310
+ catch (error) {
311
+ const message = error instanceof Error ? error.message : String(error);
312
+ logger.error('URL install failed:', message);
313
+ alert(`Failed to install from URL: ${message}`);
314
+ }
315
+ finally {
316
+ if (btn) {
317
+ btn.textContent = 'Install URL';
318
+ btn.disabled = false;
319
+ }
320
+ }
321
+ },
322
+ /**
323
+ * Uninstall a skill
324
+ */
325
+ async uninstall(source, name) {
326
+ if (!confirm(`Remove skill "${name}"?`)) {
327
+ return;
328
+ }
329
+ try {
330
+ await API.uninstallSkill(name, source);
331
+ await this.loadSkills();
332
+ this.render();
333
+ }
334
+ catch (error) {
335
+ logger.error('Uninstall failed:', error instanceof Error ? error.message : String(error));
336
+ }
337
+ },
338
+ /**
339
+ * Toggle skill enabled/disabled
340
+ */
341
+ async toggle(source, name, enabled) {
342
+ try {
343
+ await API.toggleSkill(name, enabled, source);
344
+ // Update local state
345
+ const skill = this.installed.find((s) => s.id === name && s.source === source);
346
+ if (skill) {
347
+ skill.enabled = enabled;
348
+ }
349
+ this.render();
350
+ }
351
+ catch (error) {
352
+ logger.error('Toggle failed:', error instanceof Error ? error.message : String(error));
353
+ }
354
+ },
355
+ /**
356
+ * Show skill detail modal
357
+ */
358
+ async showDetail(source, name) {
359
+ const modal = getElementByIdOrNull('skill-detail-modal');
360
+ const modalContent = getElementByIdOrNull('skill-detail-content');
361
+ if (!modal || !modalContent) {
362
+ return;
363
+ }
364
+ modalContent.innerHTML = '<p class="text-gray-400">Loading...</p>';
365
+ modal.classList.remove('hidden');
366
+ try {
367
+ const { content } = await API.getSkillContent(name, source);
368
+ modalContent.innerHTML = `
373
369
  <div class="flex items-center justify-between mb-4">
374
370
  <h2 class="text-lg font-bold text-white">${this._escapeHtml(name)}</h2>
375
371
  <span class="text-xs px-2 py-1 rounded bg-gray-700 text-gray-400">${this._escapeHtml(source)}</span>
@@ -378,60 +374,45 @@ export const SkillsModule = {
378
374
  ${this._renderMarkdown(content)}
379
375
  </div>
380
376
  `;
381
- } catch (error) {
382
- const message = error instanceof Error ? error.message : String(error);
383
- modalContent.innerHTML = `<p class="text-red-400">Failed to load: ${this._escapeHtml(message)}</p>`;
384
- }
385
- },
386
-
387
- /**
388
- * Close detail modal
389
- */
390
- closeDetail() {
391
- const modal = document.getElementById('skill-detail-modal');
392
- if (modal) {
393
- modal.classList.add('hidden');
394
- }
395
- },
396
-
397
- /**
398
- * Escape HTML special characters
399
- */
400
- _escapeHtml(unsafe) {
401
- if (!unsafe) {
402
- return '';
403
- }
404
- return unsafe
405
- .toString()
406
- .replace(/&/g, '&amp;')
407
- .replace(/</g, '&lt;')
408
- .replace(/>/g, '&gt;')
409
- .replace(/"/g, '&quot;')
410
- .replace(/'/g, '&#039;');
411
- },
412
-
413
- /**
414
- * Render markdown to HTML using marked.js, with sanitization
415
- */
416
- _renderMarkdown(md) {
417
- if (!md) {
418
- return '';
419
- }
420
- // Remove frontmatter
421
- md = md.replace(/^---\n[\s\S]*?\n---\n/, '');
422
-
423
- if (typeof marked !== 'undefined' && marked.parse) {
424
- return marked.parse(md, {
425
- mangle: false,
426
- headerIds: false,
427
- // Sanitize any HTML in the markdown content
428
- sanitize: true,
429
- });
430
- }
431
-
432
- // Fallback if marked.js is not available (should not happen in viewer)
433
- const div = document.createElement('div');
434
- div.textContent = md;
435
- return div.innerHTML;
436
- },
377
+ }
378
+ catch (error) {
379
+ const message = error instanceof Error ? error.message : String(error);
380
+ modalContent.innerHTML = `<p class="text-red-400">Failed to load: ${this._escapeHtml(message)}</p>`;
381
+ }
382
+ },
383
+ /**
384
+ * Close detail modal
385
+ */
386
+ closeDetail() {
387
+ const modal = getElementByIdOrNull('skill-detail-modal');
388
+ if (modal) {
389
+ modal.classList.add('hidden');
390
+ }
391
+ },
392
+ /**
393
+ * Escape HTML special characters
394
+ */
395
+ _escapeHtml(unsafe) {
396
+ if (!unsafe) {
397
+ return '';
398
+ }
399
+ return unsafe
400
+ .toString()
401
+ .replace(/&/g, '&amp;')
402
+ .replace(/</g, '&lt;')
403
+ .replace(/>/g, '&gt;')
404
+ .replace(/"/g, '&quot;')
405
+ .replace(/'/g, '&#039;');
406
+ },
407
+ /**
408
+ * Render markdown to HTML using marked.js, with sanitization
409
+ */
410
+ _renderMarkdown(md) {
411
+ if (!md) {
412
+ return '';
413
+ }
414
+ // Remove frontmatter
415
+ md = md.replace(/^---\n[\s\S]*?\n---\n/, '');
416
+ return renderSafeMarkdown(md);
417
+ },
437
418
  };