@cremini/skillpack 1.0.8 → 1.0.9-im.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/README.md +3 -1
- package/dist/cli.js +45 -11
- package/package.json +3 -2
- package/runtime/server/chat-proxy.js +68 -0
- package/runtime/server/dist/adapters/telegram.js +200 -0
- package/runtime/server/dist/adapters/types.js +1 -0
- package/runtime/server/dist/adapters/web.js +176 -0
- package/runtime/server/dist/agent.js +218 -0
- package/runtime/server/dist/index.js +116 -0
- package/runtime/server/package-lock.json +2311 -41
- package/runtime/server/package.json +11 -2
- package/runtime/start.bat +2 -2
- package/runtime/start.sh +1 -1
- package/runtime/web/app.js +17 -3
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# SkillPack.sh - Pack AI Skills into Standalone Apps
|
|
2
2
|
|
|
3
|
+
Skillpack by Cremini is built on the idea of distributed intelligence, much like cremini mushrooms that grow from a vast, interconnected mycelial network.
|
|
4
|
+
|
|
3
5
|
Go to [skillpack.sh](https://skillpack.sh) to pack skills and try existing skill packs.
|
|
4
6
|
|
|
5
|
-
One command to orchestrate [Skills](https://skills.sh), tools, mcps into a standalone app users can download and use on their own computer!
|
|
7
|
+
One command to orchestrate [Skills](https://skills.sh), tools, mcps into a standalone app users can download and use on their own computer to address a well-defined problem or complete specific tasks!
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
10
|
npx @cremini/skillpack create
|
package/dist/cli.js
CHANGED
|
@@ -342,6 +342,10 @@ function collectRuntimeTemplateEntries(runtimeDir) {
|
|
|
342
342
|
if (dirEntry.name === "node_modules") {
|
|
343
343
|
continue;
|
|
344
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
|
+
}
|
|
345
349
|
const absolutePath = path3.join(currentDir, dirEntry.name);
|
|
346
350
|
const relativePath = relativeDir ? path3.posix.join(relativeDir, dirEntry.name) : dirEntry.name;
|
|
347
351
|
const stats = fs3.statSync(absolutePath);
|
|
@@ -450,6 +454,25 @@ async function bundle(workDir) {
|
|
|
450
454
|
function parseSkillNames(value) {
|
|
451
455
|
return value.split(",").map((name) => name.trim()).filter(Boolean);
|
|
452
456
|
}
|
|
457
|
+
function normalizeSourceInput(value) {
|
|
458
|
+
return value.trim().replace(/^npx\s+skills\s+add\s+/u, "");
|
|
459
|
+
}
|
|
460
|
+
function parseSourceInput(value) {
|
|
461
|
+
const trimmedValue = normalizeSourceInput(value);
|
|
462
|
+
const skillFlagIndex = trimmedValue.indexOf(" --skill ");
|
|
463
|
+
if (skillFlagIndex === -1) {
|
|
464
|
+
return {
|
|
465
|
+
source: trimmedValue,
|
|
466
|
+
inlineSkillNames: []
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const source = trimmedValue.slice(0, skillFlagIndex).trim();
|
|
470
|
+
const inlineSkillValue = trimmedValue.slice(skillFlagIndex + " --skill ".length).trim();
|
|
471
|
+
return {
|
|
472
|
+
source,
|
|
473
|
+
inlineSkillNames: inlineSkillValue.split(/[,\s]+/).map((name) => name.trim()).filter(Boolean)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
453
476
|
async function createCommand(directory) {
|
|
454
477
|
const workDir = directory ? path5.resolve(directory) : process.cwd();
|
|
455
478
|
if (directory) {
|
|
@@ -493,7 +516,10 @@ async function createCommand(directory) {
|
|
|
493
516
|
chalk3.dim(" Supported formats: owner/repo, GitHub URL, or local path")
|
|
494
517
|
);
|
|
495
518
|
console.log(chalk3.dim(" Example source: vercel-labs/agent-skills"));
|
|
496
|
-
console.log(
|
|
519
|
+
console.log(
|
|
520
|
+
chalk3.dim(" Example inline skill: vercel-labs/agent-skills --skill find-skills")
|
|
521
|
+
);
|
|
522
|
+
console.log();
|
|
497
523
|
while (true) {
|
|
498
524
|
const { source } = await inquirer.prompt([
|
|
499
525
|
{
|
|
@@ -505,16 +531,24 @@ async function createCommand(directory) {
|
|
|
505
531
|
if (!source.trim()) {
|
|
506
532
|
break;
|
|
507
533
|
}
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
534
|
+
const parsedSource = parseSourceInput(source);
|
|
535
|
+
let skillNames = parsedSource.inlineSkillNames;
|
|
536
|
+
if (skillNames.length === 0) {
|
|
537
|
+
console.log(
|
|
538
|
+
chalk3.dim(" Example skill names: frontend-design, skill-creator")
|
|
539
|
+
);
|
|
540
|
+
const promptResult = await inquirer.prompt([
|
|
541
|
+
{
|
|
542
|
+
type: "input",
|
|
543
|
+
name: "skillNames",
|
|
544
|
+
message: "Skill names (comma-separated):",
|
|
545
|
+
validate: (value) => parseSkillNames(value).length > 0 ? true : "Enter at least one skill name"
|
|
546
|
+
}
|
|
547
|
+
]);
|
|
548
|
+
skillNames = parseSkillNames(promptResult.skillNames);
|
|
549
|
+
}
|
|
550
|
+
const nextSkills = skillNames.map((skillName) => ({
|
|
551
|
+
source: parsedSource.source,
|
|
518
552
|
name: skillName,
|
|
519
553
|
description: ""
|
|
520
554
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cremini/skillpack",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9-im.0",
|
|
4
4
|
"description": "Turn Skills into a Standalone App with UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"node": ">=20"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "tsup",
|
|
27
|
+
"build": "npm run build:runtime && tsup",
|
|
28
|
+
"build:runtime": "cd runtime/server && npx tsc",
|
|
28
29
|
"dev": "tsup --watch",
|
|
29
30
|
"check": "tsc --noEmit",
|
|
30
31
|
"format": "prettier --write .",
|
|
@@ -12,6 +12,34 @@ const DEBUG = true;
|
|
|
12
12
|
const log = (...args) => DEBUG && console.log(...args);
|
|
13
13
|
const write = (data) => DEBUG && process.stdout.write(data);
|
|
14
14
|
|
|
15
|
+
function getAssistantDiagnostics(message) {
|
|
16
|
+
if (!message || message.role !== "assistant") {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const stopReason = message.stopReason;
|
|
21
|
+
const errorMessage =
|
|
22
|
+
message.errorMessage ||
|
|
23
|
+
(stopReason === "error" || stopReason === "aborted"
|
|
24
|
+
? `Request ${stopReason}`
|
|
25
|
+
: "");
|
|
26
|
+
|
|
27
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
28
|
+
const text = content
|
|
29
|
+
.filter((item) => item?.type === "text")
|
|
30
|
+
.map((item) => item.text || "")
|
|
31
|
+
.join("")
|
|
32
|
+
.trim();
|
|
33
|
+
const toolCalls = content.filter((item) => item?.type === "toolCall").length;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
stopReason,
|
|
37
|
+
errorMessage,
|
|
38
|
+
hasText: text.length > 0,
|
|
39
|
+
toolCalls,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
15
43
|
/**
|
|
16
44
|
* Handle incoming WebSocket connection using pi-coding-agent
|
|
17
45
|
* @param {import("ws").WebSocket} ws
|
|
@@ -24,6 +52,8 @@ export async function handleWsConnection(
|
|
|
24
52
|
{ apiKey, rootDir, provider = "openai", modelId = "gpt-5.4" },
|
|
25
53
|
) {
|
|
26
54
|
try {
|
|
55
|
+
let turnHadVisibleOutput = false;
|
|
56
|
+
|
|
27
57
|
// Create an in-memory auth storage to avoid touching disk
|
|
28
58
|
const authStorage = AuthStorage.inMemory({
|
|
29
59
|
[provider]: { type: "api_key", key: apiKey },
|
|
@@ -78,6 +108,7 @@ export async function handleWsConnection(
|
|
|
78
108
|
|
|
79
109
|
case "message_update":
|
|
80
110
|
if (event.assistantMessageEvent?.type === "text_delta") {
|
|
111
|
+
turnHadVisibleOutput = true;
|
|
81
112
|
write(event.assistantMessageEvent.delta);
|
|
82
113
|
ws.send(
|
|
83
114
|
JSON.stringify({
|
|
@@ -86,6 +117,7 @@ export async function handleWsConnection(
|
|
|
86
117
|
}),
|
|
87
118
|
);
|
|
88
119
|
} else if (event.assistantMessageEvent?.type === "thinking_delta") {
|
|
120
|
+
turnHadVisibleOutput = true;
|
|
89
121
|
ws.send(
|
|
90
122
|
JSON.stringify({
|
|
91
123
|
type: "thinking_delta",
|
|
@@ -97,6 +129,17 @@ export async function handleWsConnection(
|
|
|
97
129
|
|
|
98
130
|
case "message_end":
|
|
99
131
|
log(`\n--- [Message End: ${event.message?.role}] ---`);
|
|
132
|
+
if (event.message?.role === "assistant") {
|
|
133
|
+
const diagnostics = getAssistantDiagnostics(event.message);
|
|
134
|
+
if (diagnostics) {
|
|
135
|
+
log(
|
|
136
|
+
`[Assistant Diagnostics] stopReason=${diagnostics.stopReason || "unknown"} text=${diagnostics.hasText ? "yes" : "no"} toolCalls=${diagnostics.toolCalls}`,
|
|
137
|
+
);
|
|
138
|
+
if (diagnostics.errorMessage) {
|
|
139
|
+
log(`[Assistant Error] ${diagnostics.errorMessage}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
100
143
|
ws.send(
|
|
101
144
|
JSON.stringify({
|
|
102
145
|
type: "message_end",
|
|
@@ -106,6 +149,7 @@ export async function handleWsConnection(
|
|
|
106
149
|
break;
|
|
107
150
|
|
|
108
151
|
case "tool_execution_start":
|
|
152
|
+
turnHadVisibleOutput = true;
|
|
109
153
|
log(`\n>>> [Tool Execution Start: ${event.toolName}] >>>`);
|
|
110
154
|
log("Args:", JSON.stringify(event.args, null, 2));
|
|
111
155
|
ws.send(
|
|
@@ -118,6 +162,7 @@ export async function handleWsConnection(
|
|
|
118
162
|
break;
|
|
119
163
|
|
|
120
164
|
case "tool_execution_end":
|
|
165
|
+
turnHadVisibleOutput = true;
|
|
121
166
|
log(`<<< [Tool Execution End: ${event.toolName}] <<<`);
|
|
122
167
|
log(`Error: ${event.isError ? "Yes" : "No"}`);
|
|
123
168
|
ws.send(
|
|
@@ -142,8 +187,31 @@ export async function handleWsConnection(
|
|
|
142
187
|
try {
|
|
143
188
|
const payload = JSON.parse(data.toString());
|
|
144
189
|
if (payload.text) {
|
|
190
|
+
turnHadVisibleOutput = false;
|
|
191
|
+
|
|
145
192
|
// Send prompt to the agent, the session will handle message history natively
|
|
146
193
|
await session.prompt(payload.text);
|
|
194
|
+
|
|
195
|
+
const lastMessage = session.state.messages.at(-1);
|
|
196
|
+
const diagnostics = getAssistantDiagnostics(lastMessage);
|
|
197
|
+
if (diagnostics?.errorMessage) {
|
|
198
|
+
ws.send(JSON.stringify({ error: diagnostics.errorMessage }));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
diagnostics &&
|
|
204
|
+
!diagnostics.hasText &&
|
|
205
|
+
diagnostics.toolCalls === 0 &&
|
|
206
|
+
!turnHadVisibleOutput
|
|
207
|
+
) {
|
|
208
|
+
const emptyResponseError =
|
|
209
|
+
"Assistant returned no visible output. Check the server logs for stopReason/provider details.";
|
|
210
|
+
log(`[Assistant Warning] ${emptyResponseError}`);
|
|
211
|
+
ws.send(JSON.stringify({ error: emptyResponseError }));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
147
215
|
ws.send(JSON.stringify({ done: true }));
|
|
148
216
|
}
|
|
149
217
|
} catch (err) {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import TelegramBot from "node-telegram-bot-api";
|
|
2
|
+
const COMMANDS = {
|
|
3
|
+
"/clear": "clear",
|
|
4
|
+
"/restart": "restart",
|
|
5
|
+
"/shutdown": "shutdown",
|
|
6
|
+
};
|
|
7
|
+
const MAX_MESSAGE_LENGTH = 4096;
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Markdown → Telegram MarkdownV2 escaping
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Escape special characters for Telegram MarkdownV2.
|
|
13
|
+
* Reference: https://core.telegram.org/bots/api#markdownv2-style
|
|
14
|
+
*/
|
|
15
|
+
function escapeMarkdownV2(text) {
|
|
16
|
+
return text.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Attempt basic conversion from standard markdown to Telegram MarkdownV2.
|
|
20
|
+
* Falls back to plain text on complex formatting.
|
|
21
|
+
*/
|
|
22
|
+
function toTelegramFormat(text) {
|
|
23
|
+
try {
|
|
24
|
+
// For now, just escape the text for MarkdownV2.
|
|
25
|
+
// Complex markdown conversion can be enhanced later.
|
|
26
|
+
return escapeMarkdownV2(text);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// TelegramAdapter
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
export class TelegramAdapter {
|
|
36
|
+
name = "telegram";
|
|
37
|
+
bot = null;
|
|
38
|
+
agent = null;
|
|
39
|
+
options;
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
}
|
|
43
|
+
async start(ctx) {
|
|
44
|
+
this.agent = ctx.agent;
|
|
45
|
+
this.bot = new TelegramBot(this.options.token, { polling: true });
|
|
46
|
+
this.bot.on("message", (msg) => {
|
|
47
|
+
this.handleTelegramMessage(msg).catch((err) => {
|
|
48
|
+
console.error("[Telegram] Error handling message:", err);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
// Register bot commands with Telegram
|
|
52
|
+
await this.bot.setMyCommands([
|
|
53
|
+
{ command: "clear", description: "Clear current session and start new" },
|
|
54
|
+
{ command: "restart", description: "Restart the server process" },
|
|
55
|
+
{ command: "shutdown", description: "Shut down the server process" },
|
|
56
|
+
]);
|
|
57
|
+
const me = await this.bot.getMe();
|
|
58
|
+
console.log(`[TelegramAdapter] Started as @${me.username}`);
|
|
59
|
+
}
|
|
60
|
+
async stop() {
|
|
61
|
+
if (this.bot) {
|
|
62
|
+
await this.bot.stopPolling();
|
|
63
|
+
this.bot = null;
|
|
64
|
+
}
|
|
65
|
+
console.log("[TelegramAdapter] Stopped");
|
|
66
|
+
}
|
|
67
|
+
// -------------------------------------------------------------------------
|
|
68
|
+
// Message handler
|
|
69
|
+
// -------------------------------------------------------------------------
|
|
70
|
+
async handleTelegramMessage(msg) {
|
|
71
|
+
if (!this.bot || !this.agent)
|
|
72
|
+
return;
|
|
73
|
+
const chatId = msg.chat.id;
|
|
74
|
+
const text = msg.text?.trim();
|
|
75
|
+
if (!text)
|
|
76
|
+
return;
|
|
77
|
+
const channelId = `telegram-${chatId}`;
|
|
78
|
+
// --- Command handling ---
|
|
79
|
+
const commandKey = text.split(/\s/)[0].toLowerCase();
|
|
80
|
+
const command = COMMANDS[commandKey];
|
|
81
|
+
if (command) {
|
|
82
|
+
const result = await this.agent.handleCommand(command, channelId);
|
|
83
|
+
await this.sendSafe(chatId, result.message || `/${command} executed.`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// --- Regular message → agent ---
|
|
87
|
+
// Send a "thinking" indicator
|
|
88
|
+
await this.bot.sendChatAction(chatId, "typing");
|
|
89
|
+
let finalText = "";
|
|
90
|
+
let hasError = false;
|
|
91
|
+
let errorMessage = "";
|
|
92
|
+
const onEvent = (event) => {
|
|
93
|
+
// Only collect final text; skip thinking/tool intermediate events
|
|
94
|
+
switch (event.type) {
|
|
95
|
+
case "text_delta":
|
|
96
|
+
finalText += event.delta;
|
|
97
|
+
break;
|
|
98
|
+
// We intentionally ignore thinking_delta, tool_start, tool_end
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
const result = await this.agent.handleMessage(channelId, text, onEvent);
|
|
103
|
+
if (result.errorMessage) {
|
|
104
|
+
hasError = true;
|
|
105
|
+
errorMessage = result.errorMessage;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
hasError = true;
|
|
110
|
+
errorMessage = String(err);
|
|
111
|
+
}
|
|
112
|
+
// --- Send response ---
|
|
113
|
+
if (hasError) {
|
|
114
|
+
await this.sendSafe(chatId, `❌ Error: ${errorMessage}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!finalText.trim()) {
|
|
118
|
+
await this.sendSafe(chatId, "(No response generated)");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Split and send the final text
|
|
122
|
+
await this.sendLongMessage(chatId, finalText);
|
|
123
|
+
}
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
125
|
+
// Send helpers
|
|
126
|
+
// -------------------------------------------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* Send a message, splitting into chunks if too long.
|
|
129
|
+
*/
|
|
130
|
+
async sendLongMessage(chatId, text) {
|
|
131
|
+
// Try to send as plain text first (more reliable than MarkdownV2 for complex content)
|
|
132
|
+
const chunks = this.splitMessage(text);
|
|
133
|
+
for (const chunk of chunks) {
|
|
134
|
+
await this.sendWithRetry(chatId, chunk);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Split text into chunks respecting Telegram's message length limit.
|
|
139
|
+
* Tries to split at paragraph boundaries.
|
|
140
|
+
*/
|
|
141
|
+
splitMessage(text) {
|
|
142
|
+
if (text.length <= MAX_MESSAGE_LENGTH) {
|
|
143
|
+
return [text];
|
|
144
|
+
}
|
|
145
|
+
const chunks = [];
|
|
146
|
+
let remaining = text;
|
|
147
|
+
while (remaining.length > 0) {
|
|
148
|
+
if (remaining.length <= MAX_MESSAGE_LENGTH) {
|
|
149
|
+
chunks.push(remaining);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
// Find a good split point (paragraph break, then line break, then space)
|
|
153
|
+
let splitAt = remaining.lastIndexOf("\n\n", MAX_MESSAGE_LENGTH);
|
|
154
|
+
if (splitAt < MAX_MESSAGE_LENGTH * 0.5) {
|
|
155
|
+
splitAt = remaining.lastIndexOf("\n", MAX_MESSAGE_LENGTH);
|
|
156
|
+
}
|
|
157
|
+
if (splitAt < MAX_MESSAGE_LENGTH * 0.3) {
|
|
158
|
+
splitAt = remaining.lastIndexOf(" ", MAX_MESSAGE_LENGTH);
|
|
159
|
+
}
|
|
160
|
+
if (splitAt < 1) {
|
|
161
|
+
splitAt = MAX_MESSAGE_LENGTH;
|
|
162
|
+
}
|
|
163
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
164
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
165
|
+
}
|
|
166
|
+
return chunks;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Send a message with automatic retry on 429 (rate limit).
|
|
170
|
+
*/
|
|
171
|
+
async sendWithRetry(chatId, text, maxRetries = 3) {
|
|
172
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
173
|
+
try {
|
|
174
|
+
await this.bot.sendMessage(chatId, text);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
if (err?.response?.statusCode === 429 &&
|
|
179
|
+
attempt < maxRetries) {
|
|
180
|
+
const retryAfter = err.response?.body?.parameters?.retry_after || 5;
|
|
181
|
+
console.log(`[Telegram] Rate limited, retrying after ${retryAfter}s...`);
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Safe send that catches and logs errors.
|
|
191
|
+
*/
|
|
192
|
+
async sendSafe(chatId, text) {
|
|
193
|
+
try {
|
|
194
|
+
await this.sendWithRetry(chatId, text);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
console.error("[Telegram] Failed to send message:", err);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { WebSocketServer } from "ws";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
function getPackConfig(rootDir) {
|
|
8
|
+
const raw = fs.readFileSync(path.join(rootDir, "skillpack.json"), "utf-8");
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
const COMMANDS = {
|
|
12
|
+
"/clear": "clear",
|
|
13
|
+
"/restart": "restart",
|
|
14
|
+
"/shutdown": "shutdown",
|
|
15
|
+
};
|
|
16
|
+
function parseCommand(text) {
|
|
17
|
+
const trimmed = text.trim().toLowerCase();
|
|
18
|
+
return COMMANDS[trimmed] ?? null;
|
|
19
|
+
}
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// WebAdapter
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export class WebAdapter {
|
|
24
|
+
name = "web";
|
|
25
|
+
wss = null;
|
|
26
|
+
agent = null;
|
|
27
|
+
async start(ctx) {
|
|
28
|
+
const { agent, server, app, rootDir } = ctx;
|
|
29
|
+
this.agent = agent;
|
|
30
|
+
// -- API key & provider (in-memory, can be overridden by frontend) ------
|
|
31
|
+
// Read from data/config.json first
|
|
32
|
+
let apiKey = "";
|
|
33
|
+
let currentProvider = "openai";
|
|
34
|
+
const configPath = path.join(rootDir, "data", "config.json");
|
|
35
|
+
if (fs.existsSync(configPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const dataConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
38
|
+
if (dataConfig.apiKey)
|
|
39
|
+
apiKey = dataConfig.apiKey;
|
|
40
|
+
if (dataConfig.provider)
|
|
41
|
+
currentProvider = dataConfig.provider;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// ignore malformed config
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Environment variables override config file
|
|
48
|
+
if (process.env.OPENAI_API_KEY) {
|
|
49
|
+
apiKey = process.env.OPENAI_API_KEY;
|
|
50
|
+
currentProvider = "openai";
|
|
51
|
+
}
|
|
52
|
+
else if (process.env.ANTHROPIC_API_KEY) {
|
|
53
|
+
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
54
|
+
currentProvider = "anthropic";
|
|
55
|
+
}
|
|
56
|
+
// -- HTTP API routes ----------------------------------------------------
|
|
57
|
+
app.get("/api/config", (_req, res) => {
|
|
58
|
+
const config = getPackConfig(rootDir);
|
|
59
|
+
res.json({
|
|
60
|
+
name: config.name,
|
|
61
|
+
description: config.description,
|
|
62
|
+
prompts: config.prompts || [],
|
|
63
|
+
skills: config.skills || [],
|
|
64
|
+
hasApiKey: !!apiKey,
|
|
65
|
+
provider: currentProvider,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
app.get("/api/skills", (_req, res) => {
|
|
69
|
+
const config = getPackConfig(rootDir);
|
|
70
|
+
res.json(config.skills || []);
|
|
71
|
+
});
|
|
72
|
+
app.post("/api/config/key", (req, res) => {
|
|
73
|
+
const { key, provider } = req.body;
|
|
74
|
+
if (!key) {
|
|
75
|
+
res.status(400).json({ error: "API key is required" });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
apiKey = key;
|
|
79
|
+
if (provider)
|
|
80
|
+
currentProvider = provider;
|
|
81
|
+
res.json({ success: true, provider: currentProvider });
|
|
82
|
+
});
|
|
83
|
+
app.delete("/api/chat", (_req, res) => {
|
|
84
|
+
res.json({ success: true });
|
|
85
|
+
});
|
|
86
|
+
// -- Reserved: session history endpoints (stub) -------------------------
|
|
87
|
+
app.get("/api/sessions", (_req, res) => {
|
|
88
|
+
const sessions = agent.listSessions();
|
|
89
|
+
res.json(sessions);
|
|
90
|
+
});
|
|
91
|
+
app.get("/api/sessions/:id", (_req, res) => {
|
|
92
|
+
// TODO: restore session by id
|
|
93
|
+
res.status(501).json({ error: "Not implemented yet" });
|
|
94
|
+
});
|
|
95
|
+
// -- WebSocket ----------------------------------------------------------
|
|
96
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
97
|
+
server.on("upgrade", (request, socket, head) => {
|
|
98
|
+
if (request.url?.startsWith("/api/chat")) {
|
|
99
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
100
|
+
this.wss.emit("connection", ws, request);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
socket.destroy();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
this.wss.on("connection", (ws, request) => {
|
|
108
|
+
const url = new URL(request.url ?? "/", `http://${request.headers.host || "127.0.0.1"}`);
|
|
109
|
+
const _reqProvider = url.searchParams.get("provider") || currentProvider;
|
|
110
|
+
if (!apiKey) {
|
|
111
|
+
ws.send(JSON.stringify({ error: "Please set an API key first" }));
|
|
112
|
+
ws.close();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Each WebSocket connection maps to a unique channel
|
|
116
|
+
const channelId = `web-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
117
|
+
this.handleWsConnection(ws, channelId, agent);
|
|
118
|
+
});
|
|
119
|
+
console.log("[WebAdapter] Started");
|
|
120
|
+
}
|
|
121
|
+
async stop() {
|
|
122
|
+
if (this.wss) {
|
|
123
|
+
for (const client of this.wss.clients) {
|
|
124
|
+
client.close();
|
|
125
|
+
}
|
|
126
|
+
this.wss.close();
|
|
127
|
+
this.wss = null;
|
|
128
|
+
}
|
|
129
|
+
console.log("[WebAdapter] Stopped");
|
|
130
|
+
}
|
|
131
|
+
// -------------------------------------------------------------------------
|
|
132
|
+
// WebSocket message handler
|
|
133
|
+
// -------------------------------------------------------------------------
|
|
134
|
+
handleWsConnection(ws, channelId, agent) {
|
|
135
|
+
ws.on("message", async (data) => {
|
|
136
|
+
try {
|
|
137
|
+
const payload = JSON.parse(data.toString());
|
|
138
|
+
if (!payload.text)
|
|
139
|
+
return;
|
|
140
|
+
const text = payload.text;
|
|
141
|
+
// Check for bot commands
|
|
142
|
+
const command = parseCommand(text);
|
|
143
|
+
if (command) {
|
|
144
|
+
const result = await agent.handleCommand(command, channelId);
|
|
145
|
+
ws.send(JSON.stringify({
|
|
146
|
+
type: "command_result",
|
|
147
|
+
command,
|
|
148
|
+
...result,
|
|
149
|
+
}));
|
|
150
|
+
if (command === "clear") {
|
|
151
|
+
ws.send(JSON.stringify({ done: true }));
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Regular message → stream events via WebSocket
|
|
156
|
+
const onEvent = (event) => {
|
|
157
|
+
if (ws.readyState !== ws.OPEN)
|
|
158
|
+
return;
|
|
159
|
+
ws.send(JSON.stringify(event));
|
|
160
|
+
};
|
|
161
|
+
const result = await agent.handleMessage(channelId, text, onEvent);
|
|
162
|
+
if (result.errorMessage) {
|
|
163
|
+
ws.send(JSON.stringify({ error: result.errorMessage }));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
ws.send(JSON.stringify({ done: true }));
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
ws.send(JSON.stringify({ error: String(err) }));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
ws.on("close", () => {
|
|
173
|
+
agent.dispose(channelId);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|