@cremini/skillpack 1.1.3 → 1.1.4

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 (40) hide show
  1. package/README.md +87 -47
  2. package/dist/cli.js +1602 -339
  3. package/package.json +17 -9
  4. package/templates/start.bat +3 -0
  5. package/templates/start.sh +3 -0
  6. package/runtime/README.md +0 -51
  7. package/runtime/server/dist/adapters/markdown.js +0 -74
  8. package/runtime/server/dist/adapters/markdown.js.map +0 -1
  9. package/runtime/server/dist/adapters/slack.js +0 -369
  10. package/runtime/server/dist/adapters/slack.js.map +0 -1
  11. package/runtime/server/dist/adapters/telegram.js +0 -199
  12. package/runtime/server/dist/adapters/telegram.js.map +0 -1
  13. package/runtime/server/dist/adapters/types.js +0 -2
  14. package/runtime/server/dist/adapters/types.js.map +0 -1
  15. package/runtime/server/dist/adapters/web.js +0 -201
  16. package/runtime/server/dist/adapters/web.js.map +0 -1
  17. package/runtime/server/dist/agent.js +0 -245
  18. package/runtime/server/dist/agent.js.map +0 -1
  19. package/runtime/server/dist/config.js +0 -79
  20. package/runtime/server/dist/config.js.map +0 -1
  21. package/runtime/server/dist/index.js +0 -146
  22. package/runtime/server/dist/index.js.map +0 -1
  23. package/runtime/server/dist/lifecycle.js +0 -85
  24. package/runtime/server/dist/lifecycle.js.map +0 -1
  25. package/runtime/server/dist/memory.js +0 -195
  26. package/runtime/server/dist/memory.js.map +0 -1
  27. package/runtime/server/package-lock.json +0 -8433
  28. package/runtime/server/package.json +0 -23
  29. package/runtime/start.bat +0 -51
  30. package/runtime/start.sh +0 -50
  31. /package/{runtime/web → web}/index.html +0 -0
  32. /package/{runtime/web → web}/js/api-key-dialog.js +0 -0
  33. /package/{runtime/web → web}/js/api.js +0 -0
  34. /package/{runtime/web → web}/js/chat-apps-dialog.js +0 -0
  35. /package/{runtime/web → web}/js/chat.js +0 -0
  36. /package/{runtime/web → web}/js/config.js +0 -0
  37. /package/{runtime/web → web}/js/main.js +0 -0
  38. /package/{runtime/web → web}/js/settings.js +0 -0
  39. /package/{runtime/web → web}/marked.min.js +0 -0
  40. /package/{runtime/web → web}/styles.css +0 -0
package/dist/cli.js CHANGED
@@ -1,16 +1,717 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/runtime/adapters/markdown.ts
13
+ function unwrapMarkdownSourceBlocks(text) {
14
+ return text.replace(
15
+ MARKDOWN_SOURCE_BLOCK_RE,
16
+ (_, content) => content.trim()
17
+ );
18
+ }
19
+ function escapeHtml(text) {
20
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
21
+ }
22
+ function escapeHtmlAttribute(text) {
23
+ return escapeHtml(text).replace(/"/g, "&quot;");
24
+ }
25
+ function protect(text, pattern, placeholders, render) {
26
+ const nextText = text.replace(pattern, (...args) => {
27
+ const match = args[0];
28
+ const groups = args.slice(1, -2);
29
+ const token = `${PLACEHOLDER_START}${placeholders.length}${PLACEHOLDER_END}`;
30
+ placeholders.push(render(match, ...groups));
31
+ return token;
32
+ });
33
+ return nextText;
34
+ }
35
+ function restore(text, placeholders) {
36
+ return text.replace(
37
+ new RegExp(`${PLACEHOLDER_START}(\\d+)${PLACEHOLDER_END}`, "g"),
38
+ (_, index) => placeholders[Number(index)] ?? ""
39
+ );
40
+ }
41
+ function formatSlackInline(text) {
42
+ let formatted = text;
43
+ formatted = formatted.replace(LINK_RE, (_, label, url) => {
44
+ return `<${url}|${label}>`;
45
+ });
46
+ formatted = formatted.replace(
47
+ /^(#{1,6})[ \t]+(.+)$/gm,
48
+ (_, __, content) => `*${content.trim()}*`
49
+ );
50
+ formatted = formatted.replace(/\*\*([^*\n]+)\*\*/g, "*$1*");
51
+ formatted = formatted.replace(/__([^_\n]+)__/g, "*$1*");
52
+ return formatted;
53
+ }
54
+ function formatTelegramInline(text) {
55
+ let formatted = escapeHtml(text);
56
+ formatted = formatted.replace(LINK_RE, (_, label, url) => {
57
+ return `<a href="${escapeHtmlAttribute(url)}">${escapeHtml(label)}</a>`;
58
+ });
59
+ formatted = formatted.replace(
60
+ /^(#{1,6})[ \t]+(.+)$/gm,
61
+ (_, __, content) => `<b>${content.trim()}</b>`
62
+ );
63
+ formatted = formatted.replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>");
64
+ formatted = formatted.replace(/__([^_\n]+)__/g, "<b>$1</b>");
65
+ formatted = formatted.replace(
66
+ /(^|[^\w<])\*([^*\n]+)\*(?=[^\w>]|$)/g,
67
+ "$1<i>$2</i>"
68
+ );
69
+ formatted = formatted.replace(
70
+ /(^|[^\w<])_([^_\n]+)_(?=[^\w>]|$)/g,
71
+ "$1<i>$2</i>"
72
+ );
73
+ formatted = formatted.replace(/^(?:-|\*) /gm, "\u2022 ");
74
+ return formatted;
75
+ }
76
+ function formatSlackMessage(text) {
77
+ const unwrapped = unwrapMarkdownSourceBlocks(text);
78
+ const placeholders = [];
79
+ const withFenced = protect(
80
+ unwrapped,
81
+ FENCED_CODE_BLOCK_RE,
82
+ placeholders,
83
+ (block) => block
84
+ );
85
+ const withInline = protect(
86
+ withFenced,
87
+ INLINE_CODE_RE,
88
+ placeholders,
89
+ (_match, code) => `\`${code}\``
90
+ );
91
+ return restore(formatSlackInline(withInline), placeholders);
92
+ }
93
+ function formatTelegramMessage(text) {
94
+ const unwrapped = unwrapMarkdownSourceBlocks(text);
95
+ const placeholders = [];
96
+ const withFenced = protect(
97
+ unwrapped,
98
+ FENCED_CODE_BLOCK_RE,
99
+ placeholders,
100
+ (block) => {
101
+ const content = block.replace(/^```[^\n]*\n/, "").replace(/\n```$/, "");
102
+ return `<pre><code>${escapeHtml(content)}</code></pre>`;
103
+ }
104
+ );
105
+ const withInline = protect(
106
+ withFenced,
107
+ INLINE_CODE_RE,
108
+ placeholders,
109
+ (_match, code) => `<code>${escapeHtml(code)}</code>`
110
+ );
111
+ return restore(formatTelegramInline(withInline), placeholders);
112
+ }
113
+ var MARKDOWN_SOURCE_BLOCK_RE, FENCED_CODE_BLOCK_RE, INLINE_CODE_RE, LINK_RE, PLACEHOLDER_START, PLACEHOLDER_END;
114
+ var init_markdown = __esm({
115
+ "src/runtime/adapters/markdown.ts"() {
116
+ "use strict";
117
+ MARKDOWN_SOURCE_BLOCK_RE = /```(?:md|markdown)\s*\n([\s\S]*?)```/gi;
118
+ FENCED_CODE_BLOCK_RE = /```[^\n]*\n[\s\S]*?```/g;
119
+ INLINE_CODE_RE = /`([^`\n]+)`/g;
120
+ LINK_RE = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g;
121
+ PLACEHOLDER_START = "\uE000";
122
+ PLACEHOLDER_END = "\uE001";
123
+ }
124
+ });
125
+
126
+ // src/runtime/adapters/telegram.ts
127
+ var telegram_exports = {};
128
+ __export(telegram_exports, {
129
+ TelegramAdapter: () => TelegramAdapter
130
+ });
131
+ import TelegramBot from "node-telegram-bot-api";
132
+ var COMMANDS2, MAX_MESSAGE_LENGTH, ACK_REACTION, TelegramAdapter;
133
+ var init_telegram = __esm({
134
+ "src/runtime/adapters/telegram.ts"() {
135
+ "use strict";
136
+ init_markdown();
137
+ COMMANDS2 = {
138
+ "/clear": "clear",
139
+ "/restart": "restart",
140
+ "/shutdown": "shutdown"
141
+ };
142
+ MAX_MESSAGE_LENGTH = 4096;
143
+ ACK_REACTION = {
144
+ type: "emoji",
145
+ emoji: "\u{1F440}"
146
+ };
147
+ TelegramAdapter = class {
148
+ name = "telegram";
149
+ bot = null;
150
+ agent = null;
151
+ options;
152
+ constructor(options) {
153
+ this.options = options;
154
+ }
155
+ async start(ctx) {
156
+ this.agent = ctx.agent;
157
+ this.bot = new TelegramBot(this.options.token, { polling: true });
158
+ this.bot.on("message", (msg) => {
159
+ this.handleTelegramMessage(msg).catch((err) => {
160
+ console.error("[Telegram] Error handling message:", err);
161
+ });
162
+ });
163
+ await this.bot.setMyCommands([
164
+ { command: "clear", description: "Clear current session and start new" },
165
+ { command: "restart", description: "Restart the server process" },
166
+ { command: "shutdown", description: "Shut down the server process" }
167
+ ]);
168
+ const me = await this.bot.getMe();
169
+ console.log(`[TelegramAdapter] Started as @${me.username}`);
170
+ }
171
+ async stop() {
172
+ if (this.bot) {
173
+ await this.bot.stopPolling();
174
+ this.bot = null;
175
+ }
176
+ console.log("[TelegramAdapter] Stopped");
177
+ }
178
+ // -------------------------------------------------------------------------
179
+ // Message handler
180
+ // -------------------------------------------------------------------------
181
+ async handleTelegramMessage(msg) {
182
+ if (!this.bot || !this.agent) return;
183
+ const chatId = msg.chat.id;
184
+ const messageId = msg.message_id;
185
+ const text = msg.text?.trim();
186
+ if (!text) return;
187
+ const channelId = `telegram-${chatId}`;
188
+ await this.tryAckReaction(chatId, messageId);
189
+ const commandKey = text.split(/\s/)[0].toLowerCase();
190
+ const command = COMMANDS2[commandKey];
191
+ if (command) {
192
+ const result = await this.agent.handleCommand(command, channelId);
193
+ await this.sendSafe(chatId, result.message || `/${command} executed.`);
194
+ return;
195
+ }
196
+ await this.bot.sendChatAction(chatId, "typing");
197
+ let finalText = "";
198
+ let hasError = false;
199
+ let errorMessage = "";
200
+ const onEvent = (event) => {
201
+ switch (event.type) {
202
+ case "text_delta":
203
+ finalText += event.delta;
204
+ break;
205
+ }
206
+ };
207
+ try {
208
+ const result = await this.agent.handleMessage(channelId, text, onEvent);
209
+ if (result.errorMessage) {
210
+ hasError = true;
211
+ errorMessage = result.errorMessage;
212
+ }
213
+ } catch (err) {
214
+ hasError = true;
215
+ errorMessage = String(err);
216
+ }
217
+ if (hasError) {
218
+ await this.sendSafe(chatId, `\u274C Error: ${errorMessage}`);
219
+ return;
220
+ }
221
+ if (!finalText.trim()) {
222
+ await this.sendSafe(chatId, "(No response generated)");
223
+ return;
224
+ }
225
+ await this.sendLongMessage(chatId, finalText);
226
+ }
227
+ // -------------------------------------------------------------------------
228
+ // Send helpers
229
+ // -------------------------------------------------------------------------
230
+ /**
231
+ * Send a message, splitting into chunks if too long.
232
+ */
233
+ async sendLongMessage(chatId, text) {
234
+ const chunks = this.splitMessage(text);
235
+ for (const chunk of chunks) {
236
+ await this.sendWithRetry(chatId, chunk);
237
+ }
238
+ }
239
+ /**
240
+ * React to the incoming message to show the bot has started processing it.
241
+ */
242
+ async tryAckReaction(chatId, messageId) {
243
+ try {
244
+ await this.bot?.setMessageReaction(chatId, messageId, {
245
+ reaction: [ACK_REACTION],
246
+ is_big: false
247
+ });
248
+ } catch (err) {
249
+ console.error("[Telegram] Failed to add ack reaction:", err);
250
+ }
251
+ }
252
+ /**
253
+ * Split text into chunks respecting Telegram's message length limit.
254
+ * Tries to split at paragraph boundaries.
255
+ */
256
+ splitMessage(text) {
257
+ if (text.length <= MAX_MESSAGE_LENGTH) {
258
+ return [text];
259
+ }
260
+ const chunks = [];
261
+ let remaining = text;
262
+ while (remaining.length > 0) {
263
+ if (remaining.length <= MAX_MESSAGE_LENGTH) {
264
+ chunks.push(remaining);
265
+ break;
266
+ }
267
+ let splitAt = remaining.lastIndexOf("\n\n", MAX_MESSAGE_LENGTH);
268
+ if (splitAt < MAX_MESSAGE_LENGTH * 0.5) {
269
+ splitAt = remaining.lastIndexOf("\n", MAX_MESSAGE_LENGTH);
270
+ }
271
+ if (splitAt < MAX_MESSAGE_LENGTH * 0.3) {
272
+ splitAt = remaining.lastIndexOf(" ", MAX_MESSAGE_LENGTH);
273
+ }
274
+ if (splitAt < 1) {
275
+ splitAt = MAX_MESSAGE_LENGTH;
276
+ }
277
+ chunks.push(remaining.slice(0, splitAt));
278
+ remaining = remaining.slice(splitAt).trimStart();
279
+ }
280
+ return chunks;
281
+ }
282
+ /**
283
+ * Send a message with automatic retry on 429 (rate limit).
284
+ */
285
+ async sendWithRetry(chatId, text, maxRetries = 3) {
286
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
287
+ try {
288
+ await this.bot.sendMessage(chatId, formatTelegramMessage(text), {
289
+ parse_mode: "HTML"
290
+ });
291
+ return;
292
+ } catch (err) {
293
+ if (err?.response?.statusCode === 429 && attempt < maxRetries) {
294
+ const retryAfter = err.response?.body?.parameters?.retry_after || 5;
295
+ console.log(
296
+ `[Telegram] Rate limited, retrying after ${retryAfter}s...`
297
+ );
298
+ await new Promise(
299
+ (resolve) => setTimeout(resolve, retryAfter * 1e3)
300
+ );
301
+ continue;
302
+ }
303
+ throw err;
304
+ }
305
+ }
306
+ }
307
+ /**
308
+ * Safe send that catches and logs errors.
309
+ */
310
+ async sendSafe(chatId, text) {
311
+ try {
312
+ await this.sendWithRetry(chatId, text);
313
+ } catch (err) {
314
+ console.error("[Telegram] Failed to send message:", err);
315
+ }
316
+ }
317
+ };
318
+ }
319
+ });
320
+
321
+ // src/runtime/adapters/slack.ts
322
+ var slack_exports = {};
323
+ __export(slack_exports, {
324
+ SlackAdapter: () => SlackAdapter
325
+ });
326
+ import { App, LogLevel } from "@slack/bolt";
327
+ var INLINE_COMMANDS, SLASH_COMMANDS, MAX_MESSAGE_LENGTH2, ACK_REACTION2, SlackAdapter;
328
+ var init_slack = __esm({
329
+ "src/runtime/adapters/slack.ts"() {
330
+ "use strict";
331
+ init_markdown();
332
+ INLINE_COMMANDS = {
333
+ "/clear": "clear",
334
+ "/restart": "restart",
335
+ "/shutdown": "shutdown"
336
+ };
337
+ SLASH_COMMANDS = {
338
+ "/skillpack-clear": "clear",
339
+ "/skillpack-restart": "restart",
340
+ "/skillpack-shutdown": "shutdown"
341
+ };
342
+ MAX_MESSAGE_LENGTH2 = 3500;
343
+ ACK_REACTION2 = "eyes";
344
+ SlackAdapter = class {
345
+ name = "slack";
346
+ app = null;
347
+ agent = null;
348
+ options;
349
+ botUserId = null;
350
+ lastThreadByChannel = /* @__PURE__ */ new Map();
351
+ constructor(options) {
352
+ this.options = options;
353
+ }
354
+ async start(ctx) {
355
+ this.agent = ctx.agent;
356
+ this.app = new App({
357
+ token: this.options.botToken,
358
+ appToken: this.options.appToken,
359
+ socketMode: true,
360
+ ignoreSelf: true,
361
+ logLevel: LogLevel.INFO
362
+ });
363
+ const auth = await this.app.client.auth.test({
364
+ token: this.options.botToken
365
+ });
366
+ this.botUserId = typeof auth.user_id === "string" ? auth.user_id : null;
367
+ this.registerListeners(this.app);
368
+ await this.app.start();
369
+ const identity = this.botUserId ? `<@${this.botUserId}>` : "Slack bot";
370
+ console.log(`[SlackAdapter] Started as ${identity}`);
371
+ }
372
+ async stop() {
373
+ if (this.app) {
374
+ await this.app.stop();
375
+ this.app = null;
376
+ }
377
+ console.log("[SlackAdapter] Stopped");
378
+ }
379
+ // -------------------------------------------------------------------------
380
+ // Listener registration
381
+ // -------------------------------------------------------------------------
382
+ registerListeners(app) {
383
+ app.event("message", async (args) => {
384
+ try {
385
+ await this.handleDirectMessage(args);
386
+ } catch (err) {
387
+ console.error("[Slack] Error handling DM:", err);
388
+ }
389
+ });
390
+ app.event("app_mention", async (args) => {
391
+ try {
392
+ await this.handleMention(args);
393
+ } catch (err) {
394
+ console.error("[Slack] Error handling mention:", err);
395
+ }
396
+ });
397
+ for (const commandName of Object.keys(SLASH_COMMANDS)) {
398
+ app.command(commandName, async (args) => {
399
+ try {
400
+ await this.handleSlashCommand(args);
401
+ } catch (err) {
402
+ console.error(`[Slack] Error handling ${commandName}:`, err);
403
+ await this.safeAck(
404
+ args.ack,
405
+ `\u274C Error: ${this.getErrorMessage(err)}`
406
+ );
407
+ }
408
+ });
409
+ }
410
+ }
411
+ // -------------------------------------------------------------------------
412
+ // Event handlers
413
+ // -------------------------------------------------------------------------
414
+ async handleDirectMessage({
415
+ event,
416
+ body,
417
+ context,
418
+ client
419
+ }) {
420
+ if (!this.agent || !this.isSupportedDmEvent(event)) {
421
+ return;
422
+ }
423
+ const text = (event.text || "").trim();
424
+ if (!text) return;
425
+ const teamId = this.getTeamId(body, context);
426
+ const channelId = `slack-dm-${teamId}-${event.channel}`;
427
+ const route = { channel: event.channel };
428
+ await this.tryAckReaction(client, event);
429
+ if (await this.tryHandleInlineCommand(text, channelId, client, route)) {
430
+ return;
431
+ }
432
+ await this.runAgent(channelId, text, client, route);
433
+ }
434
+ async handleMention({
435
+ event,
436
+ body,
437
+ context,
438
+ client
439
+ }) {
440
+ if (!this.agent || !this.isSupportedMentionEvent(event)) {
441
+ return;
442
+ }
443
+ const teamId = this.getTeamId(body, context);
444
+ const threadTs = event.thread_ts || event.ts;
445
+ const channelId = `slack-thread-${teamId}-${event.channel}-${threadTs}`;
446
+ const route = {
447
+ channel: event.channel,
448
+ threadTs
449
+ };
450
+ this.lastThreadByChannel.set(
451
+ this.getChannelKey(teamId, event.channel),
452
+ threadTs
453
+ );
454
+ const text = this.stripBotMention(event.text || "").trim();
455
+ if (!text) {
456
+ await this.sendSafe(
457
+ client,
458
+ route,
459
+ "Mention me with a message, or use `/clear` to reset this thread."
460
+ );
461
+ return;
462
+ }
463
+ await this.tryAckReaction(client, event);
464
+ if (await this.tryHandleInlineCommand(text, channelId, client, route)) {
465
+ return;
466
+ }
467
+ await this.runAgent(channelId, text, client, route);
468
+ }
469
+ async handleSlashCommand({
470
+ command,
471
+ body,
472
+ context,
473
+ ack
474
+ }) {
475
+ const commandName = command?.command;
476
+ const mapped = commandName ? SLASH_COMMANDS[commandName] : void 0;
477
+ if (!this.agent || !mapped) {
478
+ await this.safeAck(ack, "Unsupported slash command.");
479
+ return;
480
+ }
481
+ const resolved = this.resolveSlashCommandTarget(body || command, context);
482
+ if (!resolved.channelId) {
483
+ await this.safeAck(ack, resolved.message);
484
+ return;
485
+ }
486
+ const result = await this.agent.handleCommand(mapped, resolved.channelId);
487
+ const parts = [result.message || `${commandName} executed.`];
488
+ if (resolved.note) {
489
+ parts.push(resolved.note);
490
+ }
491
+ await this.safeAck(ack, parts.join("\n"));
492
+ }
493
+ // -------------------------------------------------------------------------
494
+ // Agent bridge
495
+ // -------------------------------------------------------------------------
496
+ async runAgent(channelId, text, client, route) {
497
+ if (!this.agent) return;
498
+ let finalText = "";
499
+ let hasError = false;
500
+ let errorMessage = "";
501
+ const onEvent = (event) => {
502
+ if (event.type === "text_delta") {
503
+ finalText += event.delta;
504
+ }
505
+ };
506
+ try {
507
+ const result = await this.agent.handleMessage(channelId, text, onEvent);
508
+ if (result.errorMessage) {
509
+ hasError = true;
510
+ errorMessage = result.errorMessage;
511
+ }
512
+ } catch (err) {
513
+ hasError = true;
514
+ errorMessage = this.getErrorMessage(err);
515
+ }
516
+ if (hasError) {
517
+ await this.sendSafe(client, route, `\u274C Error: ${errorMessage}`);
518
+ return;
519
+ }
520
+ if (!finalText.trim()) {
521
+ await this.sendSafe(client, route, "(No response generated)");
522
+ return;
523
+ }
524
+ await this.sendLongMessage(client, route, finalText);
525
+ }
526
+ // -------------------------------------------------------------------------
527
+ // Helpers
528
+ // -------------------------------------------------------------------------
529
+ async tryHandleInlineCommand(text, channelId, client, route) {
530
+ if (!this.agent) return false;
531
+ const commandKey = text.split(/\s/)[0].toLowerCase();
532
+ const command = INLINE_COMMANDS[commandKey];
533
+ if (!command) return false;
534
+ const result = await this.agent.handleCommand(command, channelId);
535
+ await this.sendSafe(
536
+ client,
537
+ route,
538
+ result.message || `${commandKey} executed.`
539
+ );
540
+ return true;
541
+ }
542
+ resolveSlashCommandTarget(payload, context) {
543
+ const teamId = this.getTeamId(payload, context);
544
+ const channel = payload?.channel_id;
545
+ if (!channel) {
546
+ return { message: "Missing Slack channel context." };
547
+ }
548
+ if (this.isDmChannelId(channel)) {
549
+ return {
550
+ channelId: `slack-dm-${teamId}-${channel}`,
551
+ message: ""
552
+ };
553
+ }
554
+ const threadTs = this.lastThreadByChannel.get(
555
+ this.getChannelKey(teamId, channel)
556
+ );
557
+ if (!threadTs) {
558
+ return {
559
+ message: "No active Skillpack thread found in this channel. Mention the bot first, or run the command inside the thread as `@bot /clear`."
560
+ };
561
+ }
562
+ return {
563
+ channelId: `slack-thread-${teamId}-${channel}-${threadTs}`,
564
+ message: "",
565
+ note: "Applied to the most recent active Skillpack thread in this channel."
566
+ };
567
+ }
568
+ isSupportedDmEvent(event) {
569
+ if (!event || event.type !== "message") return false;
570
+ if (event.channel_type !== "im") return false;
571
+ if (event.subtype) return false;
572
+ if (event.bot_id) return false;
573
+ if (!event.user || typeof event.text !== "string") return false;
574
+ return true;
575
+ }
576
+ isSupportedMentionEvent(event) {
577
+ if (!event || event.type !== "app_mention") return false;
578
+ if (event.subtype) return false;
579
+ if (event.bot_id) return false;
580
+ if (!event.user || typeof event.text !== "string") return false;
581
+ return true;
582
+ }
583
+ stripBotMention(text) {
584
+ const mention = this.botUserId ? new RegExp(`^\\s*<@${this.escapeRegExp(this.botUserId)}>\\s*`) : /^\s*<@[^>]+>\s*/;
585
+ return text.replace(mention, "");
586
+ }
587
+ splitMessage(text) {
588
+ if (text.length <= MAX_MESSAGE_LENGTH2) {
589
+ return [text];
590
+ }
591
+ const chunks = [];
592
+ let remaining = text;
593
+ while (remaining.length > 0) {
594
+ if (remaining.length <= MAX_MESSAGE_LENGTH2) {
595
+ chunks.push(remaining);
596
+ break;
597
+ }
598
+ let splitAt = remaining.lastIndexOf("\n\n", MAX_MESSAGE_LENGTH2);
599
+ if (splitAt < MAX_MESSAGE_LENGTH2 * 0.5) {
600
+ splitAt = remaining.lastIndexOf("\n", MAX_MESSAGE_LENGTH2);
601
+ }
602
+ if (splitAt < MAX_MESSAGE_LENGTH2 * 0.3) {
603
+ splitAt = remaining.lastIndexOf(" ", MAX_MESSAGE_LENGTH2);
604
+ }
605
+ if (splitAt < 1) {
606
+ splitAt = MAX_MESSAGE_LENGTH2;
607
+ }
608
+ chunks.push(remaining.slice(0, splitAt));
609
+ remaining = remaining.slice(splitAt).trimStart();
610
+ }
611
+ return chunks;
612
+ }
613
+ async sendLongMessage(client, route, text) {
614
+ for (const chunk of this.splitMessage(text)) {
615
+ await this.sendWithRetry(client, route, chunk);
616
+ }
617
+ }
618
+ async sendSafe(client, route, text) {
619
+ try {
620
+ await this.sendWithRetry(client, route, text);
621
+ } catch (err) {
622
+ console.error("[Slack] Failed to send message:", err);
623
+ }
624
+ }
625
+ async sendWithRetry(client, route, text, maxRetries = 3) {
626
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
627
+ try {
628
+ await client.chat.postMessage({
629
+ channel: route.channel,
630
+ text: formatSlackMessage(text),
631
+ mrkdwn: true,
632
+ thread_ts: route.threadTs,
633
+ reply_broadcast: false
634
+ });
635
+ return;
636
+ } catch (err) {
637
+ const retryAfter = this.getRetryAfterSeconds(err);
638
+ if (retryAfter && attempt < maxRetries) {
639
+ console.log(
640
+ `[Slack] Rate limited, retrying after ${retryAfter}s...`
641
+ );
642
+ await new Promise(
643
+ (resolve) => setTimeout(resolve, retryAfter * 1e3)
644
+ );
645
+ continue;
646
+ }
647
+ throw err;
648
+ }
649
+ }
650
+ }
651
+ async tryAckReaction(client, event) {
652
+ try {
653
+ await client.reactions.add({
654
+ channel: event.channel,
655
+ timestamp: event.ts,
656
+ name: ACK_REACTION2
657
+ });
658
+ } catch (err) {
659
+ console.error("[Slack] Failed to add ack reaction:", err);
660
+ }
661
+ }
662
+ async safeAck(ack, message) {
663
+ if (!ack) return;
664
+ try {
665
+ await ack(message);
666
+ } catch (err) {
667
+ console.error("[Slack] Failed to ack slash command:", err);
668
+ }
669
+ }
670
+ getRetryAfterSeconds(err) {
671
+ const candidates = [
672
+ err?.data?.retryAfter,
673
+ err?.retryAfter,
674
+ err?.headers?.["retry-after"],
675
+ err?.data?.headers?.["retry-after"]
676
+ ];
677
+ for (const value of candidates) {
678
+ const seconds = Number(value);
679
+ if (Number.isFinite(seconds) && seconds > 0) {
680
+ return seconds;
681
+ }
682
+ }
683
+ return null;
684
+ }
685
+ getTeamId(payload, context) {
686
+ return context?.teamId || payload?.team_id || payload?.team?.id || payload?.authorizations?.[0]?.team_id || "unknown";
687
+ }
688
+ getChannelKey(teamId, channelId) {
689
+ return `${teamId}:${channelId}`;
690
+ }
691
+ isDmChannelId(channelId) {
692
+ return channelId.startsWith("D");
693
+ }
694
+ getErrorMessage(err) {
695
+ return err instanceof Error ? err.message : String(err);
696
+ }
697
+ escapeRegExp(value) {
698
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
699
+ }
700
+ };
701
+ }
702
+ });
2
703
 
3
704
  // src/cli.ts
4
705
  import { Command } from "commander";
5
- import chalk8 from "chalk";
706
+ import chalk5 from "chalk";
6
707
 
7
708
  // src/commands/create.ts
8
- import fs5 from "fs";
9
- import path5 from "path";
709
+ import fs4 from "fs";
710
+ import path4 from "path";
10
711
  import inquirer from "inquirer";
11
712
  import chalk3 from "chalk";
12
713
 
13
- // src/core/pack-config.ts
714
+ // src/pack-config.ts
14
715
  import fs from "fs";
15
716
  import path from "path";
16
717
  var PACK_FILE = "skillpack.json";
@@ -113,13 +814,13 @@ function configExists(workDir) {
113
814
  return fs.existsSync(getPackPath(workDir));
114
815
  }
115
816
 
116
- // src/core/bundler.ts
117
- import fs4 from "fs";
118
- import path4 from "path";
817
+ // src/commands/zip.ts
818
+ import fs3 from "fs";
819
+ import path3 from "path";
119
820
  import archiver from "archiver";
120
821
  import chalk2 from "chalk";
121
822
 
122
- // src/core/skill-manager.ts
823
+ // src/skill-manager.ts
123
824
  import { spawnSync } from "child_process";
124
825
  import fs2 from "fs";
125
826
  import path2 from "path";
@@ -282,149 +983,17 @@ function refreshDescriptionsAndSave(workDir, config) {
282
983
  saveConfig(workDir, config);
283
984
  return config;
284
985
  }
285
- function removeSkill(workDir, skillName) {
286
- const config = loadConfig(workDir);
287
- const normalizedName = normalizeName(skillName);
288
- const nextSkills = config.skills.filter(
289
- (skill) => normalizeName(skill.name) !== normalizedName
290
- );
291
- if (nextSkills.length === config.skills.length) {
292
- console.log(chalk.yellow(`Skill not found: ${skillName}`));
293
- return false;
294
- }
295
- config.skills = nextSkills;
296
- saveConfig(workDir, config);
297
- const installedMatches = scanInstalledSkills(workDir).filter(
298
- (skill) => normalizeName(skill.name) === normalizedName
299
- );
300
- if (installedMatches.length === 0) {
301
- console.log(
302
- chalk.yellow(`Removed config for ${skillName}, but no installed files were found`)
303
- );
304
- return true;
305
- }
306
- for (const skill of installedMatches) {
307
- if (fs2.existsSync(skill.dir)) {
308
- fs2.rmSync(skill.dir, { recursive: true, force: true });
309
- }
310
- }
311
- console.log(chalk.green(`Removed skill: ${skillName}`));
312
- return true;
313
- }
314
986
 
315
- // src/core/runtime-template.ts
316
- import fs3 from "fs";
317
- import path3 from "path";
318
- import { fileURLToPath } from "url";
319
- var __dirname = path3.dirname(fileURLToPath(import.meta.url));
320
- var EXECUTABLE_RUNTIME_FILES = /* @__PURE__ */ new Set(["start.sh", "start.bat"]);
321
- function isExecutableRuntimeFile(relativePath) {
322
- return EXECUTABLE_RUNTIME_FILES.has(relativePath);
323
- }
324
- function withExecuteBits(mode) {
325
- return mode | 73;
326
- }
327
- function getRuntimeDir() {
328
- const projectRoot = path3.resolve(__dirname, "..");
329
- return path3.join(projectRoot, "runtime");
330
- }
331
- function assertRuntimeDirExists(runtimeDir) {
332
- if (!fs3.existsSync(runtimeDir)) {
333
- throw new Error(`Runtime directory not found: ${runtimeDir}`);
334
- }
335
- }
336
- function collectRuntimeTemplateEntries(runtimeDir) {
337
- assertRuntimeDirExists(runtimeDir);
338
- const entries = [];
339
- function visit(currentDir, relativeDir = "") {
340
- const dirEntries = fs3.readdirSync(currentDir, { withFileTypes: true });
341
- for (const dirEntry of dirEntries) {
342
- if (dirEntry.name === "node_modules") {
343
- continue;
344
- }
345
- const currentRelative = relativeDir ? path3.posix.join(relativeDir, dirEntry.name) : dirEntry.name;
346
- if (currentRelative === "server/src" || currentRelative === "server/tsconfig.json") {
347
- continue;
348
- }
349
- const absolutePath = path3.join(currentDir, dirEntry.name);
350
- const relativePath = relativeDir ? path3.posix.join(relativeDir, dirEntry.name) : dirEntry.name;
351
- const stats = fs3.statSync(absolutePath);
352
- if (dirEntry.isDirectory()) {
353
- entries.push({
354
- absolutePath,
355
- relativePath,
356
- stats,
357
- type: "directory"
358
- });
359
- visit(absolutePath, relativePath);
360
- continue;
361
- }
362
- if (dirEntry.isFile()) {
363
- entries.push({
364
- absolutePath,
365
- relativePath,
366
- stats,
367
- type: "file"
368
- });
369
- }
370
- }
371
- }
372
- visit(runtimeDir);
373
- return entries;
374
- }
375
- function copyRuntimeTemplate(runtimeDir, workDir) {
376
- const entries = collectRuntimeTemplateEntries(runtimeDir);
377
- for (const entry of entries) {
378
- const destinationPath = path3.join(workDir, entry.relativePath);
379
- if (entry.type === "directory") {
380
- fs3.mkdirSync(destinationPath, { recursive: true });
381
- continue;
382
- }
383
- fs3.mkdirSync(path3.dirname(destinationPath), { recursive: true });
384
- fs3.copyFileSync(entry.absolutePath, destinationPath);
385
- fs3.chmodSync(destinationPath, entry.stats.mode);
386
- }
387
- }
388
- function ensureRuntimeLaunchersExecutable(workDir) {
389
- for (const relativePath of EXECUTABLE_RUNTIME_FILES) {
390
- const filePath = path3.join(workDir, relativePath);
391
- if (!fs3.existsSync(filePath)) {
392
- continue;
393
- }
394
- const currentMode = fs3.statSync(filePath).mode;
395
- fs3.chmodSync(filePath, withExecuteBits(currentMode));
396
- }
397
- }
398
- function addRuntimeFiles(archive, runtimeDir, prefix) {
399
- const entries = collectRuntimeTemplateEntries(runtimeDir);
400
- for (const entry of entries) {
401
- const archivePath = `${prefix}/${entry.relativePath}`;
402
- if (entry.type === "directory") {
403
- archive.append("", {
404
- name: `${archivePath}/`,
405
- mode: entry.stats.mode
406
- });
407
- continue;
408
- }
409
- archive.file(entry.absolutePath, {
410
- name: archivePath,
411
- mode: isExecutableRuntimeFile(entry.relativePath) ? withExecuteBits(entry.stats.mode) : entry.stats.mode
412
- });
413
- }
414
- }
415
-
416
- // src/core/bundler.ts
417
- async function bundle(workDir) {
987
+ // src/commands/zip.ts
988
+ async function zipCommand(workDir) {
418
989
  const config = loadConfig(workDir);
419
990
  const zipName = `${config.name}.zip`;
420
- const zipPath = path4.join(workDir, zipName);
421
- const runtimeDir = getRuntimeDir();
422
- assertRuntimeDirExists(runtimeDir);
991
+ const zipPath = path3.join(workDir, zipName);
423
992
  installConfiguredSkills(workDir, config);
424
993
  syncSkillDescriptions(workDir, config);
425
994
  saveConfig(workDir, config);
426
995
  console.log(chalk2.blue(`Packaging ${config.name}...`));
427
- const output = fs4.createWriteStream(zipPath);
996
+ const output = fs3.createWriteStream(zipPath);
428
997
  const archive = archiver("zip", { zlib: { level: 9 } });
429
998
  return new Promise((resolve, reject) => {
430
999
  output.on("close", () => {
@@ -441,11 +1010,18 @@ async function bundle(workDir) {
441
1010
  archive.file(getPackPath(workDir), {
442
1011
  name: `${prefix}/${PACK_FILE}`
443
1012
  });
444
- const skillsDir = path4.join(workDir, "skills");
445
- if (fs4.existsSync(skillsDir)) {
1013
+ const skillsDir = path3.join(workDir, "skills");
1014
+ if (fs3.existsSync(skillsDir)) {
446
1015
  archive.directory(skillsDir, `${prefix}/skills`);
447
1016
  }
448
- addRuntimeFiles(archive, runtimeDir, prefix);
1017
+ const startSh = path3.join(workDir, "start.sh");
1018
+ if (fs3.existsSync(startSh)) {
1019
+ archive.file(startSh, { name: `${prefix}/start.sh`, mode: 493 });
1020
+ }
1021
+ const startBat = path3.join(workDir, "start.bat");
1022
+ if (fs3.existsSync(startBat)) {
1023
+ archive.file(startBat, { name: `${prefix}/start.bat` });
1024
+ }
449
1025
  archive.finalize();
450
1026
  });
451
1027
  }
@@ -473,11 +1049,92 @@ function parseSourceInput(value) {
473
1049
  inlineSkillNames: inlineSkillValue.split(/[,\s]+/).map((name) => name.trim()).filter(Boolean)
474
1050
  };
475
1051
  }
476
- async function createCommand(directory) {
477
- const workDir = directory ? path5.resolve(directory) : process.cwd();
1052
+ function isHttpUrl(value) {
1053
+ try {
1054
+ const url = new URL(value);
1055
+ return url.protocol === "http:" || url.protocol === "https:";
1056
+ } catch {
1057
+ return false;
1058
+ }
1059
+ }
1060
+ async function readConfigSource(source) {
1061
+ let raw = "";
1062
+ if (isHttpUrl(source)) {
1063
+ const response = await fetch(source);
1064
+ if (!response.ok) {
1065
+ throw new Error(
1066
+ `Failed to download config: ${response.status} ${response.statusText}`
1067
+ );
1068
+ }
1069
+ raw = await response.text();
1070
+ } else {
1071
+ const filePath = path4.resolve(source);
1072
+ raw = fs4.readFileSync(filePath, "utf-8");
1073
+ }
1074
+ const parsed = JSON.parse(raw);
1075
+ validateConfigShape(parsed, source);
1076
+ return parsed;
1077
+ }
1078
+ function copyStartTemplates(workDir) {
1079
+ const templateDir = path4.resolve(
1080
+ new URL("../templates", import.meta.url).pathname
1081
+ );
1082
+ for (const file of ["start.sh", "start.bat"]) {
1083
+ const src = path4.join(templateDir, file);
1084
+ const dest = path4.join(workDir, file);
1085
+ if (fs4.existsSync(src)) {
1086
+ fs4.copyFileSync(src, dest);
1087
+ if (file === "start.sh") {
1088
+ fs4.chmodSync(dest, 493);
1089
+ }
1090
+ } else {
1091
+ console.warn(chalk3.yellow(` [warn] Template not found: ${src}`));
1092
+ }
1093
+ }
1094
+ }
1095
+ async function createCommand(directory, options = {}) {
1096
+ const workDir = directory ? path4.resolve(directory) : process.cwd();
478
1097
  if (directory) {
479
- fs5.mkdirSync(workDir, { recursive: true });
1098
+ fs4.mkdirSync(workDir, { recursive: true });
480
1099
  }
1100
+ if (options.config) {
1101
+ await initFromConfig(workDir, options.config);
1102
+ return;
1103
+ }
1104
+ await interactiveCreate(workDir);
1105
+ }
1106
+ async function initFromConfig(workDir, configSource) {
1107
+ if (configExists(workDir)) {
1108
+ const { overwrite } = await inquirer.prompt([
1109
+ {
1110
+ type: "confirm",
1111
+ name: "overwrite",
1112
+ message: `A ${PACK_FILE} file already exists in this directory. Overwrite it?`,
1113
+ default: false
1114
+ }
1115
+ ]);
1116
+ if (!overwrite) {
1117
+ console.log(chalk3.yellow("Cancelled"));
1118
+ return;
1119
+ }
1120
+ }
1121
+ const config = await readConfigSource(configSource);
1122
+ saveConfig(workDir, config);
1123
+ console.log(chalk3.blue(`
1124
+ Initialize ${config.name} from ${configSource}
1125
+ `));
1126
+ installConfiguredSkills(workDir, config);
1127
+ refreshDescriptionsAndSave(workDir, config);
1128
+ copyStartTemplates(workDir);
1129
+ console.log(chalk3.green(`
1130
+ ${PACK_FILE} saved`));
1131
+ console.log(chalk3.green(` start.sh / start.bat created`));
1132
+ console.log(chalk3.green(` Initialization complete.
1133
+ `));
1134
+ console.log(chalk3.dim(` Run npx @cremini/skillpack run . to start
1135
+ `));
1136
+ }
1137
+ async function interactiveCreate(workDir) {
481
1138
  if (configExists(workDir)) {
482
1139
  const { overwrite } = await inquirer.prompt([
483
1140
  {
@@ -568,261 +1225,867 @@ async function createCommand(directory) {
568
1225
  {
569
1226
  type: "input",
570
1227
  name: "prompt",
571
- message: isFirst ? `Prompt #${promptIndex} (required):` : `Prompt #${promptIndex} (leave blank to finish):`,
572
- validate: isFirst ? (value) => value.trim() ? true : "The first Prompt cannot be empty" : void 0
1228
+ message: isFirst ? `Prompt #${promptIndex} (leave blank to skip):` : `Prompt #${promptIndex} (leave blank to finish):`
573
1229
  }
574
1230
  ]);
575
- if (!isFirst && !prompt.trim()) {
1231
+ if (!prompt.trim()) {
576
1232
  break;
577
1233
  }
578
1234
  config.prompts.push(prompt.trim());
579
1235
  promptIndex++;
580
1236
  }
581
- const { shouldBundle } = await inquirer.prompt([
1237
+ const { shouldZip } = await inquirer.prompt([
582
1238
  {
583
1239
  type: "confirm",
584
- name: "shouldBundle",
585
- message: "Bundle as a zip now?",
1240
+ name: "shouldZip",
1241
+ message: "Package as a zip now?",
586
1242
  default: true
587
1243
  }
588
1244
  ]);
589
1245
  saveConfig(workDir, config);
1246
+ copyStartTemplates(workDir);
590
1247
  console.log(chalk3.green(`
591
- ${PACK_FILE} saved
1248
+ ${PACK_FILE} saved`));
1249
+ console.log(chalk3.green(` start.sh / start.bat created
592
1250
  `));
593
1251
  if (requestedSkills.length > 0) {
594
1252
  installConfiguredSkills(workDir, config);
595
1253
  refreshDescriptionsAndSave(workDir, config);
596
1254
  }
597
- if (shouldBundle) {
598
- await bundle(workDir);
1255
+ if (shouldZip) {
1256
+ await zipCommand(workDir);
599
1257
  }
600
1258
  console.log(chalk3.green("\n Done!"));
601
- if (!shouldBundle) {
1259
+ if (!shouldZip) {
602
1260
  console.log(
603
- chalk3.dim(" Run npx @cremini/skillpack build to create the zip\n")
1261
+ chalk3.dim(
1262
+ " Run npx @cremini/skillpack run . to start\n Run npx @cremini/skillpack zip to create the zip\n"
1263
+ )
604
1264
  );
605
1265
  }
606
1266
  }
607
1267
 
608
- // src/commands/init.ts
609
- import fs6 from "fs";
610
- import path6 from "path";
1268
+ // src/commands/run.ts
1269
+ import path9 from "path";
1270
+ import fs9 from "fs";
611
1271
  import inquirer2 from "inquirer";
612
1272
  import chalk4 from "chalk";
613
- function isHttpUrl(value) {
614
- try {
615
- const url = new URL(value);
616
- return url.protocol === "http:" || url.protocol === "https:";
617
- } catch {
618
- return false;
1273
+
1274
+ // src/runtime/server.ts
1275
+ import express from "express";
1276
+ import path8 from "path";
1277
+ import fs8 from "fs";
1278
+ import { fileURLToPath } from "url";
1279
+ import { createServer } from "http";
1280
+ import { exec } from "child_process";
1281
+
1282
+ // src/runtime/agent.ts
1283
+ import path5 from "path";
1284
+ import fs5 from "fs";
1285
+ import {
1286
+ AuthStorage,
1287
+ createAgentSession,
1288
+ createCodingTools,
1289
+ ModelRegistry,
1290
+ SessionManager,
1291
+ DefaultResourceLoader
1292
+ } from "@mariozechner/pi-coding-agent";
1293
+ var DEBUG = true;
1294
+ var log = (...args) => DEBUG && console.log(...args);
1295
+ var write = (data) => DEBUG && process.stdout.write(data);
1296
+ function getAssistantDiagnostics(message) {
1297
+ if (!message || message.role !== "assistant") {
1298
+ return null;
619
1299
  }
1300
+ const stopReason = message.stopReason ?? "unknown";
1301
+ const errorMessage = message.errorMessage || (stopReason === "error" || stopReason === "aborted" ? `Request ${stopReason}` : "");
1302
+ const content = Array.isArray(message.content) ? message.content : [];
1303
+ const text = content.filter((item) => item?.type === "text").map((item) => item.text || "").join("").trim();
1304
+ const toolCalls = content.filter(
1305
+ (item) => item?.type === "toolCall"
1306
+ ).length;
1307
+ return { stopReason, errorMessage, hasText: text.length > 0, toolCalls };
620
1308
  }
621
- async function readConfigSource(source) {
622
- let raw = "";
623
- if (isHttpUrl(source)) {
624
- const response = await fetch(source);
625
- if (!response.ok) {
626
- throw new Error(`Failed to download config: ${response.status} ${response.statusText}`);
1309
+ function getLifecycleTrigger(channelId) {
1310
+ if (channelId.startsWith("telegram-")) return "telegram";
1311
+ if (channelId.startsWith("slack-")) return "slack";
1312
+ return "web";
1313
+ }
1314
+ var PackAgent = class {
1315
+ options;
1316
+ channels = /* @__PURE__ */ new Map();
1317
+ pendingSessionCreations = /* @__PURE__ */ new Map();
1318
+ constructor(options) {
1319
+ this.options = options;
1320
+ }
1321
+ /**
1322
+ * Lazily create (or return existing) session for a channel.
1323
+ */
1324
+ async getOrCreateSession(channelId) {
1325
+ const existing = this.channels.get(channelId);
1326
+ if (existing) return existing;
1327
+ const pendingCreation = this.pendingSessionCreations.get(channelId);
1328
+ if (pendingCreation) return pendingCreation;
1329
+ const createSessionPromise = (async () => {
1330
+ const { apiKey, rootDir, provider, modelId } = this.options;
1331
+ const authStorage = AuthStorage.inMemory({
1332
+ [provider]: { type: "api_key", key: apiKey }
1333
+ });
1334
+ authStorage.setRuntimeApiKey(provider, apiKey);
1335
+ const modelRegistry = new ModelRegistry(authStorage);
1336
+ const model = modelRegistry.find(provider, modelId);
1337
+ const sessionDir = path5.resolve(
1338
+ rootDir,
1339
+ "data",
1340
+ "sessions",
1341
+ channelId
1342
+ );
1343
+ fs5.mkdirSync(sessionDir, { recursive: true });
1344
+ const sessionManager = SessionManager.continueRecent(rootDir, sessionDir);
1345
+ log(`[PackAgent] Session dir: ${sessionDir}`);
1346
+ const workspaceDir = path5.resolve(
1347
+ rootDir,
1348
+ "data",
1349
+ "workspaces",
1350
+ channelId
1351
+ );
1352
+ fs5.mkdirSync(workspaceDir, { recursive: true });
1353
+ log(`[PackAgent] Workspace dir: ${workspaceDir}`);
1354
+ const skillsPath = path5.resolve(rootDir, "skills");
1355
+ log(`[PackAgent] Loading skills from: ${skillsPath}`);
1356
+ const resourceLoader = new DefaultResourceLoader({
1357
+ cwd: rootDir,
1358
+ additionalSkillPaths: [skillsPath]
1359
+ });
1360
+ await resourceLoader.reload();
1361
+ const tools = createCodingTools(workspaceDir);
1362
+ const { session } = await createAgentSession({
1363
+ cwd: workspaceDir,
1364
+ authStorage,
1365
+ modelRegistry,
1366
+ sessionManager,
1367
+ resourceLoader,
1368
+ model,
1369
+ tools
1370
+ });
1371
+ const channelSession = {
1372
+ session,
1373
+ running: false,
1374
+ pending: Promise.resolve()
1375
+ };
1376
+ this.channels.set(channelId, channelSession);
1377
+ return channelSession;
1378
+ })();
1379
+ this.pendingSessionCreations.set(channelId, createSessionPromise);
1380
+ try {
1381
+ return await createSessionPromise;
1382
+ } finally {
1383
+ this.pendingSessionCreations.delete(channelId);
627
1384
  }
628
- raw = await response.text();
629
- } else {
630
- const filePath = path6.resolve(source);
631
- raw = fs6.readFileSync(filePath, "utf-8");
632
1385
  }
633
- const parsed = JSON.parse(raw);
634
- validateConfigShape(parsed, source);
635
- return parsed;
636
- }
637
- async function initCommand(directory, options) {
638
- const workDir = directory ? path6.resolve(directory) : process.cwd();
639
- if (directory) {
640
- fs6.mkdirSync(workDir, { recursive: true });
1386
+ async handleMessage(channelId, text, onEvent) {
1387
+ const cs = await this.getOrCreateSession(channelId);
1388
+ const run = async () => {
1389
+ cs.running = true;
1390
+ let turnHadVisibleOutput = false;
1391
+ const unsubscribe = cs.session.subscribe((event) => {
1392
+ switch (event.type) {
1393
+ case "agent_start":
1394
+ log("\n=== [AGENT SESSION START] ===");
1395
+ log("System Prompt:\n", cs.session.systemPrompt);
1396
+ log("============================\n");
1397
+ onEvent({ type: "agent_start" });
1398
+ break;
1399
+ case "message_start":
1400
+ log(`
1401
+ --- [Message Start: ${event.message?.role}] ---`);
1402
+ if (event.message?.role === "user") {
1403
+ log(JSON.stringify(event.message.content, null, 2));
1404
+ }
1405
+ onEvent({ type: "message_start", role: event.message?.role ?? "" });
1406
+ break;
1407
+ case "message_update":
1408
+ if (event.assistantMessageEvent?.type === "text_delta") {
1409
+ turnHadVisibleOutput = true;
1410
+ write(event.assistantMessageEvent.delta);
1411
+ onEvent({
1412
+ type: "text_delta",
1413
+ delta: event.assistantMessageEvent.delta
1414
+ });
1415
+ } else if (event.assistantMessageEvent?.type === "thinking_delta") {
1416
+ turnHadVisibleOutput = true;
1417
+ onEvent({
1418
+ type: "thinking_delta",
1419
+ delta: event.assistantMessageEvent.delta
1420
+ });
1421
+ }
1422
+ break;
1423
+ case "message_end":
1424
+ log(`
1425
+ --- [Message End: ${event.message?.role}] ---`);
1426
+ if (event.message?.role === "assistant") {
1427
+ const diagnostics = getAssistantDiagnostics(event.message);
1428
+ if (diagnostics) {
1429
+ log(
1430
+ `[Assistant Diagnostics] stopReason=${diagnostics.stopReason} text=${diagnostics.hasText ? "yes" : "no"} toolCalls=${diagnostics.toolCalls}`
1431
+ );
1432
+ if (diagnostics.errorMessage) {
1433
+ log(`[Assistant Error] ${diagnostics.errorMessage}`);
1434
+ }
1435
+ }
1436
+ }
1437
+ onEvent({ type: "message_end", role: event.message?.role ?? "" });
1438
+ break;
1439
+ case "tool_execution_start":
1440
+ turnHadVisibleOutput = true;
1441
+ log(`
1442
+ >>> [Tool Start: ${event.toolName}] >>>`);
1443
+ log("Args:", JSON.stringify(event.args, null, 2));
1444
+ onEvent({
1445
+ type: "tool_start",
1446
+ toolName: event.toolName,
1447
+ toolInput: event.args
1448
+ });
1449
+ break;
1450
+ case "tool_execution_end":
1451
+ turnHadVisibleOutput = true;
1452
+ log(`<<< [Tool End: ${event.toolName}] <<<`);
1453
+ log(`Error: ${event.isError ? "Yes" : "No"}`);
1454
+ onEvent({
1455
+ type: "tool_end",
1456
+ toolName: event.toolName,
1457
+ isError: event.isError,
1458
+ result: event.result
1459
+ });
1460
+ break;
1461
+ case "agent_end":
1462
+ log("\n=== [AGENT SESSION END] ===\n");
1463
+ onEvent({ type: "agent_end" });
1464
+ break;
1465
+ }
1466
+ });
1467
+ try {
1468
+ await cs.session.prompt(text);
1469
+ const lastMessage = cs.session.state.messages.at(-1);
1470
+ const diagnostics = getAssistantDiagnostics(lastMessage);
1471
+ if (diagnostics?.errorMessage) {
1472
+ return {
1473
+ stopReason: diagnostics.stopReason,
1474
+ errorMessage: diagnostics.errorMessage
1475
+ };
1476
+ }
1477
+ if (diagnostics && !diagnostics.hasText && diagnostics.toolCalls === 0 && !turnHadVisibleOutput) {
1478
+ return {
1479
+ stopReason: diagnostics.stopReason,
1480
+ errorMessage: "Assistant returned no visible output. Check the server logs for details."
1481
+ };
1482
+ }
1483
+ return { stopReason: diagnostics?.stopReason ?? "unknown" };
1484
+ } finally {
1485
+ cs.running = false;
1486
+ unsubscribe();
1487
+ }
1488
+ };
1489
+ const resultPromise = cs.pending.catch(() => void 0).then(run);
1490
+ cs.pending = resultPromise.then(() => void 0, () => void 0);
1491
+ return resultPromise;
641
1492
  }
642
- if (configExists(workDir)) {
643
- const { overwrite } = await inquirer2.prompt([
644
- {
645
- type: "confirm",
646
- name: "overwrite",
647
- message: `A ${PACK_FILE} file already exists in this directory. Overwrite it?`,
648
- default: false
1493
+ async handleCommand(command, channelId) {
1494
+ switch (command) {
1495
+ case "new":
1496
+ case "clear": {
1497
+ const cs = this.channels.get(channelId);
1498
+ if (cs) {
1499
+ cs.session.dispose();
1500
+ this.channels.delete(channelId);
1501
+ }
1502
+ const { rootDir } = this.options;
1503
+ const sessionDir = path5.resolve(rootDir, "data", "sessions", channelId);
1504
+ if (fs5.existsSync(sessionDir)) {
1505
+ fs5.rmSync(sessionDir, { recursive: true, force: true });
1506
+ log(`[PackAgent] Cleared session dir: ${sessionDir}`);
1507
+ }
1508
+ return {
1509
+ success: true,
1510
+ message: command === "new" ? "New session started." : "Session cleared."
1511
+ };
649
1512
  }
650
- ]);
651
- if (!overwrite) {
652
- console.log(chalk4.yellow("Cancelled"));
653
- return;
1513
+ case "restart":
1514
+ log("[PackAgent] Restart requested");
1515
+ return this.options.lifecycleHandler.requestRestart(
1516
+ getLifecycleTrigger(channelId)
1517
+ );
1518
+ case "shutdown":
1519
+ log("[PackAgent] Shutdown requested");
1520
+ return this.options.lifecycleHandler.requestShutdown(
1521
+ getLifecycleTrigger(channelId)
1522
+ );
1523
+ default:
1524
+ return { success: false, message: `Unknown command: ${command}` };
654
1525
  }
655
1526
  }
656
- const config = await readConfigSource(options.config);
657
- saveConfig(workDir, config);
658
- console.log(chalk4.blue(`
659
- Initialize ${config.name} from ${options.config}
660
- `));
661
- installConfiguredSkills(workDir, config);
662
- refreshDescriptionsAndSave(workDir, config);
663
- copyRuntimeTemplate(getRuntimeDir(), workDir);
664
- ensureRuntimeLaunchersExecutable(workDir);
665
- if (options.bundle) {
666
- await bundle(workDir);
1527
+ abort(channelId) {
1528
+ const cs = this.channels.get(channelId);
1529
+ if (cs?.running) {
1530
+ cs.session.abort?.();
1531
+ }
667
1532
  }
668
- console.log(chalk4.green(`
669
- ${PACK_FILE} saved
670
- `));
671
- console.log(chalk4.green(" Runtime template expanded.\n"));
672
- console.log(chalk4.green(" Initialization complete.\n"));
673
- if (!options.bundle) {
674
- console.log(
675
- chalk4.dim(" Run npx @cremini/skillpack build to create the zip when needed\n")
676
- );
1533
+ isRunning(channelId) {
1534
+ return this.channels.get(channelId)?.running ?? false;
677
1535
  }
678
- }
1536
+ dispose(channelId) {
1537
+ const cs = this.channels.get(channelId);
1538
+ if (cs) {
1539
+ cs.session.dispose();
1540
+ this.channels.delete(channelId);
1541
+ }
1542
+ }
1543
+ /** Reserved: list all sessions */
1544
+ listSessions() {
1545
+ return [];
1546
+ }
1547
+ /** Reserved: restore a historical session */
1548
+ async restoreSession(_sessionId) {
1549
+ }
1550
+ };
679
1551
 
680
- // src/commands/skills-cmd.ts
681
- import chalk5 from "chalk";
682
- function registerSkillsCommand(program2) {
683
- const skills = program2.command("skills").description("Manage skills in the app");
684
- skills.command("add <source>").description("Add a skill from a git repo, URL, or local path").option("-s, --skill <names...>", "Specify skill name(s)").action(async (source, opts) => {
685
- if (!opts.skill || opts.skill.length === 0) {
686
- console.log(
687
- chalk5.red(
688
- "Specify at least one skill name with --skill when adding a source"
689
- )
690
- );
691
- process.exitCode = 1;
692
- return;
1552
+ // src/runtime/adapters/web.ts
1553
+ import fs7 from "fs";
1554
+ import path7 from "path";
1555
+ import { WebSocketServer } from "ws";
1556
+
1557
+ // src/runtime/config.ts
1558
+ import fs6 from "fs";
1559
+ import path6 from "path";
1560
+ var ConfigManager = class _ConfigManager {
1561
+ static instance;
1562
+ configData = {};
1563
+ configPath = "";
1564
+ constructor() {
1565
+ }
1566
+ static getInstance() {
1567
+ if (!_ConfigManager.instance) {
1568
+ _ConfigManager.instance = new _ConfigManager();
1569
+ }
1570
+ return _ConfigManager.instance;
1571
+ }
1572
+ load(rootDir) {
1573
+ this.configPath = path6.join(rootDir, "data", "config.json");
1574
+ if (fs6.existsSync(this.configPath)) {
1575
+ try {
1576
+ this.configData = JSON.parse(fs6.readFileSync(this.configPath, "utf-8"));
1577
+ console.log(" Loaded config from data/config.json");
1578
+ } catch (err) {
1579
+ console.warn(" Warning: Failed to parse data/config.json:", err);
1580
+ }
1581
+ }
1582
+ let { apiKey = "", provider = "openai" } = this.configData;
1583
+ if (!apiKey) {
1584
+ if (process.env.OPENAI_API_KEY) {
1585
+ apiKey = process.env.OPENAI_API_KEY;
1586
+ provider = "openai";
1587
+ } else if (process.env.ANTHROPIC_API_KEY) {
1588
+ apiKey = process.env.ANTHROPIC_API_KEY;
1589
+ provider = "anthropic";
1590
+ }
1591
+ }
1592
+ this.configData.apiKey = apiKey;
1593
+ this.configData.provider = provider;
1594
+ return this.configData;
1595
+ }
1596
+ getConfig() {
1597
+ return this.configData;
1598
+ }
1599
+ save(rootDir, updates) {
1600
+ const configDir = path6.join(rootDir, "data");
1601
+ if (!this.configPath) {
1602
+ this.configPath = path6.join(rootDir, "data", "config.json");
1603
+ }
1604
+ if (!fs6.existsSync(configDir)) {
1605
+ fs6.mkdirSync(configDir, { recursive: true });
1606
+ }
1607
+ if (updates.apiKey !== void 0) this.configData.apiKey = updates.apiKey;
1608
+ if (updates.provider !== void 0) this.configData.provider = updates.provider;
1609
+ if (updates.adapters !== void 0) {
1610
+ const merged = { ...this.configData.adapters || {} };
1611
+ for (const [adapterKey, adapterVal] of Object.entries(updates.adapters)) {
1612
+ if (adapterVal === null || adapterVal === void 0) {
1613
+ delete merged[adapterKey];
1614
+ } else {
1615
+ merged[adapterKey] = adapterVal;
1616
+ }
1617
+ }
1618
+ this.configData.adapters = merged;
693
1619
  }
694
- const workDir = process.cwd();
695
- const config = loadConfig(workDir);
696
- const requestedSkills = opts.skill.map((name) => ({
697
- name: name.trim(),
698
- source,
699
- description: ""
700
- }));
701
- upsertSkills(config, requestedSkills);
702
- saveConfig(workDir, config);
703
1620
  try {
704
- installSkills(workDir, requestedSkills);
705
- refreshDescriptionsAndSave(workDir, config);
706
- } catch (error) {
707
- console.log(
708
- chalk5.red(
709
- `Skill installation failed: ${error instanceof Error ? error.message : String(error)}`
710
- )
1621
+ fs6.writeFileSync(
1622
+ this.configPath,
1623
+ JSON.stringify(this.configData, null, 2),
1624
+ "utf-8"
711
1625
  );
712
- process.exitCode = 1;
713
- return;
1626
+ } catch (err) {
1627
+ console.error("Failed to save config:", err);
714
1628
  }
715
- console.log(chalk5.green(`Installed ${requestedSkills.length} skill(s).`));
716
- });
717
- skills.command("remove <name>").description("Remove a skill").action((name) => {
718
- removeSkill(process.cwd(), name);
1629
+ }
1630
+ };
1631
+ var configManager = ConfigManager.getInstance();
1632
+
1633
+ // src/runtime/adapters/web.ts
1634
+ function getPackConfig(rootDir) {
1635
+ const raw = fs7.readFileSync(path7.join(rootDir, "skillpack.json"), "utf-8");
1636
+ return JSON.parse(raw);
1637
+ }
1638
+ var COMMANDS = {
1639
+ "/new": "new",
1640
+ "/clear": "clear",
1641
+ "/restart": "restart",
1642
+ "/shutdown": "shutdown"
1643
+ };
1644
+ function parseCommand(text) {
1645
+ const trimmed = text.trim().toLowerCase();
1646
+ return COMMANDS[trimmed] ?? null;
1647
+ }
1648
+ function getRuntimeConfigSignature(config) {
1649
+ return JSON.stringify({
1650
+ apiKey: config.apiKey || "",
1651
+ provider: config.provider || "openai",
1652
+ telegramToken: config.adapters?.telegram?.token || "",
1653
+ slackBotToken: config.adapters?.slack?.botToken || "",
1654
+ slackAppToken: config.adapters?.slack?.appToken || ""
719
1655
  });
720
- skills.command("list").description("List installed skills").action(() => {
721
- const config = loadConfig(process.cwd());
722
- if (config.skills.length === 0) {
723
- console.log(chalk5.dim(" No skills installed"));
1656
+ }
1657
+ var WebAdapter = class {
1658
+ name = "web";
1659
+ wss = null;
1660
+ agent = null;
1661
+ async start(ctx) {
1662
+ const { agent, server, app, rootDir, lifecycle } = ctx;
1663
+ this.agent = agent;
1664
+ const currentConf = configManager.getConfig();
1665
+ let apiKey = currentConf.apiKey || "";
1666
+ let currentProvider = currentConf.provider || "openai";
1667
+ app.get("/api/config", (_req, res) => {
1668
+ const config = getPackConfig(rootDir);
1669
+ const conf = configManager.getConfig();
1670
+ res.json({
1671
+ name: config.name,
1672
+ description: config.description,
1673
+ prompts: config.prompts || [],
1674
+ skills: config.skills || [],
1675
+ hasApiKey: !!conf.apiKey,
1676
+ apiKey: conf.apiKey || "",
1677
+ provider: conf.provider || "openai",
1678
+ adapters: conf.adapters || {},
1679
+ runtimeControl: lifecycle.getRuntimeControl()
1680
+ });
1681
+ });
1682
+ app.get("/api/skills", (_req, res) => {
1683
+ const config = getPackConfig(rootDir);
1684
+ res.json(config.skills || []);
1685
+ });
1686
+ app.post("/api/config/update", (req, res) => {
1687
+ const { key, provider, adapters } = req.body;
1688
+ const updates = {};
1689
+ const beforeConfig = JSON.parse(JSON.stringify(configManager.getConfig()));
1690
+ if (key !== void 0) {
1691
+ updates.apiKey = key;
1692
+ apiKey = key;
1693
+ }
1694
+ if (provider !== void 0) {
1695
+ updates.provider = provider;
1696
+ currentProvider = provider;
1697
+ }
1698
+ if (adapters !== void 0) {
1699
+ updates.adapters = adapters;
1700
+ }
1701
+ configManager.save(rootDir, updates);
1702
+ const newConf = configManager.getConfig();
1703
+ const requiresRestart = getRuntimeConfigSignature(beforeConfig) !== getRuntimeConfigSignature(newConf);
1704
+ res.json({
1705
+ success: true,
1706
+ provider: newConf.provider,
1707
+ adapters: newConf.adapters,
1708
+ requiresRestart,
1709
+ runtimeControl: lifecycle.getRuntimeControl()
1710
+ });
1711
+ });
1712
+ app.post("/api/runtime/restart", async (_req, res) => {
1713
+ const runtimeControl = lifecycle.getRuntimeControl();
1714
+ if (!runtimeControl.canManagedRestart) {
1715
+ res.status(409).json({
1716
+ success: false,
1717
+ message: "Managed restart is unavailable for this process.",
1718
+ runtimeControl
1719
+ });
1720
+ return;
1721
+ }
1722
+ const result = await lifecycle.requestRestart("web");
1723
+ res.status(202).json({ ...result, runtimeControl });
1724
+ });
1725
+ app.delete("/api/chat", (_req, res) => {
1726
+ res.json({ success: true });
1727
+ });
1728
+ app.get("/api/sessions", (_req, res) => {
1729
+ const sessions = agent.listSessions();
1730
+ res.json(sessions);
1731
+ });
1732
+ app.get("/api/sessions/:id", (_req, res) => {
1733
+ res.status(501).json({ error: "Not implemented yet" });
1734
+ });
1735
+ this.wss = new WebSocketServer({ noServer: true });
1736
+ server.on("upgrade", (request, socket, head) => {
1737
+ if (request.url?.startsWith("/api/chat")) {
1738
+ this.wss.handleUpgrade(request, socket, head, (ws) => {
1739
+ this.wss.emit("connection", ws, request);
1740
+ });
1741
+ } else {
1742
+ socket.destroy();
1743
+ }
1744
+ });
1745
+ this.wss.on("connection", (ws, request) => {
1746
+ const url = new URL(
1747
+ request.url ?? "/",
1748
+ `http://${request.headers.host || "127.0.0.1"}`
1749
+ );
1750
+ const _reqProvider = url.searchParams.get("provider") || currentProvider;
1751
+ if (!apiKey) {
1752
+ ws.send(JSON.stringify({ error: "Please set an API key first" }));
1753
+ ws.close();
1754
+ return;
1755
+ }
1756
+ const channelId = `web-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1757
+ this.handleWsConnection(ws, channelId, agent);
1758
+ });
1759
+ console.log("[WebAdapter] Started");
1760
+ }
1761
+ async stop() {
1762
+ if (this.wss) {
1763
+ for (const client of this.wss.clients) {
1764
+ client.close();
1765
+ }
1766
+ this.wss.close();
1767
+ this.wss = null;
1768
+ }
1769
+ console.log("[WebAdapter] Stopped");
1770
+ }
1771
+ // -------------------------------------------------------------------------
1772
+ // WebSocket message handler
1773
+ // -------------------------------------------------------------------------
1774
+ handleWsConnection(ws, channelId, agent) {
1775
+ ws.on("message", async (data) => {
1776
+ try {
1777
+ const payload = JSON.parse(data.toString());
1778
+ if (!payload.text) return;
1779
+ const text = payload.text;
1780
+ const command = parseCommand(text);
1781
+ if (command) {
1782
+ const result2 = await agent.handleCommand(command, channelId);
1783
+ ws.send(
1784
+ JSON.stringify({
1785
+ type: "command_result",
1786
+ command,
1787
+ ...result2
1788
+ })
1789
+ );
1790
+ if (command === "clear" || command === "new") {
1791
+ ws.send(JSON.stringify({ done: true }));
1792
+ }
1793
+ return;
1794
+ }
1795
+ const onEvent = (event) => {
1796
+ if (ws.readyState !== ws.OPEN) return;
1797
+ ws.send(JSON.stringify(event));
1798
+ };
1799
+ const result = await agent.handleMessage(channelId, text, onEvent);
1800
+ if (result.errorMessage) {
1801
+ ws.send(JSON.stringify({ error: result.errorMessage }));
1802
+ return;
1803
+ }
1804
+ ws.send(JSON.stringify({ done: true }));
1805
+ } catch (err) {
1806
+ ws.send(JSON.stringify({ error: String(err) }));
1807
+ }
1808
+ });
1809
+ ws.on("close", () => {
1810
+ agent.dispose(channelId);
1811
+ });
1812
+ }
1813
+ };
1814
+
1815
+ // src/runtime/lifecycle.ts
1816
+ var SHUTDOWN_EXIT_CODE = 64;
1817
+ var RESTART_EXIT_CODE = 75;
1818
+ var STOP_TIMEOUT_MS = 3e3;
1819
+ function detectProcessManager() {
1820
+ return process.env.PACK_ROOT ? "wrapper" : "none";
1821
+ }
1822
+ var Lifecycle = class {
1823
+ server;
1824
+ exitFn;
1825
+ processManager;
1826
+ adapters = [];
1827
+ stopReason = null;
1828
+ constructor(server, exitFn = (code) => process.exit(code)) {
1829
+ this.server = server;
1830
+ this.exitFn = exitFn;
1831
+ this.processManager = detectProcessManager();
1832
+ }
1833
+ registerAdapters(adapters) {
1834
+ this.adapters = adapters;
1835
+ }
1836
+ getRuntimeControl() {
1837
+ return {
1838
+ canManagedRestart: this.processManager === "wrapper",
1839
+ processManager: this.processManager
1840
+ };
1841
+ }
1842
+ async requestRestart(trigger) {
1843
+ return this.requestStop("restart", trigger);
1844
+ }
1845
+ async requestShutdown(trigger) {
1846
+ return this.requestStop("shutdown", trigger);
1847
+ }
1848
+ async requestStop(reason, trigger) {
1849
+ if (this.stopReason) {
1850
+ const message = this.stopReason === "restart" ? "Restart already in progress." : "Shutdown already in progress.";
1851
+ return { success: true, message };
1852
+ }
1853
+ this.stopReason = reason;
1854
+ console.log(`[Lifecycle] ${reason} requested via ${trigger}`);
1855
+ setTimeout(() => {
1856
+ void this.gracefulStopAndExit(reason);
1857
+ }, 50);
1858
+ return {
1859
+ success: true,
1860
+ message: reason === "restart" ? "Restarting..." : "Shutting down..."
1861
+ };
1862
+ }
1863
+ async gracefulStopAndExit(reason) {
1864
+ try {
1865
+ await Promise.race([
1866
+ this.gracefulStop(),
1867
+ new Promise((resolve) => {
1868
+ setTimeout(() => {
1869
+ console.warn("[Lifecycle] Graceful stop timed out, forcing exit.");
1870
+ resolve();
1871
+ }, STOP_TIMEOUT_MS);
1872
+ })
1873
+ ]);
1874
+ } catch (err) {
1875
+ console.error("[Lifecycle] Error during graceful stop:", err);
1876
+ }
1877
+ const exitCode = reason === "restart" ? RESTART_EXIT_CODE : SHUTDOWN_EXIT_CODE;
1878
+ this.exitFn(exitCode);
1879
+ }
1880
+ async gracefulStop() {
1881
+ for (const adapter of [...this.adapters].reverse()) {
1882
+ try {
1883
+ await adapter.stop();
1884
+ } catch (err) {
1885
+ console.error(`[Lifecycle] Failed to stop ${adapter.name}:`, err);
1886
+ }
1887
+ }
1888
+ if (!this.server.listening) {
724
1889
  return;
725
1890
  }
726
- console.log(chalk5.blue(`
727
- ${config.name} Skills:
728
- `));
729
- for (const skill of config.skills) {
730
- console.log(` ${chalk5.green("\u25CF")} ${chalk5.bold(skill.name)}`);
731
- if (skill.description) {
732
- console.log(` ${chalk5.dim(skill.description)}`);
1891
+ await new Promise((resolve) => {
1892
+ this.server.close(() => resolve());
1893
+ });
1894
+ }
1895
+ };
1896
+
1897
+ // src/runtime/server.ts
1898
+ var __dirname = path8.dirname(fileURLToPath(import.meta.url));
1899
+ async function startServer(options) {
1900
+ const {
1901
+ rootDir,
1902
+ host = process.env.HOST || "127.0.0.1",
1903
+ port = Number(process.env.PORT) || 26313,
1904
+ firstRun = true
1905
+ } = options;
1906
+ const dataConfig = configManager.load(rootDir);
1907
+ const apiKey = dataConfig.apiKey || "";
1908
+ const provider = dataConfig.provider || "openai";
1909
+ const modelId = provider === "anthropic" ? "claude-opus-4-6" : "gpt-5.4";
1910
+ const packageRoot = path8.resolve(__dirname, "..");
1911
+ const webDir = fs8.existsSync(path8.join(rootDir, "web")) ? path8.join(rootDir, "web") : path8.join(packageRoot, "web");
1912
+ const app = express();
1913
+ app.use(express.json());
1914
+ app.use(express.static(webDir));
1915
+ const server = createServer(app);
1916
+ const lifecycle = new Lifecycle(server);
1917
+ const agent = new PackAgent({
1918
+ apiKey,
1919
+ rootDir,
1920
+ provider,
1921
+ modelId,
1922
+ lifecycleHandler: lifecycle
1923
+ });
1924
+ const adapters = [];
1925
+ const webAdapter = new WebAdapter();
1926
+ await webAdapter.start({ agent, server, app, rootDir, lifecycle });
1927
+ adapters.push(webAdapter);
1928
+ if (dataConfig.adapters?.telegram?.token) {
1929
+ try {
1930
+ const { TelegramAdapter: TelegramAdapter2 } = await Promise.resolve().then(() => (init_telegram(), telegram_exports));
1931
+ const telegramAdapter = new TelegramAdapter2({
1932
+ token: dataConfig.adapters.telegram.token
1933
+ });
1934
+ await telegramAdapter.start({ agent, server, app, rootDir, lifecycle });
1935
+ adapters.push(telegramAdapter);
1936
+ } catch (err) {
1937
+ console.error("[Telegram] Failed to start:", err);
1938
+ }
1939
+ }
1940
+ const slackConfig = dataConfig.adapters?.slack;
1941
+ if (slackConfig?.botToken || slackConfig?.appToken) {
1942
+ if (!slackConfig.botToken || !slackConfig.appToken) {
1943
+ console.warn(
1944
+ "[Slack] Skipped: both adapters.slack.botToken and adapters.slack.appToken are required."
1945
+ );
1946
+ } else {
1947
+ try {
1948
+ const { SlackAdapter: SlackAdapter2 } = await Promise.resolve().then(() => (init_slack(), slack_exports));
1949
+ const slackAdapter = new SlackAdapter2({
1950
+ botToken: slackConfig.botToken,
1951
+ appToken: slackConfig.appToken
1952
+ });
1953
+ await slackAdapter.start({ agent, server, app, rootDir, lifecycle });
1954
+ adapters.push(slackAdapter);
1955
+ } catch (err) {
1956
+ console.error("[Slack] Failed to start:", err);
733
1957
  }
734
1958
  }
735
- console.log();
1959
+ }
1960
+ lifecycle.registerAdapters(adapters);
1961
+ server.once("listening", () => {
1962
+ const address = server.address();
1963
+ const actualPort = typeof address === "string" ? address : address?.port;
1964
+ const url = `http://${host}:${actualPort}`;
1965
+ console.log(`
1966
+ Skills Pack Server`);
1967
+ console.log(` Running at ${url}
1968
+ `);
1969
+ if (firstRun) {
1970
+ const cmd = process.platform === "darwin" ? `open ${url}` : process.platform === "win32" ? `start ${url}` : `xdg-open ${url}`;
1971
+ exec(cmd, (err) => {
1972
+ if (err) console.warn(` Could not open browser: ${err.message}`);
1973
+ });
1974
+ }
1975
+ });
1976
+ process.on("SIGINT", () => {
1977
+ void lifecycle.requestShutdown("signal");
1978
+ });
1979
+ process.on("SIGTERM", () => {
1980
+ void lifecycle.requestShutdown("signal");
1981
+ });
1982
+ await new Promise((resolve, reject) => {
1983
+ function tryListen(listenPort) {
1984
+ server.listen(listenPort, host);
1985
+ server.once("error", (err) => {
1986
+ if (err.code === "EADDRINUSE") {
1987
+ console.log(` Port ${listenPort} is in use, trying ${listenPort + 1}...`);
1988
+ server.close();
1989
+ tryListen(listenPort + 1);
1990
+ } else {
1991
+ reject(err);
1992
+ }
1993
+ });
1994
+ server.once("listening", () => resolve());
1995
+ }
1996
+ tryListen(port);
1997
+ });
1998
+ await new Promise(() => {
736
1999
  });
737
2000
  }
738
2001
 
739
- // src/commands/prompts-cmd.ts
740
- import chalk7 from "chalk";
741
-
742
- // src/core/prompts.ts
743
- import chalk6 from "chalk";
744
- function addPrompt(workDir, text) {
745
- const config = loadConfig(workDir);
746
- config.prompts.push(text);
747
- saveConfig(workDir, config);
748
- console.log(chalk6.green(`Added Prompt #${config.prompts.length}`));
2002
+ // src/commands/run.ts
2003
+ function findMissingSkills(workDir, config) {
2004
+ const installed = scanInstalledSkills(workDir);
2005
+ const installedNames = new Set(
2006
+ installed.map((s) => s.name.trim().toLowerCase())
2007
+ );
2008
+ return config.skills.filter((skill) => {
2009
+ if (skill.source.startsWith("./skills")) return false;
2010
+ return !installedNames.has(skill.name.trim().toLowerCase());
2011
+ });
749
2012
  }
750
- function removePrompt(workDir, index) {
751
- const config = loadConfig(workDir);
752
- const idx = index - 1;
753
- if (idx < 0 || idx >= config.prompts.length) {
754
- console.log(
755
- chalk6.yellow(`Invalid index: ${index} (${config.prompts.length} total)`)
756
- );
757
- return false;
758
- }
759
- const removed = config.prompts.splice(idx, 1)[0];
760
- saveConfig(workDir, config);
761
- console.log(
762
- chalk6.green(`Removed Prompt #${index}: "${removed.substring(0, 50)}..."`)
2013
+ function copyStartTemplates2(workDir) {
2014
+ const templateDir = path9.resolve(
2015
+ new URL("../templates", import.meta.url).pathname
763
2016
  );
764
- return true;
2017
+ for (const file of ["start.sh", "start.bat"]) {
2018
+ const src = path9.join(templateDir, file);
2019
+ const dest = path9.join(workDir, file);
2020
+ if (fs9.existsSync(src)) {
2021
+ fs9.copyFileSync(src, dest);
2022
+ if (file === "start.sh") {
2023
+ fs9.chmodSync(dest, 493);
2024
+ }
2025
+ }
2026
+ }
765
2027
  }
766
- function listPrompts(workDir) {
2028
+ async function runCommand(directory) {
2029
+ const workDir = directory ? path9.resolve(directory) : process.cwd();
2030
+ fs9.mkdirSync(workDir, { recursive: true });
2031
+ if (!configExists(workDir)) {
2032
+ console.log(chalk4.blue("\n No skillpack.json found. Let's set one up.\n"));
2033
+ const { name, description } = await inquirer2.prompt([
2034
+ {
2035
+ type: "input",
2036
+ name: "name",
2037
+ message: "App name:",
2038
+ validate: (v) => v.trim() ? true : "Name is required"
2039
+ },
2040
+ {
2041
+ type: "input",
2042
+ name: "description",
2043
+ message: "Description:",
2044
+ default: "A skill App, powered by SkillPack.sh"
2045
+ }
2046
+ ]);
2047
+ const config2 = createDefaultConfig(name.trim(), description.trim());
2048
+ saveConfig(workDir, config2);
2049
+ copyStartTemplates2(workDir);
2050
+ console.log(chalk4.green(`
2051
+ skillpack.json created
2052
+ `));
2053
+ }
767
2054
  const config = loadConfig(workDir);
768
- return config.prompts;
769
- }
770
-
771
- // src/commands/prompts-cmd.ts
772
- function registerPromptsCommand(program2) {
773
- const prompts = program2.command("prompts").description("Manage prompts");
774
- prompts.command("add <text>").description("Add a prompt").action((text) => {
775
- addPrompt(process.cwd(), text);
776
- });
777
- prompts.command("remove <index>").description("Remove a prompt by number, starting from 1").action((index) => {
778
- const num = parseInt(index, 10);
779
- if (isNaN(num)) {
780
- console.log(chalk7.red("Enter a valid numeric index"));
781
- return;
782
- }
783
- removePrompt(process.cwd(), num);
784
- });
785
- prompts.command("list").description("List all prompts").action(() => {
786
- const prompts2 = listPrompts(process.cwd());
787
- if (prompts2.length === 0) {
788
- console.log(chalk7.dim(" No prompts yet"));
789
- return;
2055
+ const missing = findMissingSkills(workDir, config);
2056
+ if (missing.length > 0) {
2057
+ console.log(chalk4.blue(`
2058
+ Installing ${missing.length} missing skill(s)...
2059
+ `));
2060
+ try {
2061
+ installSkills(workDir, missing);
2062
+ } catch (err) {
2063
+ console.warn(chalk4.yellow(` Warning: Some skills could not be installed: ${err}`));
790
2064
  }
791
- console.log(chalk7.blue("\n Prompts:\n"));
792
- prompts2.forEach((u, i) => {
793
- const marker = i === 0 ? chalk7.green("\u2605") : chalk7.dim("\u25CF");
794
- const label = i === 0 ? chalk7.dim(" (default)") : "";
795
- const display = u.length > 80 ? u.substring(0, 80) + "..." : u;
796
- console.log(` ${marker} #${i + 1}${label} ${display}`);
797
- });
798
- console.log();
799
- });
2065
+ }
2066
+ await startServer({ rootDir: workDir, firstRun: true });
800
2067
  }
801
2068
 
802
2069
  // src/cli.ts
803
- import fs7 from "fs";
2070
+ import fs10 from "fs";
804
2071
  var packageJson = JSON.parse(
805
- fs7.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
2072
+ fs10.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
806
2073
  );
807
2074
  var program = new Command();
808
2075
  program.name("skillpack").description("Assemble, package, and run Agent Skills packs").version(packageJson.version);
809
- program.command("create [directory]").description("Create a skills pack interactively").action(async (directory) => {
810
- await createCommand(directory);
2076
+ program.command("create [directory]").description("Create a skills pack interactively").option("--config <path-or-url>", "Initialize from a local or remote skillpack.json").action(async (directory, options) => {
2077
+ await createCommand(directory, options);
811
2078
  });
812
- program.command("init [directory]").description(
813
- "Initialize a skills pack from a local config file or URL and expand runtime files"
814
- ).requiredOption("--config <path-or-url>", "Path or URL to a skillpack.json file").option("--bundle", "Bundle as a zip after initialization").action(
815
- async (directory, options) => {
816
- await initCommand(directory, options);
817
- }
818
- );
819
- registerSkillsCommand(program);
820
- registerPromptsCommand(program);
821
- program.command("build").description("Package the current pack as a zip file").action(async () => {
2079
+ program.command("run [directory]").description("Start the SkillPack runtime server").option("--port <port>", "Port to listen on").option("--host <host>", "Host to bind to").action(async (directory, options) => {
2080
+ if (options?.port) process.env.PORT = options.port;
2081
+ if (options?.host) process.env.HOST = options.host;
2082
+ await runCommand(directory);
2083
+ });
2084
+ program.command("zip").description("Package the current pack as a zip file (skillpack.json + skills/ + start scripts)").action(async () => {
822
2085
  try {
823
- await bundle(process.cwd());
2086
+ await zipCommand(process.cwd());
824
2087
  } catch (err) {
825
- console.error(chalk8.red(`Packaging failed: ${err}`));
2088
+ console.error(chalk5.red(`Packaging failed: ${err}`));
826
2089
  process.exit(1);
827
2090
  }
828
2091
  });