@cremini/skillpack 1.0.7 → 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 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
@@ -84,7 +86,7 @@ skillpack/
84
86
 
85
87
  ```bash
86
88
  # macOS / Linux
87
- chmod +x start.sh && ./start.sh
89
+ ./start.sh
88
90
 
89
91
  # Windows
90
92
  start.bat
package/dist/cli.js CHANGED
@@ -317,6 +317,13 @@ import fs3 from "fs";
317
317
  import path3 from "path";
318
318
  import { fileURLToPath } from "url";
319
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
+ }
320
327
  function getRuntimeDir() {
321
328
  const projectRoot = path3.resolve(__dirname, "..");
322
329
  return path3.join(projectRoot, "runtime");
@@ -374,6 +381,16 @@ function copyRuntimeTemplate(runtimeDir, workDir) {
374
381
  fs3.chmodSync(destinationPath, entry.stats.mode);
375
382
  }
376
383
  }
384
+ function ensureRuntimeLaunchersExecutable(workDir) {
385
+ for (const relativePath of EXECUTABLE_RUNTIME_FILES) {
386
+ const filePath = path3.join(workDir, relativePath);
387
+ if (!fs3.existsSync(filePath)) {
388
+ continue;
389
+ }
390
+ const currentMode = fs3.statSync(filePath).mode;
391
+ fs3.chmodSync(filePath, withExecuteBits(currentMode));
392
+ }
393
+ }
377
394
  function addRuntimeFiles(archive, runtimeDir, prefix) {
378
395
  const entries = collectRuntimeTemplateEntries(runtimeDir);
379
396
  for (const entry of entries) {
@@ -387,7 +404,7 @@ function addRuntimeFiles(archive, runtimeDir, prefix) {
387
404
  }
388
405
  archive.file(entry.absolutePath, {
389
406
  name: archivePath,
390
- mode: entry.stats.mode
407
+ mode: isExecutableRuntimeFile(entry.relativePath) ? withExecuteBits(entry.stats.mode) : entry.stats.mode
391
408
  });
392
409
  }
393
410
  }
@@ -433,6 +450,25 @@ async function bundle(workDir) {
433
450
  function parseSkillNames(value) {
434
451
  return value.split(",").map((name) => name.trim()).filter(Boolean);
435
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
+ }
436
472
  async function createCommand(directory) {
437
473
  const workDir = directory ? path5.resolve(directory) : process.cwd();
438
474
  if (directory) {
@@ -476,7 +512,10 @@ async function createCommand(directory) {
476
512
  chalk3.dim(" Supported formats: owner/repo, GitHub URL, or local path")
477
513
  );
478
514
  console.log(chalk3.dim(" Example source: vercel-labs/agent-skills"));
479
- console.log(chalk3.dim(" Example skill names: frontend-design, skill-creator\n"));
515
+ console.log(
516
+ chalk3.dim(" Example inline skill: vercel-labs/agent-skills --skill find-skills")
517
+ );
518
+ console.log();
480
519
  while (true) {
481
520
  const { source } = await inquirer.prompt([
482
521
  {
@@ -488,16 +527,24 @@ async function createCommand(directory) {
488
527
  if (!source.trim()) {
489
528
  break;
490
529
  }
491
- const { skillNames } = await inquirer.prompt([
492
- {
493
- type: "input",
494
- name: "skillNames",
495
- message: "Skill names (comma-separated):",
496
- validate: (value) => parseSkillNames(value).length > 0 ? true : "Enter at least one skill name"
497
- }
498
- ]);
499
- const nextSkills = parseSkillNames(skillNames).map((skillName) => ({
500
- source: source.trim(),
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,
501
548
  name: skillName,
502
549
  description: ""
503
550
  }));
@@ -610,6 +657,7 @@ async function initCommand(directory, options) {
610
657
  installConfiguredSkills(workDir, config);
611
658
  refreshDescriptionsAndSave(workDir, config);
612
659
  copyRuntimeTemplate(getRuntimeDir(), workDir);
660
+ ensureRuntimeLaunchersExecutable(workDir);
613
661
  if (options.bundle) {
614
662
  await bundle(workDir);
615
663
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cremini/skillpack",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Turn Skills into a Standalone App with UI",
5
5
  "type": "module",
6
6
  "repository": {
@@ -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/start.bat CHANGED
File without changes
package/runtime/start.sh CHANGED
File without changes
@@ -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 = marked.parse("```json\n" + safeInput + "\n```");
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
  }