@cremini/skillpack 1.2.7 → 1.2.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cremini/skillpack",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "description": "Pack AI Skills into Local Agents",
5
5
  "type": "module",
6
6
  "repository": {
@@ -28,6 +28,7 @@
28
28
  "build": "tsup",
29
29
  "dev": "tsup --watch",
30
30
  "check": "tsc --noEmit",
31
+ "test": "node --import tsx --test tests/*.test.ts",
31
32
  "format": "prettier --write .",
32
33
  "prepare": "npm run build",
33
34
  "create": "tsup && node dist/cli.js create",
@@ -43,11 +44,12 @@
43
44
  "author": "CreminiAI",
44
45
  "license": "MIT",
45
46
  "dependencies": {
46
- "@sinclair/typebox": "^0.34.41",
47
47
  "@mariozechner/pi-coding-agent": "^0.57.1",
48
+ "@sinclair/typebox": "^0.34.41",
48
49
  "@slack/bolt": "^4.6.0",
49
50
  "ajv": "^8.17.1",
50
51
  "archiver": "^7.0.1",
52
+ "better-sqlite3": "^11.10.0",
51
53
  "chalk": "^5.6.2",
52
54
  "commander": "^14.0.3",
53
55
  "express": "^5.1.0",
@@ -58,6 +60,7 @@
58
60
  },
59
61
  "devDependencies": {
60
62
  "@types/archiver": "^7.0.0",
63
+ "@types/better-sqlite3": "^7.6.13",
61
64
  "@types/express": "^5.0.0",
62
65
  "@types/inquirer": "^9.0.9",
63
66
  "@types/node": "^25.5.0",
@@ -66,6 +69,7 @@
66
69
  "@types/ws": "^8.18.0",
67
70
  "prettier": "^3.8.1",
68
71
  "tsup": "^8.5.1",
72
+ "tsx": "^4.21.0",
69
73
  "typescript": "^5.9.3"
70
74
  }
71
75
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { state } from "./config.js";
7
7
  import { saveConfigData, restartRuntime } from "./api.js";
8
+ import { refreshWebSocketConnectionPreference } from "./chat.js";
8
9
 
9
10
  // --- DOM Elements ---
10
11
  let dialog;
@@ -245,6 +246,7 @@ async function handleSave() {
245
246
  populateForm();
246
247
  state.restartRequired = !!res.requiresRestart;
247
248
  updateApiKeyButton();
249
+ refreshWebSocketConnectionPreference();
248
250
 
249
251
  if (res.requiresRestart) {
250
252
  setStatus("Settings saved. Restart service to apply changes.", "warning");
@@ -301,6 +303,7 @@ async function handleOAuthLogout() {
301
303
  updateOAuthUI(false);
302
304
  state.config.oauthConnected = false;
303
305
  updateApiKeyButton();
306
+ refreshWebSocketConnectionPreference();
304
307
  setStatus("Logged out successfully", "success");
305
308
  } catch (err) {
306
309
  setStatus("Logout failed: " + err.message, "error");
@@ -314,6 +317,7 @@ async function checkOAuthStatus() {
314
317
  updateOAuthUI(connected);
315
318
  state.config.oauthConnected = connected;
316
319
  updateApiKeyButton();
320
+ refreshWebSocketConnectionPreference();
317
321
  } catch (err) {
318
322
  console.error("Failed to check OAuth status:", err);
319
323
  }
@@ -329,6 +333,7 @@ function pollOAuthStatus() {
329
333
  state.config.oauthConnected = true;
330
334
  state.restartRequired = true;
331
335
  updateApiKeyButton();
336
+ refreshWebSocketConnectionPreference();
332
337
  setStatus("Connected successfully!", "success");
333
338
  updateRestartButton(true);
334
339
  }
package/web/js/api.js CHANGED
@@ -23,3 +23,33 @@ export async function restartRuntime() {
23
23
  }
24
24
  return payload;
25
25
  }
26
+
27
+ export async function listConversations() {
28
+ const res = await fetch(state.API_BASE + "/api/conversations");
29
+ if (!res.ok) {
30
+ throw new Error("Load Conversations Failed");
31
+ }
32
+ return await res.json();
33
+ }
34
+
35
+ export async function createConversation() {
36
+ const res = await fetch(state.API_BASE + "/api/conversations", {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ });
40
+ if (!res.ok) {
41
+ throw new Error("Create Conversation Failed");
42
+ }
43
+ return await res.json();
44
+ }
45
+
46
+ export async function getConversationMessages(channelId, limit = 200) {
47
+ const params = new URLSearchParams({ limit: String(limit) });
48
+ const res = await fetch(
49
+ state.API_BASE + `/api/conversations/${encodeURIComponent(channelId)}/messages?${params.toString()}`,
50
+ );
51
+ if (!res.ok) {
52
+ throw new Error("Load Conversation Messages Failed");
53
+ }
54
+ return await res.json();
55
+ }
package/web/js/chat.js CHANGED
@@ -1,10 +1,28 @@
1
1
  import { state } from "./config.js";
2
+ import {
3
+ createConversation,
4
+ getConversationMessages,
5
+ listConversations,
6
+ } from "./api.js";
2
7
 
3
8
  export const chatHistory = [];
9
+ const DEFAULT_WEB_CHANNEL_ID = "web";
4
10
  let ws = null;
11
+ let wsConnectPromise = null;
5
12
  let currentAssistantMsg = null;
13
+ let currentChannelId = DEFAULT_WEB_CHANNEL_ID;
14
+ const pendingSendFileCalls = new Map();
15
+ const anonymousSendFileCalls = [];
16
+ let reconnectTimer = null;
17
+ let shouldMaintainWs = false;
18
+
19
+ function hasConfiguredWebAuth() {
20
+ return Boolean(
21
+ state.config && (state.config.hasApiKey || state.config.oauthConnected)
22
+ );
23
+ }
6
24
 
7
- export function initChat() {
25
+ export async function initChat() {
8
26
  // Send button
9
27
  document.getElementById("send-btn").addEventListener("click", sendMessage);
10
28
 
@@ -39,6 +57,12 @@ export function initChat() {
39
57
  }
40
58
  });
41
59
  }
60
+
61
+ await initializeConversation();
62
+ shouldMaintainWs = hasConfiguredWebAuth();
63
+ if (shouldMaintainWs) {
64
+ void ensureBackgroundWsConnection();
65
+ }
42
66
  }
43
67
 
44
68
  export function showWelcome(config) {
@@ -184,30 +208,90 @@ function renderEmbeddedMarkdownBlocks(html) {
184
208
  return template.innerHTML;
185
209
  }
186
210
 
187
- async function getOrCreateWs() {
211
+ function clearReconnectTimer() {
212
+ if (reconnectTimer !== null) {
213
+ window.clearTimeout(reconnectTimer);
214
+ reconnectTimer = null;
215
+ }
216
+ }
217
+
218
+ function scheduleReconnect() {
219
+ if (!shouldMaintainWs || !hasConfiguredWebAuth() || reconnectTimer !== null) {
220
+ return;
221
+ }
222
+
223
+ reconnectTimer = window.setTimeout(() => {
224
+ reconnectTimer = null;
225
+ void ensureBackgroundWsConnection();
226
+ }, 1000);
227
+ }
228
+
229
+ async function ensureBackgroundWsConnection() {
230
+ try {
231
+ await getOrCreateWs({ background: true });
232
+ } catch (err) {
233
+ console.warn("Background WebSocket connection failed:", err);
234
+ }
235
+ }
236
+
237
+ export function refreshWebSocketConnectionPreference() {
238
+ shouldMaintainWs = hasConfiguredWebAuth();
239
+
240
+ if (shouldMaintainWs) {
241
+ void ensureBackgroundWsConnection();
242
+ return;
243
+ }
244
+
245
+ clearReconnectTimer();
246
+ if (ws && ws.readyState === WebSocket.OPEN) {
247
+ ws.close();
248
+ }
249
+ }
250
+
251
+ async function getOrCreateWs(options = {}) {
252
+ const { background = false } = options;
188
253
  if (ws && ws.readyState === WebSocket.OPEN) {
189
254
  return ws;
190
255
  }
191
256
 
192
- return new Promise((resolve, reject) => {
257
+ if (wsConnectPromise) {
258
+ return wsConnectPromise;
259
+ }
260
+
261
+ clearReconnectTimer();
262
+
263
+ wsConnectPromise = new Promise((resolve, reject) => {
193
264
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
194
265
  const provider = state.config && state.config.provider ? state.config.provider : "openai";
266
+ const params = new URLSearchParams({
267
+ provider,
268
+ channelId: currentChannelId || DEFAULT_WEB_CHANNEL_ID,
269
+ });
270
+ const wsUrl = `${protocol}//${window.location.host}${state.API_BASE}/api/chat?${params.toString()}`;
195
271
 
196
- const wsUrl = `${protocol}//${window.location.host}${state.API_BASE}/api/chat?provider=${provider}`;
197
-
198
- ws = new WebSocket(wsUrl);
272
+ const socket = new WebSocket(wsUrl);
273
+ ws = socket;
199
274
 
200
- ws.onopen = () => resolve(ws);
201
- ws.onerror = (err) => {
275
+ socket.onopen = () => {
276
+ shouldMaintainWs = true;
277
+ wsConnectPromise = null;
278
+ resolve(socket);
279
+ };
280
+ socket.onerror = (err) => {
202
281
  console.error(err);
282
+ wsConnectPromise = null;
203
283
  reject(new Error("WebSocket connection failed"));
204
284
  };
205
285
 
206
- ws.onmessage = (event) => {
286
+ socket.onmessage = (event) => {
207
287
  try {
208
288
  const parsed = JSON.parse(event.data);
209
289
  if (parsed.error) {
210
- handleError(parsed.error);
290
+ if (background && !currentAssistantMsg) {
291
+ console.warn("Background WebSocket message error:", parsed.error);
292
+ } else {
293
+ handleError(parsed.error);
294
+ }
211
295
  } else if (parsed.done) {
212
296
  handleDone();
213
297
  } else if (parsed.type) {
@@ -218,11 +302,58 @@ async function getOrCreateWs() {
218
302
  }
219
303
  };
220
304
 
221
- ws.onclose = () => {
222
- ws = null;
305
+ socket.onclose = () => {
306
+ if (ws === socket) {
307
+ ws = null;
308
+ }
309
+ wsConnectPromise = null;
223
310
  enableInput();
311
+ scheduleReconnect();
224
312
  };
225
313
  });
314
+
315
+ return wsConnectPromise;
316
+ }
317
+
318
+ async function initializeConversation() {
319
+ const channelId = await ensureConversation();
320
+ currentChannelId = channelId;
321
+ await loadConversationHistory(channelId);
322
+ }
323
+
324
+ async function ensureConversation() {
325
+ const conversations = await listConversations();
326
+ const existing = conversations.find(
327
+ (conversation) => conversation.channelId === DEFAULT_WEB_CHANNEL_ID,
328
+ );
329
+ if (existing) {
330
+ return existing.channelId;
331
+ }
332
+
333
+ const created = await createConversation();
334
+ return created.channelId || DEFAULT_WEB_CHANNEL_ID;
335
+ }
336
+
337
+ async function loadConversationHistory(channelId) {
338
+ const messages = await getConversationMessages(channelId, 200);
339
+ const messagesEl = document.getElementById("messages");
340
+ const chatArea = document.getElementById("chat-area");
341
+
342
+ messagesEl.innerHTML = "";
343
+ chatHistory.length = 0;
344
+
345
+ for (const message of messages) {
346
+ appendMessage(message.role, message.text, message.toolCalls);
347
+ chatHistory.push({ role: message.role, content: message.text });
348
+ }
349
+
350
+ if (messages.length > 0) {
351
+ chatArea.classList.remove("mode-welcome");
352
+ chatArea.classList.add("mode-chat");
353
+ } else {
354
+ chatArea.classList.remove("mode-chat");
355
+ chatArea.classList.add("mode-welcome");
356
+ }
226
357
  }
227
358
 
228
359
  function handleError(errorMsg) {
@@ -271,7 +402,28 @@ function hideLoadingIndicator() {
271
402
  }
272
403
  }
273
404
 
405
+ function ensureAssistantMessageForEvent(event) {
406
+ if (currentAssistantMsg) return;
407
+
408
+ if (
409
+ !["agent_start", "message_start", "text_delta", "thinking_delta", "tool_start"].includes(
410
+ event.type
411
+ )
412
+ ) {
413
+ return;
414
+ }
415
+
416
+ const chatArea = document.getElementById("chat-area");
417
+ if (chatArea.classList.contains("mode-welcome")) {
418
+ chatArea.classList.remove("mode-welcome");
419
+ chatArea.classList.add("mode-chat");
420
+ }
421
+
422
+ currentAssistantMsg = appendMessage("assistant", "");
423
+ }
424
+
274
425
  function handleAgentEvent(event) {
426
+ ensureAssistantMessageForEvent(event);
275
427
  if (!currentAssistantMsg) return;
276
428
 
277
429
  if (
@@ -317,6 +469,13 @@ function handleAgentEvent(event) {
317
469
  break;
318
470
 
319
471
  case "tool_start":
472
+ if (event.toolName === "send_file") {
473
+ queueSendFileToolCall(event.toolCallId, event.toolInput);
474
+ scrollToBottom();
475
+ showLoadingIndicator();
476
+ break;
477
+ }
478
+
320
479
  const toolCard = document.createElement("div");
321
480
  toolCard.className = "tool-card running collapsed";
322
481
  const safeInput =
@@ -360,13 +519,27 @@ function handleAgentEvent(event) {
360
519
  }
361
520
 
362
521
  toolCard.dataset.toolName = event.toolName;
522
+ toolCard.dataset.toolCallId = event.toolCallId || "";
363
523
  scrollToBottom();
364
524
  showLoadingIndicator();
365
525
  break;
366
526
 
367
527
  case "tool_end":
528
+ if (event.toolName === "send_file") {
529
+ const file = dequeueSendFileToolCall(event.toolCallId);
530
+ if (!event.isError && file) {
531
+ appendFileCard(currentAssistantMsg, file.filePath, file.caption);
532
+ }
533
+ scrollToBottom();
534
+ showLoadingIndicator();
535
+ break;
536
+ }
537
+
368
538
  const cards = Array.from(currentAssistantMsg.querySelectorAll(".tool-card.running"));
369
- const card = cards.reverse().find((c) => c.dataset.toolName === event.toolName);
539
+ const card = cards.reverse().find((c) =>
540
+ c.dataset.toolName === event.toolName &&
541
+ (event.toolCallId ? c.dataset.toolCallId === event.toolCallId : true)
542
+ );
370
543
  if (card) {
371
544
  card.classList.remove("running");
372
545
  card.classList.add(event.isError ? "error" : "success");
@@ -405,6 +578,100 @@ function handleAgentEvent(event) {
405
578
  }
406
579
  }
407
580
 
581
+ function queueSendFileToolCall(toolCallId, toolInput) {
582
+ const file = extractSendFileToolInput(toolInput);
583
+ if (!file) return;
584
+
585
+ if (toolCallId) {
586
+ pendingSendFileCalls.set(toolCallId, file);
587
+ return;
588
+ }
589
+
590
+ anonymousSendFileCalls.push(file);
591
+ }
592
+
593
+ function dequeueSendFileToolCall(toolCallId) {
594
+ if (toolCallId) {
595
+ const file = pendingSendFileCalls.get(toolCallId) || null;
596
+ pendingSendFileCalls.delete(toolCallId);
597
+ return file;
598
+ }
599
+
600
+ return anonymousSendFileCalls.shift() || null;
601
+ }
602
+
603
+ function extractSendFileToolInput(toolInput) {
604
+ if (!toolInput || typeof toolInput !== "object") {
605
+ return null;
606
+ }
607
+
608
+ const filePath =
609
+ typeof toolInput.filePath === "string" ? toolInput.filePath.trim() : "";
610
+ const caption =
611
+ typeof toolInput.caption === "string" ? toolInput.caption.trim() : "";
612
+
613
+ if (!filePath) {
614
+ return null;
615
+ }
616
+
617
+ return {
618
+ filePath,
619
+ caption,
620
+ };
621
+ }
622
+
623
+ function getVisibleSendFileToolCalls(toolCalls) {
624
+ if (!Array.isArray(toolCalls)) {
625
+ return [];
626
+ }
627
+
628
+ return toolCalls.filter((toolCall) =>
629
+ toolCall &&
630
+ toolCall.name === "send_file" &&
631
+ !toolCall.isError &&
632
+ toolCall.arguments &&
633
+ typeof toolCall.arguments.filePath === "string" &&
634
+ toolCall.arguments.filePath
635
+ );
636
+ }
637
+
638
+ function appendFileCard(container, filePath, caption) {
639
+ const fileName = basename(filePath);
640
+ const title = caption || fileName;
641
+ const card = document.createElement("a");
642
+ card.className = "file-card";
643
+ card.href = buildFileDownloadUrl(filePath);
644
+ card.title = filePath;
645
+ card.setAttribute("download", fileName);
646
+ card.setAttribute("target", "_blank");
647
+ card.setAttribute("rel", "noopener noreferrer");
648
+ card.innerHTML = `
649
+ <div class="file-card-icon">FILE</div>
650
+ <div class="file-card-copy">
651
+ <div class="file-card-title">${escapeHtml(title)}</div>
652
+ <div class="file-card-meta">${escapeHtml(fileName)}</div>
653
+ </div>
654
+ <div class="file-card-action">Download</div>
655
+ `;
656
+
657
+ const indicator = container.querySelector(".loading-indicator");
658
+ if (indicator) {
659
+ container.insertBefore(card, indicator);
660
+ } else {
661
+ container.appendChild(card);
662
+ }
663
+ }
664
+
665
+ function basename(filePath) {
666
+ const normalized = String(filePath).replace(/\\/g, "/");
667
+ const parts = normalized.split("/").filter(Boolean);
668
+ return parts[parts.length - 1] || String(filePath);
669
+ }
670
+
671
+ function buildFileDownloadUrl(filePath) {
672
+ return `${state.API_BASE}/api/files?path=${encodeURIComponent(filePath)}`;
673
+ }
674
+
408
675
  function getOrCreateThinkingBlock() {
409
676
  const children = Array.from(currentAssistantMsg.children).filter(
410
677
  (c) => !c.classList.contains("loading-indicator")
@@ -465,10 +732,12 @@ function getOrCreateTextBlock() {
465
732
  function enableInput() {
466
733
  const sendBtn = document.getElementById("send-btn");
467
734
  if (sendBtn) sendBtn.disabled = false;
735
+ pendingSendFileCalls.clear();
736
+ anonymousSendFileCalls.length = 0;
468
737
  currentAssistantMsg = null;
469
738
  }
470
739
 
471
- function appendMessage(role, text) {
740
+ function appendMessage(role, text, toolCalls = []) {
472
741
  const messages = document.getElementById("messages");
473
742
  const div = document.createElement("div");
474
743
  div.className = "message " + role;
@@ -483,6 +752,13 @@ function appendMessage(role, text) {
483
752
  div.appendChild(tb);
484
753
  }
485
754
 
755
+ if (role === "assistant") {
756
+ const sendFileCalls = getVisibleSendFileToolCalls(toolCalls);
757
+ sendFileCalls.forEach((toolCall) => {
758
+ appendFileCard(div, toolCall.arguments.filePath, toolCall.arguments.caption);
759
+ });
760
+ }
761
+
486
762
  messages.appendChild(div);
487
763
  scrollToBottom();
488
764
  return div;
package/web/js/main.js CHANGED
@@ -39,13 +39,16 @@ async function init() {
39
39
  } catch (err) {
40
40
  console.error("Initialization Failed:", err);
41
41
  }
42
-
43
- // Initialize all dialog modules
42
+
44
43
  initApiKeyDialog();
45
44
  initChatAppsDialog();
46
- initChat();
47
45
 
48
- // Update action button states based on config
46
+ try {
47
+ await initChat();
48
+ } catch (err) {
49
+ console.error("Chat initialization failed:", err);
50
+ }
51
+
49
52
  updateApiKeyButton();
50
53
  updateChatAppsButton();
51
54
  }
package/web/styles.css CHANGED
@@ -646,6 +646,71 @@ body {
646
646
  color: var(--text-primary);
647
647
  }
648
648
 
649
+ .file-card {
650
+ display: flex;
651
+ align-items: center;
652
+ gap: 14px;
653
+ padding: 14px 16px;
654
+ margin: 0;
655
+ border: 1px solid var(--border-color);
656
+ border-radius: var(--radius);
657
+ background: linear-gradient(135deg, #f9fafb 0%, #f1f4f7 100%);
658
+ color: inherit;
659
+ text-decoration: none;
660
+ transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
661
+ }
662
+
663
+ .file-card:hover {
664
+ transform: translateY(-1px);
665
+ box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08);
666
+ border-color: rgba(0, 122, 255, 0.24);
667
+ }
668
+
669
+ .file-card-icon {
670
+ flex: 0 0 auto;
671
+ min-width: 46px;
672
+ height: 46px;
673
+ padding: 0 10px;
674
+ border-radius: 12px;
675
+ background: #0f172a;
676
+ color: #ffffff;
677
+ display: inline-flex;
678
+ align-items: center;
679
+ justify-content: center;
680
+ font-size: 11px;
681
+ font-weight: 700;
682
+ letter-spacing: 0.08em;
683
+ }
684
+
685
+ .file-card-copy {
686
+ min-width: 0;
687
+ flex: 1 1 auto;
688
+ }
689
+
690
+ .file-card-title {
691
+ font-weight: 600;
692
+ color: var(--text-primary);
693
+ line-height: 1.35;
694
+ }
695
+
696
+ .file-card-meta {
697
+ margin-top: 4px;
698
+ color: var(--text-secondary);
699
+ font-size: 12px;
700
+ word-break: break-all;
701
+ }
702
+
703
+ .file-card-action {
704
+ flex: 0 0 auto;
705
+ padding: 8px 12px;
706
+ border-radius: 999px;
707
+ background: #ffffff;
708
+ border: 1px solid rgba(15, 23, 42, 0.08);
709
+ color: var(--accent);
710
+ font-size: 12px;
711
+ font-weight: 600;
712
+ }
713
+
649
714
  /* Markdown Specifics */
650
715
  .markdown-body pre {
651
716
  background: #f8f9fa;