@cremini/skillpack 1.0.2

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.
@@ -0,0 +1,582 @@
1
+ const API_BASE = "";
2
+ let chatHistory = [];
3
+
4
+ // Initialize
5
+ async function init() {
6
+ await loadConfig();
7
+ setupEventListeners();
8
+ }
9
+
10
+ async function loadConfig() {
11
+ try {
12
+ const res = await fetch(API_BASE + "/api/config");
13
+ const config = await res.json();
14
+
15
+ document.getElementById("pack-name").textContent = config.name;
16
+ document.getElementById("pack-desc").textContent = config.description;
17
+ document.title = config.name;
18
+
19
+ // Skills
20
+ const skillsList = document.getElementById("skills-list");
21
+ skillsList.innerHTML = config.skills
22
+ .map(
23
+ (s) =>
24
+ `<li><div class="skill-name">${s.name}</div><div class="skill-desc">${s.description}</div></li>`,
25
+ )
26
+ .join("");
27
+
28
+ // Pre-fill when there is exactly one prompt
29
+ if (config.prompts && config.prompts.length === 1) {
30
+ const input = document.getElementById("user-input");
31
+ input.value = config.prompts[0];
32
+ input.style.height = "auto";
33
+ input.style.height = Math.min(input.scrollHeight, 120) + "px";
34
+ }
35
+
36
+ // API key status and provider
37
+ const keyStatus = document.getElementById("key-status");
38
+ if (config.hasApiKey) {
39
+ keyStatus.textContent = "API key configured";
40
+ keyStatus.className = "status-text success";
41
+ }
42
+
43
+ const providerSelect = document.getElementById("provider-select");
44
+ if (providerSelect && config.provider) {
45
+ providerSelect.value = config.provider;
46
+ }
47
+
48
+ const updatePlaceholder = () => {
49
+ const p = providerSelect.value;
50
+ const input = document.getElementById("api-key-input");
51
+ if (p === "openai") input.placeholder = "sk-proj-...";
52
+ else if (p === "anthropic") input.placeholder = "sk-ant-api03-...";
53
+ else input.placeholder = "sk-...";
54
+ };
55
+
56
+ providerSelect.addEventListener("change", updatePlaceholder);
57
+ updatePlaceholder();
58
+
59
+ // Show welcome view
60
+ showWelcome(config);
61
+ } catch (err) {
62
+ console.error("Failed to load config:", err);
63
+ }
64
+ }
65
+
66
+ function showWelcome(config) {
67
+ const welcomeContent = document.getElementById("welcome-content");
68
+
69
+ let promptsHtml = "";
70
+ if (config.prompts && config.prompts.length > 1) {
71
+ promptsHtml = `
72
+ <div class="prompt-cards">
73
+ ${config.prompts
74
+ .map(
75
+ (u, i) => `
76
+ <div class="prompt-card" data-index="${i}" title="${u}">
77
+ ${u.length > 60 ? u.substring(0, 60) + "..." : u}
78
+ </div>
79
+ `,
80
+ )
81
+ .join("")}
82
+ </div>
83
+ `;
84
+ }
85
+
86
+ if (welcomeContent) {
87
+ welcomeContent.innerHTML = `
88
+ <div class="welcome-message">
89
+ <h2>Turn Skills into a Standalone App with UI</h2>
90
+ <p>One command to orchestrate skills into a standalone app users can download and use on their computer</p>
91
+ ${promptsHtml}
92
+ </div>
93
+ `;
94
+ }
95
+ }
96
+
97
+ function setupEventListeners() {
98
+ // Send button
99
+ document.getElementById("send-btn").addEventListener("click", sendMessage);
100
+
101
+ // Send on Enter
102
+ document.getElementById("user-input").addEventListener("keydown", (e) => {
103
+ if (e.key === "Enter" && !e.shiftKey) {
104
+ e.preventDefault();
105
+ sendMessage();
106
+ }
107
+ });
108
+
109
+ // Auto-resize the input box
110
+ document.getElementById("user-input").addEventListener("input", (e) => {
111
+ e.target.style.height = "auto";
112
+ e.target.style.height = Math.min(e.target.scrollHeight, 120) + "px";
113
+ });
114
+
115
+ // Save API key
116
+ document.getElementById("save-key-btn").addEventListener("click", saveApiKey);
117
+
118
+ // Prompt click
119
+ const welcomeContent = document.getElementById("welcome-content");
120
+ if (welcomeContent) {
121
+ welcomeContent.addEventListener("click", (e) => {
122
+ const item = e.target.closest(".prompt-card");
123
+ if (!item) return;
124
+ const index = parseInt(item.dataset.index);
125
+
126
+ // Get the full prompt text
127
+ fetch(API_BASE + "/api/config")
128
+ .then((r) => r.json())
129
+ .then((config) => {
130
+ if (config.prompts[index]) {
131
+ const input = document.getElementById("user-input");
132
+ input.value = config.prompts[index];
133
+ input.focus();
134
+ input.style.height = "auto";
135
+ input.style.height = Math.min(input.scrollHeight, 120) + "px";
136
+ }
137
+ });
138
+ });
139
+ }
140
+ }
141
+
142
+ async function saveApiKey() {
143
+ const input = document.getElementById("api-key-input");
144
+ const providerSelect = document.getElementById("provider-select");
145
+ const status = document.getElementById("key-status");
146
+ const key = input.value.trim();
147
+ const provider = providerSelect ? providerSelect.value : "openai";
148
+
149
+ if (!key) {
150
+ status.textContent = "Enter an API key";
151
+ status.className = "status-text error";
152
+ return;
153
+ }
154
+
155
+ try {
156
+ const res = await fetch(API_BASE + "/api/config/key", {
157
+ method: "POST",
158
+ headers: { "Content-Type": "application/json" },
159
+ body: JSON.stringify({ key, provider }),
160
+ });
161
+
162
+ if (res.ok) {
163
+ status.textContent = "API key saved";
164
+ status.className = "status-text success";
165
+ input.value = "";
166
+ } else {
167
+ status.textContent = "Save failed";
168
+ status.className = "status-text error";
169
+ }
170
+ } catch (err) {
171
+ status.textContent = "Save failed: " + err.message;
172
+ status.className = "status-text error";
173
+ }
174
+ }
175
+
176
+ let ws = null;
177
+ let currentAssistantMsg = null;
178
+
179
+ function renderMarkdown(mdText, { renderEmbeddedMarkdown = true } = {}) {
180
+ if (typeof marked === "undefined") {
181
+ return escapeHtml(mdText);
182
+ }
183
+
184
+ const html = marked.parse(mdText);
185
+ if (!renderEmbeddedMarkdown) {
186
+ return html;
187
+ }
188
+
189
+ return renderEmbeddedMarkdownBlocks(html);
190
+ }
191
+
192
+ function renderEmbeddedMarkdownBlocks(html) {
193
+ const template = document.createElement("template");
194
+ template.innerHTML = html;
195
+
196
+ const codeBlocks = template.content.querySelectorAll("pre > code");
197
+ codeBlocks.forEach((codeEl) => {
198
+ const languageClass = Array.from(codeEl.classList).find((className) =>
199
+ className.startsWith("language-"),
200
+ );
201
+ const language = languageClass
202
+ ? languageClass.slice("language-".length)
203
+ : "";
204
+
205
+ if (!/^(markdown|md)$/i.test(language)) {
206
+ return;
207
+ }
208
+
209
+ const preview = document.createElement("div");
210
+ preview.className = "embedded-markdown-preview markdown-body";
211
+ preview.innerHTML = renderMarkdown(codeEl.textContent || "", {
212
+ renderEmbeddedMarkdown: false,
213
+ });
214
+
215
+ const pre = codeEl.parentElement;
216
+ if (pre) {
217
+ pre.replaceWith(preview);
218
+ }
219
+ });
220
+
221
+ return template.innerHTML;
222
+ }
223
+
224
+ async function getOrCreateWs() {
225
+ if (ws && ws.readyState === WebSocket.OPEN) {
226
+ return ws;
227
+ }
228
+
229
+ return new Promise((resolve, reject) => {
230
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
231
+ const providerSelect = document.getElementById("provider-select");
232
+ const provider = providerSelect ? providerSelect.value : "openai";
233
+
234
+ // URLSearchParams would be cleaner if more query params are added later
235
+ const wsUrl = `${protocol}//${window.location.host}${API_BASE}/api/chat?provider=${provider}`;
236
+
237
+ ws = new WebSocket(wsUrl);
238
+
239
+ ws.onopen = () => resolve(ws);
240
+ ws.onerror = (err) => {
241
+ console.error(err);
242
+ reject(new Error("WebSocket connection failed"));
243
+ };
244
+
245
+ ws.onmessage = (event) => {
246
+ try {
247
+ const parsed = JSON.parse(event.data);
248
+ if (parsed.error) {
249
+ handleError(parsed.error);
250
+ } else if (parsed.done) {
251
+ handleDone();
252
+ } else if (parsed.type) {
253
+ handleAgentEvent(parsed);
254
+ }
255
+ } catch (e) {
256
+ console.error("Failed to parse message", e);
257
+ }
258
+ };
259
+
260
+ ws.onclose = () => {
261
+ ws = null;
262
+ enableInput();
263
+ };
264
+ });
265
+ }
266
+
267
+ function handleError(errorMsg) {
268
+ if (!currentAssistantMsg) {
269
+ appendMessage("assistant", "Error: " + errorMsg).classList.add("error");
270
+ } else {
271
+ const errDiv = document.createElement("div");
272
+ errDiv.className = "content error-text";
273
+ errDiv.textContent = "Error: " + errorMsg;
274
+ currentAssistantMsg.appendChild(errDiv);
275
+ currentAssistantMsg.classList.add("error");
276
+ }
277
+ enableInput();
278
+ }
279
+
280
+ function handleDone() {
281
+ let fullText = "";
282
+ if (currentAssistantMsg) {
283
+ const blocks = currentAssistantMsg.querySelectorAll(".text-block");
284
+ blocks.forEach((b) => {
285
+ fullText += b.dataset.mdContent + "\n";
286
+ });
287
+ }
288
+ chatHistory.push({ role: "assistant", content: fullText });
289
+ enableInput();
290
+ }
291
+
292
+ function showLoadingIndicator() {
293
+ if (!currentAssistantMsg) return;
294
+ let indicator = currentAssistantMsg.querySelector(".loading-indicator");
295
+ if (!indicator) {
296
+ indicator = document.createElement("div");
297
+ indicator.className = "loading-indicator";
298
+ indicator.innerHTML = `<span></span><span></span><span></span>`;
299
+ currentAssistantMsg.appendChild(indicator);
300
+ }
301
+ indicator.style.display = "flex";
302
+ scrollToBottom();
303
+ }
304
+
305
+ function hideLoadingIndicator() {
306
+ if (!currentAssistantMsg) return;
307
+ const indicator = currentAssistantMsg.querySelector(".loading-indicator");
308
+ if (indicator) {
309
+ indicator.style.display = "none";
310
+ }
311
+ }
312
+
313
+ function handleAgentEvent(event) {
314
+ if (!currentAssistantMsg) return;
315
+
316
+ if (
317
+ ["text_delta", "thinking_delta", "tool_start", "tool_end"].includes(
318
+ event.type,
319
+ )
320
+ ) {
321
+ hideLoadingIndicator();
322
+ }
323
+
324
+ switch (event.type) {
325
+ case "agent_start":
326
+ case "message_start":
327
+ showLoadingIndicator();
328
+ break;
329
+
330
+ case "agent_end":
331
+ case "message_end":
332
+ hideLoadingIndicator();
333
+ break;
334
+
335
+ case "thinking_delta":
336
+ const thinkingBlock = getOrCreateThinkingBlock();
337
+ thinkingBlock.dataset.mdContent += event.delta;
338
+ const contentEl = thinkingBlock.querySelector(".thinking-content");
339
+ if (typeof marked !== "undefined") {
340
+ contentEl.innerHTML = renderMarkdown(thinkingBlock.dataset.mdContent);
341
+ } else {
342
+ contentEl.textContent = thinkingBlock.dataset.mdContent;
343
+ }
344
+ scrollToBottom();
345
+ break;
346
+
347
+ case "text_delta":
348
+ const textBlock = getOrCreateTextBlock();
349
+ textBlock.dataset.mdContent += event.delta;
350
+ if (typeof marked !== "undefined") {
351
+ textBlock.innerHTML = renderMarkdown(textBlock.dataset.mdContent);
352
+ } else {
353
+ textBlock.textContent = textBlock.dataset.mdContent;
354
+ }
355
+ scrollToBottom();
356
+ break;
357
+
358
+ case "tool_start":
359
+ const toolCard = document.createElement("div");
360
+ toolCard.className = "tool-card running collapsed";
361
+ const safeInput =
362
+ typeof event.toolInput === "string"
363
+ ? event.toolInput
364
+ : JSON.stringify(event.toolInput, null, 2);
365
+
366
+ let inputHtml = "";
367
+ if (typeof marked !== "undefined") {
368
+ inputHtml = marked.parse("```json\n" + safeInput + "\n```");
369
+ } else {
370
+ inputHtml = escapeHtml(safeInput);
371
+ }
372
+
373
+ toolCard.innerHTML = `
374
+ <div class="tool-header">
375
+ <span class="tool-chevron">
376
+ <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
377
+ </span>
378
+ <span class="tool-icon">🛠️</span>
379
+ <span class="tool-name">${escapeHtml(event.toolName)}</span>
380
+ <span class="tool-status spinner"></span>
381
+ </div>
382
+ <div class="tool-content">
383
+ <div class="tool-input markdown-body">${inputHtml}</div>
384
+ <div class="tool-result markdown-body" style="display: none;"></div>
385
+ </div>
386
+ `;
387
+
388
+ toolCard.querySelector(".tool-header").addEventListener("click", () => {
389
+ toolCard.classList.toggle("collapsed");
390
+ });
391
+
392
+ // Insert before loading indicator if exists
393
+ const toolIndicator =
394
+ currentAssistantMsg.querySelector(".loading-indicator");
395
+ if (toolIndicator) {
396
+ currentAssistantMsg.insertBefore(toolCard, toolIndicator);
397
+ } else {
398
+ currentAssistantMsg.appendChild(toolCard);
399
+ }
400
+
401
+ toolCard.dataset.toolName = event.toolName;
402
+ scrollToBottom();
403
+
404
+ showLoadingIndicator();
405
+ break;
406
+
407
+ case "tool_end":
408
+ const cards = Array.from(
409
+ currentAssistantMsg.querySelectorAll(".tool-card.running"),
410
+ );
411
+ const card = cards
412
+ .reverse()
413
+ .find((c) => c.dataset.toolName === event.toolName);
414
+ if (card) {
415
+ card.classList.remove("running");
416
+ card.classList.add(event.isError ? "error" : "success");
417
+
418
+ if (event.isError) {
419
+ card.classList.remove("collapsed");
420
+ }
421
+
422
+ const statusEl = card.querySelector(".tool-status");
423
+ statusEl.className = "tool-status";
424
+ statusEl.textContent = event.isError ? "❌" : "✅";
425
+
426
+ const resultEl = card.querySelector(".tool-result");
427
+ resultEl.style.display = "block";
428
+ const safeResult =
429
+ typeof event.result === "string"
430
+ ? event.result
431
+ : JSON.stringify(event.result, null, 2);
432
+
433
+ const mdText =
434
+ event.result &&
435
+ typeof event.result === "string" &&
436
+ (event.result.includes("\n") || event.result.length > 50)
437
+ ? "```bash\n" + safeResult + "\n```"
438
+ : "```json\n" + safeResult + "\n```";
439
+
440
+ if (typeof marked !== "undefined") {
441
+ resultEl.innerHTML = marked.parse(mdText);
442
+ } else {
443
+ resultEl.textContent = safeResult;
444
+ }
445
+ }
446
+ scrollToBottom();
447
+
448
+ showLoadingIndicator();
449
+ break;
450
+ }
451
+ }
452
+
453
+ function getOrCreateThinkingBlock() {
454
+ const children = Array.from(currentAssistantMsg.children).filter(
455
+ (c) => !c.classList.contains("loading-indicator"),
456
+ );
457
+ let lastChild = children[children.length - 1];
458
+
459
+ if (!lastChild || !lastChild.classList.contains("thinking-card")) {
460
+ lastChild = document.createElement("div");
461
+ lastChild.className = "tool-card thinking-card collapsed";
462
+ lastChild.dataset.mdContent = "";
463
+
464
+ lastChild.innerHTML = `
465
+ <div class="tool-header thinking-header">
466
+ <span class="tool-chevron">
467
+ <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
468
+ </span>
469
+ <span class="tool-icon">🧠</span>
470
+ <span class="tool-name" style="color: var(--text-secondary);">Thinking Process</span>
471
+ </div>
472
+ <div class="tool-content thinking-content markdown-body"></div>
473
+ `;
474
+
475
+ lastChild.querySelector(".tool-header").addEventListener("click", () => {
476
+ lastChild.classList.toggle("collapsed");
477
+ });
478
+
479
+ const indicator = currentAssistantMsg.querySelector(".loading-indicator");
480
+ if (indicator) {
481
+ currentAssistantMsg.insertBefore(lastChild, indicator);
482
+ } else {
483
+ currentAssistantMsg.appendChild(lastChild);
484
+ }
485
+ }
486
+ return lastChild;
487
+ }
488
+
489
+ function getOrCreateTextBlock() {
490
+ const children = Array.from(currentAssistantMsg.children).filter(
491
+ (c) => !c.classList.contains("loading-indicator"),
492
+ );
493
+ let lastChild = children[children.length - 1];
494
+
495
+ if (!lastChild || !lastChild.classList.contains("text-block")) {
496
+ lastChild = document.createElement("div");
497
+ lastChild.className = "content text-block markdown-body";
498
+ lastChild.dataset.mdContent = "";
499
+
500
+ const indicator = currentAssistantMsg.querySelector(".loading-indicator");
501
+ if (indicator) {
502
+ currentAssistantMsg.insertBefore(lastChild, indicator);
503
+ } else {
504
+ currentAssistantMsg.appendChild(lastChild);
505
+ }
506
+ }
507
+ return lastChild;
508
+ }
509
+
510
+ function enableInput() {
511
+ const sendBtn = document.getElementById("send-btn");
512
+ if (sendBtn) sendBtn.disabled = false;
513
+ currentAssistantMsg = null;
514
+ }
515
+
516
+ async function sendMessage() {
517
+ const input = document.getElementById("user-input");
518
+ const text = input.value.trim();
519
+ if (!text) return;
520
+
521
+ const chatArea = document.getElementById("chat-area");
522
+ if (chatArea.classList.contains("mode-welcome")) {
523
+ chatArea.classList.remove("mode-welcome");
524
+ chatArea.classList.add("mode-chat");
525
+ }
526
+
527
+ input.value = "";
528
+ input.style.height = "auto";
529
+
530
+ // Add the user message
531
+ appendMessage("user", text);
532
+ chatHistory.push({ role: "user", content: text });
533
+
534
+ // Disable input while the agent is responding
535
+ const sendBtn = document.getElementById("send-btn");
536
+ sendBtn.disabled = true;
537
+
538
+ // Create an assistant message placeholder
539
+ currentAssistantMsg = appendMessage("assistant", "");
540
+ showLoadingIndicator();
541
+
542
+ try {
543
+ const socket = await getOrCreateWs();
544
+ socket.send(JSON.stringify({ text }));
545
+ } catch (err) {
546
+ handleError(err.message);
547
+ }
548
+ }
549
+
550
+ function appendMessage(role, text) {
551
+ const messages = document.getElementById("messages");
552
+ const div = document.createElement("div");
553
+ div.className = "message " + role;
554
+
555
+ if (role === "user") {
556
+ div.innerHTML = '<div class="content">' + escapeHtml(text) + "</div>";
557
+ } else if (text) {
558
+ const tb = document.createElement("div");
559
+ tb.className = "content text-block markdown-body";
560
+ tb.dataset.mdContent = text;
561
+ tb.innerHTML = renderMarkdown(text);
562
+ div.appendChild(tb);
563
+ }
564
+
565
+ messages.appendChild(div);
566
+ scrollToBottom();
567
+ return div;
568
+ }
569
+
570
+ function escapeHtml(text) {
571
+ const div = document.createElement("div");
572
+ div.textContent = text;
573
+ return div.innerHTML;
574
+ }
575
+
576
+ function scrollToBottom() {
577
+ const messages = document.getElementById("messages");
578
+ messages.scrollTop = messages.scrollHeight;
579
+ }
580
+
581
+ // Start the app
582
+ init();
@@ -0,0 +1,72 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <meta name="description" content="Skills Pack Chat" />
8
+ <title>Skills Pack</title>
9
+ <link rel="stylesheet" href="styles.css" />
10
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
12
+ </head>
13
+
14
+ <body>
15
+ <div id="app">
16
+ <aside id="sidebar">
17
+ <div class="sidebar-header">
18
+ <h1 id="pack-name">Skills Pack</h1>
19
+ <p id="pack-desc">Loading...</p>
20
+ </div>
21
+
22
+ <div class="sidebar-section">
23
+ <h3>Skills</h3>
24
+ <ul id="skills-list"></ul>
25
+ </div>
26
+
27
+ <div class="sidebar-section">
28
+ <h3>API KEYS</h3>
29
+ <div class="provider-select-wrapper">
30
+ <select id="provider-select">
31
+ <option value="openai">OpenAI</option>
32
+ <option value="anthropic">Anthropic</option>
33
+ </select>
34
+ </div>
35
+ <div class="api-key-form">
36
+ <input type="password" id="api-key-input" placeholder="sk-..." />
37
+ </div>
38
+ <div class="api-key-footer">
39
+ <p id="key-status" class="status-text"></p>
40
+ <button id="save-key-btn">Save</button>
41
+ </div>
42
+ </div>
43
+ </aside>
44
+
45
+ <main id="chat-area" class="mode-welcome">
46
+ <div id="welcome-view">
47
+ <div id="welcome-content"></div>
48
+ </div>
49
+ <div id="chat-view">
50
+ <div id="messages"></div>
51
+ </div>
52
+ <div id="input-area">
53
+ <div class="input-container">
54
+ <textarea id="user-input" placeholder="Type a message..." rows="1"></textarea>
55
+ <button id="send-btn">
56
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
57
+ <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" />
58
+ </svg>
59
+ </button>
60
+ </div>
61
+ <p class="powered-by">
62
+ Powered by
63
+ <a href="https://skillpack.sh" target="_blank" rel="noopener noreferrer">SkillPack.sh</a>
64
+ </p>
65
+ </div>
66
+ </main>
67
+ </div>
68
+ <script src="marked.min.js"></script>
69
+ <script src="app.js"></script>
70
+ </body>
71
+
72
+ </html>