@cremini/skillpack 1.1.3 → 1.1.4-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1602 -339
- package/package.json +17 -9
- package/templates/start.bat +3 -0
- package/templates/start.sh +3 -0
- package/runtime/README.md +0 -51
- package/runtime/server/dist/adapters/markdown.js +0 -74
- package/runtime/server/dist/adapters/markdown.js.map +0 -1
- package/runtime/server/dist/adapters/slack.js +0 -369
- package/runtime/server/dist/adapters/slack.js.map +0 -1
- package/runtime/server/dist/adapters/telegram.js +0 -199
- package/runtime/server/dist/adapters/telegram.js.map +0 -1
- package/runtime/server/dist/adapters/types.js +0 -2
- package/runtime/server/dist/adapters/types.js.map +0 -1
- package/runtime/server/dist/adapters/web.js +0 -201
- package/runtime/server/dist/adapters/web.js.map +0 -1
- package/runtime/server/dist/agent.js +0 -245
- package/runtime/server/dist/agent.js.map +0 -1
- package/runtime/server/dist/config.js +0 -79
- package/runtime/server/dist/config.js.map +0 -1
- package/runtime/server/dist/index.js +0 -146
- package/runtime/server/dist/index.js.map +0 -1
- package/runtime/server/dist/lifecycle.js +0 -85
- package/runtime/server/dist/lifecycle.js.map +0 -1
- package/runtime/server/dist/memory.js +0 -195
- package/runtime/server/dist/memory.js.map +0 -1
- package/runtime/server/package-lock.json +0 -8433
- package/runtime/server/package.json +0 -23
- package/runtime/start.bat +0 -51
- package/runtime/start.sh +0 -50
- /package/{runtime/web → web}/index.html +0 -0
- /package/{runtime/web → web}/js/api-key-dialog.js +0 -0
- /package/{runtime/web → web}/js/api.js +0 -0
- /package/{runtime/web → web}/js/chat-apps-dialog.js +0 -0
- /package/{runtime/web → web}/js/chat.js +0 -0
- /package/{runtime/web → web}/js/config.js +0 -0
- /package/{runtime/web → web}/js/main.js +0 -0
- /package/{runtime/web → web}/js/settings.js +0 -0
- /package/{runtime/web → web}/marked.min.js +0 -0
- /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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
21
|
+
}
|
|
22
|
+
function escapeHtmlAttribute(text) {
|
|
23
|
+
return escapeHtml(text).replace(/"/g, """);
|
|
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
|
|
706
|
+
import chalk5 from "chalk";
|
|
6
707
|
|
|
7
708
|
// src/commands/create.ts
|
|
8
|
-
import
|
|
9
|
-
import
|
|
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/
|
|
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/
|
|
117
|
-
import
|
|
118
|
-
import
|
|
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/
|
|
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/
|
|
316
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
445
|
-
if (
|
|
1013
|
+
const skillsDir = path3.join(workDir, "skills");
|
|
1014
|
+
if (fs3.existsSync(skillsDir)) {
|
|
446
1015
|
archive.directory(skillsDir, `${prefix}/skills`);
|
|
447
1016
|
}
|
|
448
|
-
|
|
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
|
-
|
|
477
|
-
|
|
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
|
-
|
|
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} (
|
|
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 (!
|
|
1231
|
+
if (!prompt.trim()) {
|
|
576
1232
|
break;
|
|
577
1233
|
}
|
|
578
1234
|
config.prompts.push(prompt.trim());
|
|
579
1235
|
promptIndex++;
|
|
580
1236
|
}
|
|
581
|
-
const {
|
|
1237
|
+
const { shouldZip } = await inquirer.prompt([
|
|
582
1238
|
{
|
|
583
1239
|
type: "confirm",
|
|
584
|
-
name: "
|
|
585
|
-
message: "
|
|
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 (
|
|
598
|
-
await
|
|
1255
|
+
if (shouldZip) {
|
|
1256
|
+
await zipCommand(workDir);
|
|
599
1257
|
}
|
|
600
1258
|
console.log(chalk3.green("\n Done!"));
|
|
601
|
-
if (!
|
|
1259
|
+
if (!shouldZip) {
|
|
602
1260
|
console.log(
|
|
603
|
-
chalk3.dim(
|
|
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/
|
|
609
|
-
import
|
|
610
|
-
import
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
669
|
-
|
|
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/
|
|
681
|
-
import
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
713
|
-
|
|
1626
|
+
} catch (err) {
|
|
1627
|
+
console.error("Failed to save config:", err);
|
|
714
1628
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
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/
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
|
751
|
-
const
|
|
752
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
792
|
-
|
|
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
|
|
2070
|
+
import fs10 from "fs";
|
|
804
2071
|
var packageJson = JSON.parse(
|
|
805
|
-
|
|
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("
|
|
813
|
-
|
|
814
|
-
)
|
|
815
|
-
|
|
816
|
-
|
|
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
|
|
2086
|
+
await zipCommand(process.cwd());
|
|
824
2087
|
} catch (err) {
|
|
825
|
-
console.error(
|
|
2088
|
+
console.error(chalk5.red(`Packaging failed: ${err}`));
|
|
826
2089
|
process.exit(1);
|
|
827
2090
|
}
|
|
828
2091
|
});
|