@cremini/skillpack 1.0.8 → 1.0.9
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 +41 -11
- package/package.json +1 -1
- package/runtime/server/chat-proxy.js +68 -0
- 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
|
@@ -450,6 +450,25 @@ async function bundle(workDir) {
|
|
|
450
450
|
function parseSkillNames(value) {
|
|
451
451
|
return value.split(",").map((name) => name.trim()).filter(Boolean);
|
|
452
452
|
}
|
|
453
|
+
function normalizeSourceInput(value) {
|
|
454
|
+
return value.trim().replace(/^npx\s+skills\s+add\s+/u, "");
|
|
455
|
+
}
|
|
456
|
+
function parseSourceInput(value) {
|
|
457
|
+
const trimmedValue = normalizeSourceInput(value);
|
|
458
|
+
const skillFlagIndex = trimmedValue.indexOf(" --skill ");
|
|
459
|
+
if (skillFlagIndex === -1) {
|
|
460
|
+
return {
|
|
461
|
+
source: trimmedValue,
|
|
462
|
+
inlineSkillNames: []
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
const source = trimmedValue.slice(0, skillFlagIndex).trim();
|
|
466
|
+
const inlineSkillValue = trimmedValue.slice(skillFlagIndex + " --skill ".length).trim();
|
|
467
|
+
return {
|
|
468
|
+
source,
|
|
469
|
+
inlineSkillNames: inlineSkillValue.split(/[,\s]+/).map((name) => name.trim()).filter(Boolean)
|
|
470
|
+
};
|
|
471
|
+
}
|
|
453
472
|
async function createCommand(directory) {
|
|
454
473
|
const workDir = directory ? path5.resolve(directory) : process.cwd();
|
|
455
474
|
if (directory) {
|
|
@@ -493,7 +512,10 @@ async function createCommand(directory) {
|
|
|
493
512
|
chalk3.dim(" Supported formats: owner/repo, GitHub URL, or local path")
|
|
494
513
|
);
|
|
495
514
|
console.log(chalk3.dim(" Example source: vercel-labs/agent-skills"));
|
|
496
|
-
console.log(
|
|
515
|
+
console.log(
|
|
516
|
+
chalk3.dim(" Example inline skill: vercel-labs/agent-skills --skill find-skills")
|
|
517
|
+
);
|
|
518
|
+
console.log();
|
|
497
519
|
while (true) {
|
|
498
520
|
const { source } = await inquirer.prompt([
|
|
499
521
|
{
|
|
@@ -505,16 +527,24 @@ async function createCommand(directory) {
|
|
|
505
527
|
if (!source.trim()) {
|
|
506
528
|
break;
|
|
507
529
|
}
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
530
|
+
const parsedSource = parseSourceInput(source);
|
|
531
|
+
let skillNames = parsedSource.inlineSkillNames;
|
|
532
|
+
if (skillNames.length === 0) {
|
|
533
|
+
console.log(
|
|
534
|
+
chalk3.dim(" Example skill names: frontend-design, skill-creator")
|
|
535
|
+
);
|
|
536
|
+
const promptResult = await inquirer.prompt([
|
|
537
|
+
{
|
|
538
|
+
type: "input",
|
|
539
|
+
name: "skillNames",
|
|
540
|
+
message: "Skill names (comma-separated):",
|
|
541
|
+
validate: (value) => parseSkillNames(value).length > 0 ? true : "Enter at least one skill name"
|
|
542
|
+
}
|
|
543
|
+
]);
|
|
544
|
+
skillNames = parseSkillNames(promptResult.skillNames);
|
|
545
|
+
}
|
|
546
|
+
const nextSkills = skillNames.map((skillName) => ({
|
|
547
|
+
source: parsedSource.source,
|
|
518
548
|
name: skillName,
|
|
519
549
|
description: ""
|
|
520
550
|
}));
|
package/package.json
CHANGED
|
@@ -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) {
|
package/runtime/web/app.js
CHANGED
|
@@ -181,7 +181,7 @@ function renderMarkdown(mdText, { renderEmbeddedMarkdown = true } = {}) {
|
|
|
181
181
|
return escapeHtml(mdText);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
const html = marked.parse(mdText);
|
|
184
|
+
const html = ensureLinksOpenInNewTab(marked.parse(mdText));
|
|
185
185
|
if (!renderEmbeddedMarkdown) {
|
|
186
186
|
return html;
|
|
187
187
|
}
|
|
@@ -189,6 +189,18 @@ function renderMarkdown(mdText, { renderEmbeddedMarkdown = true } = {}) {
|
|
|
189
189
|
return renderEmbeddedMarkdownBlocks(html);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
function ensureLinksOpenInNewTab(html) {
|
|
193
|
+
const template = document.createElement("template");
|
|
194
|
+
template.innerHTML = html;
|
|
195
|
+
|
|
196
|
+
template.content.querySelectorAll("a[href]").forEach((linkEl) => {
|
|
197
|
+
linkEl.setAttribute("target", "_blank");
|
|
198
|
+
linkEl.setAttribute("rel", "noopener noreferrer");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return template.innerHTML;
|
|
202
|
+
}
|
|
203
|
+
|
|
192
204
|
function renderEmbeddedMarkdownBlocks(html) {
|
|
193
205
|
const template = document.createElement("template");
|
|
194
206
|
template.innerHTML = html;
|
|
@@ -365,7 +377,9 @@ function handleAgentEvent(event) {
|
|
|
365
377
|
|
|
366
378
|
let inputHtml = "";
|
|
367
379
|
if (typeof marked !== "undefined") {
|
|
368
|
-
inputHtml =
|
|
380
|
+
inputHtml = ensureLinksOpenInNewTab(
|
|
381
|
+
marked.parse("```json\n" + safeInput + "\n```"),
|
|
382
|
+
);
|
|
369
383
|
} else {
|
|
370
384
|
inputHtml = escapeHtml(safeInput);
|
|
371
385
|
}
|
|
@@ -438,7 +452,7 @@ function handleAgentEvent(event) {
|
|
|
438
452
|
: "```json\n" + safeResult + "\n```";
|
|
439
453
|
|
|
440
454
|
if (typeof marked !== "undefined") {
|
|
441
|
-
resultEl.innerHTML = marked.parse(mdText);
|
|
455
|
+
resultEl.innerHTML = ensureLinksOpenInNewTab(marked.parse(mdText));
|
|
442
456
|
} else {
|
|
443
457
|
resultEl.textContent = safeResult;
|
|
444
458
|
}
|