@cremini/skillpack 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Skillpack is built for teams that want AI Agents to be deployable, trusted, and
23
23
  1. Download the example
24
24
  - [Garry Tan SkillPack](https://github.com/CreminiAI/skillpack-examples/releases/download/v.0.0.3/garry-tan.zip)
25
25
  - [Company Deep Research SkillPack](https://github.com/FinpeakInc/downloads/releases/download/v.0.0.1/Company-Deep-Research.zip)
26
- 2. Unzip it and Run ./start.sh on Mac OS, and double click start.bat on Windows (see below), the server starts and opens http://127.0.0.1:26313 in your browser
26
+ 2. Unzip it and Run ./start.sh on Mac OS, Or double click start.bat on Windows (see below), the server starts and opens http://127.0.0.1:26313 in your browser
27
27
 
28
28
  ```bash
29
29
  # macOS / Linux
@@ -92,11 +92,13 @@ Multiple skill names from the same source can be listed comma-separated.
92
92
 
93
93
  ## Zip Output
94
94
 
95
- The archive produced by `zip` is intentionally minimal:
95
+ The archive produced by `zip` is intentionally lightweight:
96
96
 
97
97
  ```text
98
98
  <pack-name>/
99
99
  ├── skillpack.json # Pack configuration
100
+ ├── AGENTS.md # Optional pack policy
101
+ ├── SOUL.md # Optional pack persona
100
102
  ├── skills/ # Installed skills
101
103
  ├── start.sh # One-click launcher for macOS / Linux
102
104
  └── start.bat # One-click launcher for Windows
@@ -104,78 +106,18 @@ The archive produced by `zip` is intentionally minimal:
104
106
 
105
107
  The start scripts use `npx @cremini/skillpack run .` so Node.js is the only prerequisite — no pre-bundled server directory is included.
106
108
 
109
+ If present, `AGENTS.md` and `SOUL.md` are read by SkillPack itself when a new chat session starts. SkillPack injects them into the runtime system prompt as pack-level policy and persona, without depending on the host machine's `AGENTS.md`, `.pi/SYSTEM.md`, or `APPEND_SYSTEM.md`.
110
+
107
111
  ## Slack/Telegram Integrations
108
112
 
109
- **Slack Configuration**: requires Slack `App Token` and `Bot Token`<br>
110
- **Telegram configuration**: requires `Bot Token`
113
+ Talk to your Agents on Slack and Telegram
111
114
 
112
- ### Slack App Setup and how to get `App Token` and `Bot Token`
115
+ ### 5 mins to get Slack `App Token` and `Bot Token`
113
116
  https://skillpack.gitbook.io/skillpack-docs/getting-started/slack-integration
114
117
 
115
- 1. Create a new Slack app at https://api.slack.com/apps
116
- 2. Enable Socket Mode (Settings → Socket Mode → Enable)
117
- 3. Generate an App-Level Token with `connections:write` scope. This is **`App Token`**
118
- 4. Add Bot Token Scopes (OAuth & Permissions):
119
-
120
- - `app_mentions:read`
121
- - `channels:history`
122
- - `channels:read`
123
- - `chat:write`
124
- - `files:read`
125
- - `files:write`
126
- - `groups:history`
127
- - `groups:read`
128
- - `im:history`
129
- - `im:read`
130
- - `im:write`
131
- - `users:read`
132
-
133
- 5. Subscribe to Bot Events (Event Subscriptions):
134
-
135
- - `app_mention`
136
- - `message.channels`
137
- - `message.groups`
138
- - `message.im`
139
-
140
- 6. Enable Direct Messages (App Home):
141
- Go to App Home in the left sidebar
142
- Under Show Tabs, enable the Messages Tab
143
- Check Allow users to send Slash commands and messages from the messages tab
144
-
145
- 7. Install the app to your workspace. Get the Bot User OAuth Token. This is **`Bot Token`**
146
- 8. Add the app to any channels where you want the agent to operate (it'll only see messages in channels it's added to)
147
- 9. On the SkillPack buit-in UI http://127.0.0.1:26313, Tap "Connect to Chat App" button and Enter the **`Bot Token`** and **`App Token`**, Save
148
-
149
- ### Telegram Setup and how to get `Bot Token`
118
+ ### 1 min to get Telegram `Bot Token`
150
119
  https://skillpack.gitbook.io/skillpack-docs/getting-started/telegram-integration
151
120
 
152
- 1. **Open Telegram** and search for the official account **`@BotFather`** (it will have a blue verified checkmark).
153
- 2. **Start a chat** by tapping "Start" or sending the `/start` command.
154
- 3. **Send the command** `/newbot` to the BotFather.
155
- 4. **Follow the prompts** to choose a display name and a unique username for your bot. The username must end with the word "bot" (e.g., `MyHelperBot` or `My_Helper_bot`).
156
- 5. **Receive the token**. Once the bot is successfully created, the BotFather will provide you with a message containing your unique API token.
157
- The token will look like a long string of numbers and letters, formatted as `123456789:AABBCCddEeff.... `
158
- 6. On the SkillPack buit-in UI http://127.0.0.1:26313, Tap "Connect to Chat App" button and Enter the **`Bot Token`**, Save
159
-
160
- ### (Optional) Put tokens into data/config.json if you don't use Web UI
161
-
162
- Or Once you have telegram or slack tokens, you can also configure them in `data/config.json` (created at runtime, not included in the zip):
163
- The runtime supports **Slack** and **Telegram** in addition to the built-in web UI.
164
-
165
- ```json
166
- {
167
- "adapters": {
168
- "telegram": {
169
- "token": "123456:ABC-DEF..."
170
- },
171
- "slack": {
172
- "botToken": "xoxb-...",
173
- "appToken": "xapp-..."
174
- }
175
- }
176
- }
177
- ```
178
-
179
121
  ---
180
122
 
181
123
  ## Example Use Cases
package/dist/cli.js CHANGED
@@ -267,6 +267,62 @@ var init_attachment_utils = __esm({
267
267
  }
268
268
  });
269
269
 
270
+ // src/runtime/commands/index.ts
271
+ function resolveCommand(input) {
272
+ const key = input.split(/\s/)[0].toLowerCase();
273
+ return COMMAND_ALIASES[key] ?? null;
274
+ }
275
+ function getVisibleCommands() {
276
+ return COMMAND_REGISTRY.filter((cmd) => cmd.visibleInHelp);
277
+ }
278
+ function getTelegramBotCommands() {
279
+ return COMMAND_REGISTRY.filter((cmd) => cmd.visibleInHelp).map((cmd) => ({
280
+ command: cmd.command,
281
+ description: cmd.description
282
+ }));
283
+ }
284
+ var COMMAND_REGISTRY, COMMAND_ALIASES;
285
+ var init_commands = __esm({
286
+ "src/runtime/commands/index.ts"() {
287
+ "use strict";
288
+ COMMAND_REGISTRY = [
289
+ {
290
+ command: "help",
291
+ description: "Show available commands, skills, and usage tips",
292
+ visibleInHelp: true
293
+ },
294
+ {
295
+ command: "clear",
296
+ description: "Clear current session and start fresh",
297
+ visibleInHelp: true
298
+ },
299
+ {
300
+ command: "restart",
301
+ description: "Restart the server process",
302
+ visibleInHelp: true
303
+ },
304
+ {
305
+ command: "shutdown",
306
+ description: "Shut down the server process",
307
+ visibleInHelp: true
308
+ },
309
+ // "new" is a hidden alias for "clear" — not shown in /help
310
+ {
311
+ command: "new",
312
+ description: "Start a new session (alias for /clear)",
313
+ visibleInHelp: false
314
+ }
315
+ ];
316
+ COMMAND_ALIASES = {
317
+ "/help": "help",
318
+ "/clear": "clear",
319
+ "/new": "clear",
320
+ "/restart": "restart",
321
+ "/shutdown": "shutdown"
322
+ };
323
+ }
324
+ });
325
+
270
326
  // src/runtime/adapters/markdown.ts
271
327
  function unwrapMarkdownSourceBlocks(text) {
272
328
  return text.replace(
@@ -307,6 +363,7 @@ function formatSlackInline(text) {
307
363
  );
308
364
  formatted = formatted.replace(/\*\*([^*\n]+)\*\*/g, "*$1*");
309
365
  formatted = formatted.replace(/__([^_\n]+)__/g, "*$1*");
366
+ formatted = formatted.replace(/^(?:-|\*) /gm, "\u2022 ");
310
367
  return formatted;
311
368
  }
312
369
  function formatTelegramInline(text) {
@@ -386,19 +443,15 @@ var telegram_exports = {};
386
443
  __export(telegram_exports, {
387
444
  TelegramAdapter: () => TelegramAdapter
388
445
  });
389
- import fs11 from "fs";
446
+ import fs12 from "fs";
390
447
  import TelegramBot from "node-telegram-bot-api";
391
- var COMMANDS2, MAX_MESSAGE_LENGTH, ACK_REACTION, TelegramAdapter;
448
+ var MAX_MESSAGE_LENGTH, ACK_REACTION, TelegramAdapter;
392
449
  var init_telegram = __esm({
393
450
  "src/runtime/adapters/telegram.ts"() {
394
451
  "use strict";
395
452
  init_markdown();
396
453
  init_attachment_utils();
397
- COMMANDS2 = {
398
- "/clear": "clear",
399
- "/restart": "restart",
400
- "/shutdown": "shutdown"
401
- };
454
+ init_commands();
402
455
  MAX_MESSAGE_LENGTH = 4096;
403
456
  ACK_REACTION = {
404
457
  type: "emoji",
@@ -422,11 +475,7 @@ var init_telegram = __esm({
422
475
  console.error("[Telegram] Error handling message:", err);
423
476
  });
424
477
  });
425
- await this.bot.setMyCommands([
426
- { command: "clear", description: "Clear current session and start new" },
427
- { command: "restart", description: "Restart the server process" },
428
- { command: "shutdown", description: "Shut down the server process" }
429
- ]);
478
+ await this.bot.setMyCommands(getTelegramBotCommands());
430
479
  const me = await this.bot.getMe();
431
480
  console.log(`[TelegramAdapter] Started as @${me.username}`);
432
481
  }
@@ -466,7 +515,7 @@ var init_telegram = __esm({
466
515
  await this.tryAckReaction(chatId, messageId);
467
516
  if (text) {
468
517
  const commandKey = text.split(/\s/)[0].toLowerCase();
469
- const command = COMMANDS2[commandKey];
518
+ const command = this.resolveTelegramCommand(commandKey);
470
519
  if (command) {
471
520
  const result = await this.agent.handleCommand(command, channelId);
472
521
  await this.sendSafe(chatId, result.message || `/${command} executed.`);
@@ -521,6 +570,9 @@ var init_telegram = __esm({
521
570
  await this.sendFileSafe(chatId, file.filePath, file.caption);
522
571
  }
523
572
  }
573
+ resolveTelegramCommand(commandKey) {
574
+ return resolveCommand(commandKey);
575
+ }
524
576
  // -------------------------------------------------------------------------
525
577
  // Send helpers
526
578
  // -------------------------------------------------------------------------
@@ -697,7 +749,7 @@ var init_telegram = __esm({
697
749
  async sendFileSafe(chatId, filePath, caption) {
698
750
  if (!this.bot) return;
699
751
  try {
700
- if (!fs11.existsSync(filePath)) {
752
+ if (!fs12.existsSync(filePath)) {
701
753
  console.error(`[Telegram] File not found for sending: ${filePath}`);
702
754
  return;
703
755
  }
@@ -717,16 +769,18 @@ var slack_exports = {};
717
769
  __export(slack_exports, {
718
770
  SlackAdapter: () => SlackAdapter
719
771
  });
720
- import fs12 from "fs";
721
- import path11 from "path";
772
+ import fs13 from "fs";
773
+ import path12 from "path";
722
774
  import { App, LogLevel } from "@slack/bolt";
723
- var INLINE_COMMANDS, SLASH_COMMANDS, MAX_MESSAGE_LENGTH2, ACK_REACTION2, SlackAdapter;
775
+ var INLINE_COMMANDS, SLASH_COMMANDS, MAX_MESSAGE_LENGTH2, ACK_REACTION2, PROCESSING_MESSAGE, SlackAdapter;
724
776
  var init_slack = __esm({
725
777
  "src/runtime/adapters/slack.ts"() {
726
778
  "use strict";
727
779
  init_markdown();
728
780
  init_attachment_utils();
781
+ init_commands();
729
782
  INLINE_COMMANDS = {
783
+ "/help": "help",
730
784
  "/clear": "clear",
731
785
  "/restart": "restart",
732
786
  "/shutdown": "shutdown"
@@ -738,6 +792,7 @@ var init_slack = __esm({
738
792
  };
739
793
  MAX_MESSAGE_LENGTH2 = 3500;
740
794
  ACK_REACTION2 = "eyes";
795
+ PROCESSING_MESSAGE = "_Processing..._";
741
796
  SlackAdapter = class {
742
797
  name = "slack";
743
798
  app = null;
@@ -807,7 +862,7 @@ var init_slack = __esm({
807
862
  console.error("[Slack] Error handling mention:", err);
808
863
  }
809
864
  });
810
- for (const commandName of Object.keys(SLASH_COMMANDS)) {
865
+ for (const commandName of [...Object.keys(SLASH_COMMANDS), "/new"]) {
811
866
  app.command(commandName, async (args) => {
812
867
  try {
813
868
  await this.handleSlashCommand(args);
@@ -872,7 +927,7 @@ var init_slack = __esm({
872
927
  await this.sendSafe(
873
928
  client,
874
929
  route,
875
- "Mention me with a message, or use `/clear` to reset this thread."
930
+ "Mention me with a message, or use `/clear` or `/new` to reset this thread."
876
931
  );
877
932
  return;
878
933
  }
@@ -890,7 +945,7 @@ var init_slack = __esm({
890
945
  ack
891
946
  }) {
892
947
  const commandName = command?.command;
893
- const mapped = commandName ? SLASH_COMMANDS[commandName] : void 0;
948
+ const mapped = commandName ? this.resolveSlashCommand(commandName) : void 0;
894
949
  if (!this.agent || !mapped) {
895
950
  await this.safeAck(ack, "Unsupported slash command.");
896
951
  return;
@@ -916,6 +971,7 @@ var init_slack = __esm({
916
971
  let hasError = false;
917
972
  let errorMessage = "";
918
973
  const pendingFiles = [];
974
+ const placeholder = await this.sendPlaceholderMessage(client, route);
919
975
  const onEvent = (event) => {
920
976
  if (event.type === "text_delta") {
921
977
  finalText += event.delta;
@@ -943,13 +999,25 @@ var init_slack = __esm({
943
999
  errorMessage = this.getErrorMessage(err);
944
1000
  }
945
1001
  if (hasError) {
946
- await this.sendSafe(client, route, `\u274C Error: ${errorMessage}`);
1002
+ await this.sendOrUpdateSafe(
1003
+ client,
1004
+ route,
1005
+ `\u274C Error: ${errorMessage}`,
1006
+ placeholder
1007
+ );
947
1008
  return;
948
1009
  }
949
1010
  if (finalText.trim()) {
950
- await this.sendLongMessage(client, route, finalText);
1011
+ await this.sendLongMessage(client, route, finalText, placeholder);
951
1012
  } else if (pendingFiles.length === 0) {
952
- await this.sendSafe(client, route, "(No response generated)");
1013
+ await this.sendOrUpdateSafe(
1014
+ client,
1015
+ route,
1016
+ "(No response generated)",
1017
+ placeholder
1018
+ );
1019
+ } else if (placeholder) {
1020
+ await this.deleteMessageSafe(client, route, placeholder.ts);
953
1021
  }
954
1022
  for (const file of pendingFiles) {
955
1023
  await this.sendFileSafe(client, route, file.filePath, file.caption);
@@ -961,7 +1029,7 @@ var init_slack = __esm({
961
1029
  async tryHandleInlineCommand(text, channelId, client, route) {
962
1030
  if (!this.agent) return false;
963
1031
  const commandKey = text.split(/\s/)[0].toLowerCase();
964
- const command = INLINE_COMMANDS[commandKey];
1032
+ const command = this.resolveInlineCommand(commandKey);
965
1033
  if (!command) return false;
966
1034
  const result = await this.agent.handleCommand(command, channelId);
967
1035
  await this.sendSafe(
@@ -971,6 +1039,19 @@ var init_slack = __esm({
971
1039
  );
972
1040
  return true;
973
1041
  }
1042
+ resolveInlineCommand(commandKey) {
1043
+ const resolved = resolveCommand(commandKey);
1044
+ if (resolved) {
1045
+ return resolved;
1046
+ }
1047
+ return INLINE_COMMANDS[commandKey];
1048
+ }
1049
+ resolveSlashCommand(commandName) {
1050
+ if (commandName === "/new") {
1051
+ return "clear";
1052
+ }
1053
+ return SLASH_COMMANDS[commandName];
1054
+ }
974
1055
  resolveSlashCommandTarget(payload, context) {
975
1056
  const teamId = this.getTeamId(payload, context);
976
1057
  const channel = payload?.channel_id;
@@ -988,7 +1069,7 @@ var init_slack = __esm({
988
1069
  );
989
1070
  if (!threadTs) {
990
1071
  return {
991
- message: "No active Skillpack thread found in this channel. Mention the bot first, or run the command inside the thread as `@bot /clear`."
1072
+ message: "No active Skillpack thread found in this channel. Mention the bot first, or run the command inside the thread as `@bot /clear` or `@bot /new`."
992
1073
  };
993
1074
  }
994
1075
  return {
@@ -1017,33 +1098,35 @@ var init_slack = __esm({
1017
1098
  return text.replace(mention, "");
1018
1099
  }
1019
1100
  splitMessage(text) {
1020
- if (text.length <= MAX_MESSAGE_LENGTH2) {
1101
+ if (this.isSlackMessageWithinLimit(text)) {
1021
1102
  return [text];
1022
1103
  }
1023
1104
  const chunks = [];
1024
1105
  let remaining = text;
1025
1106
  while (remaining.length > 0) {
1026
- if (remaining.length <= MAX_MESSAGE_LENGTH2) {
1107
+ if (this.isSlackMessageWithinLimit(remaining)) {
1027
1108
  chunks.push(remaining);
1028
1109
  break;
1029
1110
  }
1030
- let splitAt = remaining.lastIndexOf("\n\n", MAX_MESSAGE_LENGTH2);
1031
- if (splitAt < MAX_MESSAGE_LENGTH2 * 0.5) {
1032
- splitAt = remaining.lastIndexOf("\n", MAX_MESSAGE_LENGTH2);
1033
- }
1034
- if (splitAt < MAX_MESSAGE_LENGTH2 * 0.3) {
1035
- splitAt = remaining.lastIndexOf(" ", MAX_MESSAGE_LENGTH2);
1036
- }
1037
- if (splitAt < 1) {
1038
- splitAt = MAX_MESSAGE_LENGTH2;
1039
- }
1111
+ let splitAt = this.findSlackSafeSplitPoint(remaining);
1040
1112
  chunks.push(remaining.slice(0, splitAt));
1041
1113
  remaining = remaining.slice(splitAt).trimStart();
1042
1114
  }
1043
1115
  return chunks;
1044
1116
  }
1045
- async sendLongMessage(client, route, text) {
1046
- for (const chunk of this.splitMessage(text)) {
1117
+ async sendLongMessage(client, route, text, placeholder) {
1118
+ const chunks = this.splitMessage(text);
1119
+ if (chunks.length === 0) {
1120
+ return;
1121
+ }
1122
+ if (placeholder) {
1123
+ await this.updateMessageSafe(client, route, placeholder.ts, chunks[0]);
1124
+ for (const chunk of chunks.slice(1)) {
1125
+ await this.sendSafe(client, route, chunk);
1126
+ }
1127
+ return;
1128
+ }
1129
+ for (const chunk of chunks) {
1047
1130
  await this.sendWithRetry(client, route, chunk);
1048
1131
  }
1049
1132
  }
@@ -1054,17 +1137,35 @@ var init_slack = __esm({
1054
1137
  console.error("[Slack] Failed to send message:", err);
1055
1138
  }
1056
1139
  }
1140
+ async sendOrUpdateSafe(client, route, text, placeholder) {
1141
+ if (placeholder) {
1142
+ await this.updateMessageSafe(client, route, placeholder.ts, text);
1143
+ return;
1144
+ }
1145
+ await this.sendSafe(client, route, text);
1146
+ }
1147
+ async sendPlaceholderMessage(client, route) {
1148
+ try {
1149
+ return await this.sendWithRetry(client, route, PROCESSING_MESSAGE);
1150
+ } catch (err) {
1151
+ console.error("[Slack] Failed to send placeholder message:", err);
1152
+ return null;
1153
+ }
1154
+ }
1057
1155
  async sendWithRetry(client, route, text, maxRetries = 3) {
1058
1156
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
1059
1157
  try {
1060
- await client.chat.postMessage({
1158
+ const response = await client.chat.postMessage({
1061
1159
  channel: route.channel,
1062
1160
  text: formatSlackMessage(text),
1063
1161
  mrkdwn: true,
1064
1162
  thread_ts: route.threadTs,
1065
1163
  reply_broadcast: false
1066
1164
  });
1067
- return;
1165
+ if (typeof response.ts !== "string") {
1166
+ throw new Error("Slack postMessage response missing ts");
1167
+ }
1168
+ return { ts: response.ts };
1068
1169
  } catch (err) {
1069
1170
  const retryAfter = this.getRetryAfterSeconds(err);
1070
1171
  if (retryAfter && attempt < maxRetries) {
@@ -1079,6 +1180,93 @@ var init_slack = __esm({
1079
1180
  throw err;
1080
1181
  }
1081
1182
  }
1183
+ throw new Error("Slack postMessage failed after retries");
1184
+ }
1185
+ async updateMessageSafe(client, route, ts, text) {
1186
+ try {
1187
+ await this.updateWithRetry(client, route, ts, text);
1188
+ } catch (err) {
1189
+ console.error("[Slack] Failed to update message:", err);
1190
+ await this.deleteMessageSafe(client, route, ts);
1191
+ await this.sendSafe(client, route, text);
1192
+ }
1193
+ }
1194
+ async updateWithRetry(client, route, ts, text, maxRetries = 3) {
1195
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1196
+ try {
1197
+ await client.chat.update({
1198
+ channel: route.channel,
1199
+ ts,
1200
+ text: formatSlackMessage(text),
1201
+ mrkdwn: true
1202
+ });
1203
+ return;
1204
+ } catch (err) {
1205
+ const retryAfter = this.getRetryAfterSeconds(err);
1206
+ if (retryAfter && attempt < maxRetries) {
1207
+ console.log(
1208
+ `[Slack] Rate limited while updating, retrying after ${retryAfter}s...`
1209
+ );
1210
+ await new Promise(
1211
+ (resolve) => setTimeout(resolve, retryAfter * 1e3)
1212
+ );
1213
+ continue;
1214
+ }
1215
+ throw err;
1216
+ }
1217
+ }
1218
+ }
1219
+ async deleteMessageSafe(client, route, ts) {
1220
+ try {
1221
+ await client.chat.delete({
1222
+ channel: route.channel,
1223
+ ts
1224
+ });
1225
+ } catch (err) {
1226
+ console.error("[Slack] Failed to delete placeholder message:", err);
1227
+ }
1228
+ }
1229
+ isSlackMessageWithinLimit(text) {
1230
+ return formatSlackMessage(text).length <= MAX_MESSAGE_LENGTH2;
1231
+ }
1232
+ findSlackSafeSplitPoint(text) {
1233
+ const preferredBreaks = ["\n\n", "\n", " "];
1234
+ const minSplit = Math.floor(MAX_MESSAGE_LENGTH2 * 0.3);
1235
+ for (const token of preferredBreaks) {
1236
+ const index = this.findBestSlackSplitBefore(text, token);
1237
+ if (index >= minSplit) {
1238
+ return index;
1239
+ }
1240
+ }
1241
+ let low = 1;
1242
+ let high = text.length;
1243
+ let best = Math.min(text.length, MAX_MESSAGE_LENGTH2);
1244
+ while (low <= high) {
1245
+ const mid = Math.floor((low + high) / 2);
1246
+ const candidate = text.slice(0, mid);
1247
+ if (this.isSlackMessageWithinLimit(candidate)) {
1248
+ best = mid;
1249
+ low = mid + 1;
1250
+ } else {
1251
+ high = mid - 1;
1252
+ }
1253
+ }
1254
+ return Math.max(1, best);
1255
+ }
1256
+ findBestSlackSplitBefore(text, token) {
1257
+ let fromIndex = Math.min(text.length, MAX_MESSAGE_LENGTH2);
1258
+ while (fromIndex > 0) {
1259
+ const index = text.lastIndexOf(token, fromIndex);
1260
+ if (index < 0) {
1261
+ return -1;
1262
+ }
1263
+ const splitAt = index + token.length;
1264
+ if (this.isSlackMessageWithinLimit(text.slice(0, splitAt))) {
1265
+ return splitAt;
1266
+ }
1267
+ fromIndex = index - 1;
1268
+ }
1269
+ return -1;
1082
1270
  }
1083
1271
  async tryAckReaction(client, event) {
1084
1272
  try {
@@ -1196,12 +1384,12 @@ var init_slack = __esm({
1196
1384
  */
1197
1385
  async sendFileSafe(client, route, filePath, caption) {
1198
1386
  try {
1199
- if (!fs12.existsSync(filePath)) {
1387
+ if (!fs13.existsSync(filePath)) {
1200
1388
  console.error(`[Slack] File not found for sending: ${filePath}`);
1201
1389
  return;
1202
1390
  }
1203
- const filename = path11.basename(filePath);
1204
- const fileContent = fs12.readFileSync(filePath);
1391
+ const filename = path12.basename(filePath);
1392
+ const fileContent = fs13.readFileSync(filePath);
1205
1393
  await client.files.uploadV2({
1206
1394
  channel_id: route.channel,
1207
1395
  thread_ts: route.threadTs,
@@ -1787,20 +1975,120 @@ function parseSkillMd(filePath) {
1787
1975
  return null;
1788
1976
  }
1789
1977
  const frontmatter = frontmatterMatch[1];
1790
- const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
1791
- const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
1792
- if (!nameMatch) {
1978
+ const name = readFrontmatterField(frontmatter, "name");
1979
+ if (!name) {
1793
1980
  return null;
1794
1981
  }
1795
1982
  return {
1796
- name: nameMatch[1].trim(),
1797
- description: descMatch ? descMatch[1].trim() : "",
1983
+ name,
1984
+ description: readFrontmatterField(frontmatter, "description") ?? "",
1798
1985
  dir: path2.dirname(filePath)
1799
1986
  };
1800
1987
  } catch {
1801
1988
  return null;
1802
1989
  }
1803
1990
  }
1991
+ function readFrontmatterField(frontmatter, field) {
1992
+ const lines = frontmatter.split(/\r?\n/);
1993
+ for (let index = 0; index < lines.length; index += 1) {
1994
+ const match = lines[index].match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
1995
+ if (!match || match[1] !== field) {
1996
+ continue;
1997
+ }
1998
+ const rawValue = (match[2] ?? "").trim();
1999
+ if (isBlockScalar(rawValue)) {
2000
+ const [value] = readBlockScalar(lines, index + 1, rawValue);
2001
+ return value;
2002
+ }
2003
+ if (rawValue === "") {
2004
+ const [value] = readIndentedScalar(lines, index + 1);
2005
+ return value;
2006
+ }
2007
+ return stripWrappingQuotes(rawValue);
2008
+ }
2009
+ return null;
2010
+ }
2011
+ function isBlockScalar(value) {
2012
+ return /^[>|][0-9+-]*$/.test(value);
2013
+ }
2014
+ function readBlockScalar(lines, startIndex, marker) {
2015
+ const blockLines = [];
2016
+ let index = startIndex;
2017
+ while (index < lines.length) {
2018
+ const line = lines[index];
2019
+ if (line.trim() === "") {
2020
+ blockLines.push("");
2021
+ index += 1;
2022
+ continue;
2023
+ }
2024
+ if (!/^\s/.test(line)) {
2025
+ break;
2026
+ }
2027
+ blockLines.push(line);
2028
+ index += 1;
2029
+ }
2030
+ const normalized = normalizeBlockIndent(blockLines);
2031
+ const style = marker[0];
2032
+ const chomp = marker.includes("-") ? "strip" : marker.includes("+") ? "keep" : "clip";
2033
+ const value = style === ">" ? foldBlockScalar(normalized) : normalized.join("\n");
2034
+ return [applyChomp(value, chomp), index];
2035
+ }
2036
+ function readIndentedScalar(lines, startIndex) {
2037
+ const blockLines = [];
2038
+ let index = startIndex;
2039
+ while (index < lines.length) {
2040
+ const line = lines[index];
2041
+ if (line.trim() === "") {
2042
+ blockLines.push("");
2043
+ index += 1;
2044
+ continue;
2045
+ }
2046
+ if (!/^\s/.test(line)) {
2047
+ break;
2048
+ }
2049
+ blockLines.push(line);
2050
+ index += 1;
2051
+ }
2052
+ return [foldBlockScalar(normalizeBlockIndent(blockLines)), index];
2053
+ }
2054
+ function normalizeBlockIndent(lines) {
2055
+ const indents = lines.filter((line) => line.trim() !== "").map((line) => line.match(/^[ \t]*/)[0].length);
2056
+ const trimLength = indents.length > 0 ? Math.min(...indents) : 0;
2057
+ return lines.map((line) => line.slice(trimLength));
2058
+ }
2059
+ function foldBlockScalar(lines) {
2060
+ let result = "";
2061
+ let previousBlank = false;
2062
+ for (const line of lines) {
2063
+ const isBlank = line.trim() === "";
2064
+ if (isBlank) {
2065
+ result += "\n";
2066
+ previousBlank = true;
2067
+ continue;
2068
+ }
2069
+ if (result !== "" && !previousBlank) {
2070
+ result += " ";
2071
+ }
2072
+ result += line;
2073
+ previousBlank = false;
2074
+ }
2075
+ return result;
2076
+ }
2077
+ function applyChomp(value, mode) {
2078
+ if (mode === "keep") {
2079
+ return value;
2080
+ }
2081
+ if (mode === "strip") {
2082
+ return value.replace(/\n+$/g, "");
2083
+ }
2084
+ return value.replace(/\n*$/g, "");
2085
+ }
2086
+ function stripWrappingQuotes(value) {
2087
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2088
+ return value.slice(1, -1);
2089
+ }
2090
+ return value;
2091
+ }
1804
2092
  function syncSkillDescriptions(workDir, config) {
1805
2093
  const descriptionByName = /* @__PURE__ */ new Map();
1806
2094
  for (const skill of scanInstalledSkills(workDir)) {
@@ -1880,6 +2168,12 @@ async function zipCommand(workDir) {
1880
2168
  archive.file(getPackPath(workDir), {
1881
2169
  name: `${prefix}/${PACK_FILE}`
1882
2170
  });
2171
+ for (const file of ["AGENTS.md", "SOUL.md"]) {
2172
+ const filePath = path3.join(workDir, file);
2173
+ if (fs3.existsSync(filePath)) {
2174
+ archive.file(filePath, { name: `${prefix}/${file}` });
2175
+ }
2176
+ }
1883
2177
  const skillsDir = path3.join(workDir, "skills");
1884
2178
  if (fs3.existsSync(skillsDir)) {
1885
2179
  archive.directory(skillsDir, `${prefix}/skills`);
@@ -2136,15 +2430,15 @@ async function interactiveCreate(workDir) {
2136
2430
  }
2137
2431
 
2138
2432
  // src/commands/run.ts
2139
- import path13 from "path";
2140
- import fs14 from "fs";
2433
+ import path14 from "path";
2434
+ import fs15 from "fs";
2141
2435
  import inquirer2 from "inquirer";
2142
2436
  import chalk4 from "chalk";
2143
2437
 
2144
2438
  // src/runtime/server.ts
2145
2439
  import express from "express";
2146
- import path12 from "path";
2147
- import fs13 from "fs";
2440
+ import path13 from "path";
2441
+ import fs14 from "fs";
2148
2442
  import { fileURLToPath as fileURLToPath2 } from "url";
2149
2443
  import { createServer } from "http";
2150
2444
  import { exec } from "child_process";
@@ -2152,8 +2446,8 @@ import { exec } from "child_process";
2152
2446
  // src/runtime/agent.ts
2153
2447
  init_config();
2154
2448
  init_attachment_utils();
2155
- import path8 from "path";
2156
- import fs8 from "fs";
2449
+ import path9 from "path";
2450
+ import fs9 from "fs";
2157
2451
  import { fileURLToPath } from "url";
2158
2452
  import {
2159
2453
  AuthStorage,
@@ -5009,6 +5303,67 @@ ${lines.join("\n")}`
5009
5303
  };
5010
5304
  }
5011
5305
 
5306
+ // src/runtime/commands/help-command.ts
5307
+ init_commands();
5308
+ import fs8 from "fs";
5309
+ import path8 from "path";
5310
+ function buildHelpMessage(rootDir) {
5311
+ const sections = [];
5312
+ const commands = getVisibleCommands();
5313
+ const commandLines = commands.map(
5314
+ (cmd) => `- \`/${cmd.command}\` \u2014 ${cmd.description}`
5315
+ );
5316
+ sections.push(`\u{1F4CB} **Available Commands**
5317
+
5318
+ ${commandLines.join("\n")}`);
5319
+ const configPath = path8.resolve(rootDir, "skillpack.json");
5320
+ const skills = readInstalledSkills(configPath);
5321
+ if (skills.length > 0) {
5322
+ const skillLines = skills.map(
5323
+ (skill) => `- **${skill.name}**${skill.description ? ` \u2014 ${skill.description}` : ""}`
5324
+ );
5325
+ sections.push(
5326
+ `\u{1F9E9} **Installed Skills** (${skills.length})
5327
+
5328
+ ${skillLines.join("\n")}`
5329
+ );
5330
+ } else {
5331
+ sections.push("\u{1F9E9} **Installed Skills**\nNo skills installed.");
5332
+ }
5333
+ sections.push(
5334
+ [
5335
+ "\u23F0 **Scheduled Tasks**",
5336
+ "",
5337
+ "You can set up recurring tasks using natural language. For example:",
5338
+ "",
5339
+ '- "Send me a daily market briefing every morning at 9 AM"',
5340
+ `- "Summarize this week's trading data every Friday at 6 PM"`,
5341
+ '- "Check for new announcements every 30 minutes"',
5342
+ "",
5343
+ "I will handle the cron scheduling automatically."
5344
+ ].join("\n")
5345
+ );
5346
+ return sections.join("\n\n");
5347
+ }
5348
+ function handleHelpCommand(rootDir) {
5349
+ return {
5350
+ success: true,
5351
+ message: buildHelpMessage(rootDir)
5352
+ };
5353
+ }
5354
+ function readInstalledSkills(configPath) {
5355
+ if (!fs8.existsSync(configPath)) {
5356
+ return [];
5357
+ }
5358
+ try {
5359
+ const raw = fs8.readFileSync(configPath, "utf-8");
5360
+ const config = JSON.parse(raw);
5361
+ return Array.isArray(config.skills) ? config.skills : [];
5362
+ } catch {
5363
+ return [];
5364
+ }
5365
+ }
5366
+
5012
5367
  // src/runtime/agent.ts
5013
5368
  var DEBUG = true;
5014
5369
  var log = (...args) => DEBUG && console.log(...args);
@@ -5018,25 +5373,27 @@ var BUILTIN_SKILL_CREATOR_DESCRIPTION = "Create new skills, modify and improve e
5018
5373
  var BUILTIN_SKILL_CREATOR_TEMPLATE_DIR = fileURLToPath(
5019
5374
  new URL("../templates/builtin-skills/skill-creator", import.meta.url)
5020
5375
  );
5376
+ var PACK_AGENTS_FILE = "AGENTS.md";
5377
+ var PACK_SOUL_FILE = "SOUL.md";
5021
5378
  function materializeBuiltinSkillCreator(rootDir, skillsPath) {
5022
- if (!fs8.existsSync(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR)) {
5379
+ if (!fs9.existsSync(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR)) {
5023
5380
  log(
5024
5381
  `[PackAgent] Built-in skill-creator template missing: ${BUILTIN_SKILL_CREATOR_TEMPLATE_DIR}`
5025
5382
  );
5026
5383
  return null;
5027
5384
  }
5028
- const packConfigPath = path8.resolve(rootDir, "skillpack.json");
5029
- const skillDir = path8.resolve(skillsPath, BUILTIN_SKILL_CREATOR_NAME);
5030
- const skillPath = path8.join(skillDir, "SKILL.md");
5385
+ const packConfigPath = path9.resolve(rootDir, "skillpack.json");
5386
+ const skillDir = path9.resolve(skillsPath, BUILTIN_SKILL_CREATOR_NAME);
5387
+ const skillPath = path9.join(skillDir, "SKILL.md");
5031
5388
  const renderTemplate = (content) => content.replaceAll("{{SKILLS_PATH}}", skillsPath).replaceAll("{{PACK_CONFIG_PATH}}", packConfigPath);
5032
5389
  const copyDir = (srcDir, destDir) => {
5033
- fs8.mkdirSync(destDir, { recursive: true });
5034
- for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) {
5390
+ fs9.mkdirSync(destDir, { recursive: true });
5391
+ for (const entry of fs9.readdirSync(srcDir, { withFileTypes: true })) {
5035
5392
  if (entry.name === ".DS_Store") {
5036
5393
  continue;
5037
5394
  }
5038
- const srcPath = path8.join(srcDir, entry.name);
5039
- const destPath = path8.join(destDir, entry.name);
5395
+ const srcPath = path9.join(srcDir, entry.name);
5396
+ const destPath = path9.join(destDir, entry.name);
5040
5397
  if (entry.isDirectory()) {
5041
5398
  copyDir(srcPath, destPath);
5042
5399
  continue;
@@ -5045,17 +5402,17 @@ function materializeBuiltinSkillCreator(rootDir, skillsPath) {
5045
5402
  continue;
5046
5403
  }
5047
5404
  if (entry.name.endsWith(".md") || entry.name.endsWith(".py")) {
5048
- const content = fs8.readFileSync(srcPath, "utf-8");
5049
- fs8.writeFileSync(destPath, renderTemplate(content), "utf-8");
5405
+ const content = fs9.readFileSync(srcPath, "utf-8");
5406
+ fs9.writeFileSync(destPath, renderTemplate(content), "utf-8");
5050
5407
  continue;
5051
5408
  }
5052
- fs8.copyFileSync(srcPath, destPath);
5409
+ fs9.copyFileSync(srcPath, destPath);
5053
5410
  }
5054
5411
  };
5055
- if (!fs8.existsSync(skillDir)) {
5412
+ if (!fs9.existsSync(skillDir)) {
5056
5413
  copyDir(BUILTIN_SKILL_CREATOR_TEMPLATE_DIR, skillDir);
5057
5414
  }
5058
- if (!fs8.existsSync(skillPath)) {
5415
+ if (!fs9.existsSync(skillPath)) {
5059
5416
  log(
5060
5417
  `[PackAgent] Materialized built-in skill-creator but SKILL.md is missing: ${skillPath}`
5061
5418
  );
@@ -5082,6 +5439,57 @@ function overrideBuiltinSkillCreator(base, materializedSkill) {
5082
5439
  diagnostics: base.diagnostics
5083
5440
  };
5084
5441
  }
5442
+ function readOptionalPackPromptFile(filePath) {
5443
+ if (!fs9.existsSync(filePath)) {
5444
+ return void 0;
5445
+ }
5446
+ try {
5447
+ const content = fs9.readFileSync(filePath, "utf-8").trim();
5448
+ return content.length > 0 ? content : void 0;
5449
+ } catch (error) {
5450
+ console.warn(`[PackAgent] Warning: Could not read ${filePath}:`, error);
5451
+ return void 0;
5452
+ }
5453
+ }
5454
+ function buildPackPromptBlock(rootDir) {
5455
+ const agentsPath = path9.resolve(rootDir, PACK_AGENTS_FILE);
5456
+ const soulPath = path9.resolve(rootDir, PACK_SOUL_FILE);
5457
+ const agentsContent = readOptionalPackPromptFile(agentsPath);
5458
+ const soulContent = readOptionalPackPromptFile(soulPath);
5459
+ if (!agentsContent && !soulContent) {
5460
+ return {
5461
+ agentsPath,
5462
+ soulPath
5463
+ };
5464
+ }
5465
+ const sections = [
5466
+ "# SkillPack Pack Context",
5467
+ "The following instructions are injected by the SkillPack runtime from files packaged with this pack.",
5468
+ "Priority order:",
5469
+ "1. Follow the user's explicit instructions first.",
5470
+ "2. Follow `AGENTS.md` as the pack's operational policy and workflow rules.",
5471
+ "3. Follow `SOUL.md` as the pack's persona, tone, and working style.",
5472
+ "4. If `SOUL.md` conflicts with `AGENTS.md`, `AGENTS.md` wins.",
5473
+ "5. `SOUL.md` does not override task goals, safety boundaries, or `AGENTS.md`."
5474
+ ];
5475
+ if (agentsContent) {
5476
+ sections.push("## Pack Policy (`AGENTS.md`)", agentsContent);
5477
+ }
5478
+ if (soulContent) {
5479
+ sections.push(
5480
+ "## Pack Persona (`SOUL.md`)",
5481
+ "Treat the following as persona, tone, and working-style guidance only. Do not let it override task requirements, safety constraints, or `AGENTS.md`.",
5482
+ soulContent
5483
+ );
5484
+ }
5485
+ return {
5486
+ agentsPath,
5487
+ soulPath,
5488
+ agentsContent,
5489
+ soulContent,
5490
+ promptBlock: sections.join("\n\n")
5491
+ };
5492
+ }
5085
5493
  function getAssistantDiagnostics(message) {
5086
5494
  if (!message || message.role !== "assistant") {
5087
5495
  return null;
@@ -5111,7 +5519,7 @@ var PackAgent = class {
5111
5519
  authStorage;
5112
5520
  constructor(options) {
5113
5521
  this.options = options;
5114
- const configPath = path8.resolve(options.rootDir, "data", "config.json");
5522
+ const configPath = path9.resolve(options.rootDir, "data", "config.json");
5115
5523
  const backend = new ConfigFileAuthBackend(configPath);
5116
5524
  this.authStorage = AuthStorage.fromStorage(backend);
5117
5525
  const providerMeta = SUPPORTED_PROVIDERS[options.provider];
@@ -5162,24 +5570,24 @@ var PackAgent = class {
5162
5570
  if (resolvedModel && baseUrl) {
5163
5571
  log(`[PackAgent] Overriding ${provider}/${modelId} baseUrl -> ${baseUrl}`);
5164
5572
  }
5165
- const sessionDir = path8.resolve(
5573
+ const sessionDir = path9.resolve(
5166
5574
  rootDir,
5167
5575
  "data",
5168
5576
  "sessions",
5169
5577
  channelId
5170
5578
  );
5171
- fs8.mkdirSync(sessionDir, { recursive: true });
5579
+ fs9.mkdirSync(sessionDir, { recursive: true });
5172
5580
  const sessionManager = SessionManager.continueRecent(rootDir, sessionDir);
5173
5581
  log(`[PackAgent] Session dir: ${sessionDir}`);
5174
- const workspaceDir = path8.resolve(
5582
+ const workspaceDir = path9.resolve(
5175
5583
  rootDir,
5176
5584
  "data",
5177
5585
  "workspaces",
5178
5586
  channelId
5179
5587
  );
5180
- fs8.mkdirSync(workspaceDir, { recursive: true });
5588
+ fs9.mkdirSync(workspaceDir, { recursive: true });
5181
5589
  log(`[PackAgent] Workspace dir: ${workspaceDir}`);
5182
- const skillsPath = path8.resolve(rootDir, "skills");
5590
+ const skillsPath = path9.resolve(rootDir, "skills");
5183
5591
  log(`[PackAgent] Loading skills from: ${skillsPath}`);
5184
5592
  const materializedSkillCreator = materializeBuiltinSkillCreator(
5185
5593
  rootDir,
@@ -5190,10 +5598,27 @@ var PackAgent = class {
5190
5598
  `[PackAgent] Materialized built-in skill-creator to: ${materializedSkillCreator.filePath}`
5191
5599
  );
5192
5600
  }
5601
+ const packPromptFiles = buildPackPromptBlock(rootDir);
5602
+ if (packPromptFiles.agentsContent) {
5603
+ log(`[PackAgent] Loaded pack policy from: ${packPromptFiles.agentsPath}`);
5604
+ } else {
5605
+ log(`[PackAgent] No pack policy file found at: ${packPromptFiles.agentsPath}`);
5606
+ }
5607
+ if (packPromptFiles.soulContent) {
5608
+ log(`[PackAgent] Loaded pack persona from: ${packPromptFiles.soulPath}`);
5609
+ } else {
5610
+ log(`[PackAgent] No pack persona file found at: ${packPromptFiles.soulPath}`);
5611
+ }
5612
+ log(
5613
+ `[PackAgent] Pack prompt injection: ${packPromptFiles.promptBlock ? "enabled" : "disabled"}`
5614
+ );
5193
5615
  const resourceLoader = new DefaultResourceLoader({
5194
5616
  cwd: rootDir,
5195
5617
  additionalSkillPaths: [skillsPath],
5196
- skillsOverride: (base) => overrideBuiltinSkillCreator(base, materializedSkillCreator)
5618
+ skillsOverride: (base) => overrideBuiltinSkillCreator(base, materializedSkillCreator),
5619
+ agentsFilesOverride: () => ({ agentsFiles: [] }),
5620
+ systemPromptOverride: () => void 0,
5621
+ appendSystemPromptOverride: () => packPromptFiles.promptBlock ? [packPromptFiles.promptBlock] : []
5197
5622
  });
5198
5623
  await resourceLoader.reload();
5199
5624
  const tools = createCodingTools(workspaceDir);
@@ -5353,6 +5778,8 @@ ${text}`;
5353
5778
  }
5354
5779
  async handleCommand(command, channelId) {
5355
5780
  switch (command) {
5781
+ case "help":
5782
+ return handleHelpCommand(this.options.rootDir);
5356
5783
  case "new":
5357
5784
  case "clear": {
5358
5785
  const cs = this.channels.get(channelId);
@@ -5361,9 +5788,9 @@ ${text}`;
5361
5788
  this.channels.delete(channelId);
5362
5789
  }
5363
5790
  const { rootDir } = this.options;
5364
- const sessionDir = path8.resolve(rootDir, "data", "sessions", channelId);
5365
- if (fs8.existsSync(sessionDir)) {
5366
- fs8.rmSync(sessionDir, { recursive: true, force: true });
5791
+ const sessionDir = path9.resolve(rootDir, "data", "sessions", channelId);
5792
+ if (fs9.existsSync(sessionDir)) {
5793
+ fs9.rmSync(sessionDir, { recursive: true, force: true });
5367
5794
  log(`[PackAgent] Cleared session dir: ${sessionDir}`);
5368
5795
  }
5369
5796
  return {
@@ -5412,22 +5839,20 @@ ${text}`;
5412
5839
 
5413
5840
  // src/runtime/adapters/web.ts
5414
5841
  init_config();
5415
- import fs9 from "fs";
5416
- import path9 from "path";
5842
+ init_commands();
5843
+ import fs10 from "fs";
5844
+ import path10 from "path";
5417
5845
  import { WebSocketServer } from "ws";
5418
5846
  function getPackConfig(rootDir) {
5419
- const raw = fs9.readFileSync(path9.join(rootDir, "skillpack.json"), "utf-8");
5847
+ const raw = fs10.readFileSync(path10.join(rootDir, "skillpack.json"), "utf-8");
5420
5848
  return JSON.parse(raw);
5421
5849
  }
5422
- var COMMANDS = {
5423
- "/new": "new",
5424
- "/clear": "clear",
5425
- "/restart": "restart",
5426
- "/shutdown": "shutdown"
5427
- };
5428
5850
  function parseCommand(text) {
5429
- const trimmed = text.trim().toLowerCase();
5430
- return COMMANDS[trimmed] ?? null;
5851
+ return resolveCommand(text.trim().toLowerCase());
5852
+ }
5853
+ function sendWsEvent(ws, event) {
5854
+ if (ws.readyState !== ws.OPEN) return;
5855
+ ws.send(JSON.stringify(event));
5431
5856
  }
5432
5857
  function getRuntimeConfigSignature(config) {
5433
5858
  return JSON.stringify({
@@ -5568,23 +5993,23 @@ var WebAdapter = class {
5568
5993
  res.status(400).json({ error: "Missing 'path' query parameter" });
5569
5994
  return;
5570
5995
  }
5571
- const resolvedPath = path9.resolve(filePath);
5572
- const dataDir = path9.resolve(rootDir, "data");
5996
+ const resolvedPath = path10.resolve(filePath);
5997
+ const dataDir = path10.resolve(rootDir, "data");
5573
5998
  if (!resolvedPath.startsWith(dataDir)) {
5574
5999
  res.status(403).json({ error: "Access denied" });
5575
6000
  return;
5576
6001
  }
5577
- if (!fs9.existsSync(resolvedPath)) {
6002
+ if (!fs10.existsSync(resolvedPath)) {
5578
6003
  res.status(404).json({ error: "File not found" });
5579
6004
  return;
5580
6005
  }
5581
- const filename = path9.basename(resolvedPath);
6006
+ const filename = path10.basename(resolvedPath);
5582
6007
  res.setHeader("Content-Type", "application/octet-stream");
5583
6008
  res.setHeader(
5584
6009
  "Content-Disposition",
5585
6010
  `attachment; filename="${filename}"`
5586
6011
  );
5587
- fs9.createReadStream(resolvedPath).pipe(res);
6012
+ fs10.createReadStream(resolvedPath).pipe(res);
5588
6013
  });
5589
6014
  const getScheduler = () => {
5590
6015
  const schedulerAdapter = ctx.adapterMap?.get("scheduler");
@@ -5705,21 +6130,14 @@ var WebAdapter = class {
5705
6130
  const command = parseCommand(text);
5706
6131
  if (command) {
5707
6132
  const result2 = await agent.handleCommand(command, channelId);
5708
- ws.send(
5709
- JSON.stringify({
5710
- type: "command_result",
5711
- command,
5712
- ...result2
5713
- })
5714
- );
5715
- if (command === "clear" || command === "new") {
5716
- ws.send(JSON.stringify({ done: true }));
6133
+ if (result2.message) {
6134
+ sendWsEvent(ws, { type: "text_delta", delta: result2.message });
5717
6135
  }
6136
+ ws.send(JSON.stringify({ done: true }));
5718
6137
  return;
5719
6138
  }
5720
6139
  const onEvent = (event) => {
5721
- if (ws.readyState !== ws.OPEN) return;
5722
- ws.send(JSON.stringify(event));
6140
+ sendWsEvent(ws, event);
5723
6141
  };
5724
6142
  const result = await agent.handleMessage("web", channelId, text, onEvent);
5725
6143
  if (result.errorMessage) {
@@ -5813,28 +6231,28 @@ var Lifecycle = class {
5813
6231
 
5814
6232
  // src/runtime/registry.ts
5815
6233
  import crypto from "crypto";
5816
- import fs10 from "fs";
6234
+ import fs11 from "fs";
5817
6235
  import os from "os";
5818
- import path10 from "path";
5819
- var SKILLPACK_HOME = path10.join(os.homedir(), ".skillpack");
5820
- var LEGACY_REGISTRY_FILE = path10.join(SKILLPACK_HOME, "registry.json");
5821
- var REGISTRY_DIR = path10.join(SKILLPACK_HOME, "registry.d");
6236
+ import path11 from "path";
6237
+ var SKILLPACK_HOME = path11.join(os.homedir(), ".skillpack");
6238
+ var LEGACY_REGISTRY_FILE = path11.join(SKILLPACK_HOME, "registry.json");
6239
+ var REGISTRY_DIR = path11.join(SKILLPACK_HOME, "registry.d");
5822
6240
  var migrationChecked = false;
5823
6241
  function ensureHomeDir() {
5824
- if (!fs10.existsSync(SKILLPACK_HOME)) {
5825
- fs10.mkdirSync(SKILLPACK_HOME, { recursive: true });
6242
+ if (!fs11.existsSync(SKILLPACK_HOME)) {
6243
+ fs11.mkdirSync(SKILLPACK_HOME, { recursive: true });
5826
6244
  }
5827
6245
  }
5828
6246
  function ensureRegistryDir() {
5829
6247
  ensureHomeDir();
5830
- if (!fs10.existsSync(REGISTRY_DIR)) {
5831
- fs10.mkdirSync(REGISTRY_DIR, { recursive: true });
6248
+ if (!fs11.existsSync(REGISTRY_DIR)) {
6249
+ fs11.mkdirSync(REGISTRY_DIR, { recursive: true });
5832
6250
  }
5833
6251
  }
5834
6252
  function canonicalizeDir(dir) {
5835
- const resolved = path10.resolve(dir);
6253
+ const resolved = path11.resolve(dir);
5836
6254
  try {
5837
- return fs10.realpathSync(resolved);
6255
+ return fs11.realpathSync(resolved);
5838
6256
  } catch {
5839
6257
  return resolved;
5840
6258
  }
@@ -5843,7 +6261,7 @@ function hashDir(dir) {
5843
6261
  return crypto.createHash("md5").update(canonicalizeDir(dir)).digest("hex");
5844
6262
  }
5845
6263
  function getEntryPathForCanonicalDir(dir) {
5846
- return path10.join(REGISTRY_DIR, `${hashDir(dir)}.json`);
6264
+ return path11.join(REGISTRY_DIR, `${hashDir(dir)}.json`);
5847
6265
  }
5848
6266
  function getEntryPath(dir) {
5849
6267
  ensureRegistryReady();
@@ -5851,11 +6269,11 @@ function getEntryPath(dir) {
5851
6269
  }
5852
6270
  function listEntryFiles() {
5853
6271
  ensureRegistryReady();
5854
- return fs10.readdirSync(REGISTRY_DIR).filter((file) => file.endsWith(".json")).sort().map((file) => path10.join(REGISTRY_DIR, file));
6272
+ return fs11.readdirSync(REGISTRY_DIR).filter((file) => file.endsWith(".json")).sort().map((file) => path11.join(REGISTRY_DIR, file));
5855
6273
  }
5856
6274
  function readEntryFile(filePath) {
5857
6275
  try {
5858
- const raw = fs10.readFileSync(filePath, "utf-8");
6276
+ const raw = fs11.readFileSync(filePath, "utf-8");
5859
6277
  const data = JSON.parse(raw);
5860
6278
  if (typeof data?.dir !== "string" || typeof data?.name !== "string" || typeof data?.version !== "string" || typeof data?.port !== "number" || typeof data?.pid !== "number" && data?.pid !== null || data?.status !== "running" && data?.status !== "stopped") {
5861
6279
  return null;
@@ -5888,8 +6306,8 @@ function writeEntryFile(entry) {
5888
6306
  };
5889
6307
  const entryPath = getEntryPathForCanonicalDir(normalized.dir);
5890
6308
  const tmpPath = createTmpPath(entryPath);
5891
- fs10.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2), "utf-8");
5892
- fs10.renameSync(tmpPath, entryPath);
6309
+ fs11.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2), "utf-8");
6310
+ fs11.renameSync(tmpPath, entryPath);
5893
6311
  }
5894
6312
  function migrateLegacyRegistryIfNeeded() {
5895
6313
  if (migrationChecked) {
@@ -5897,14 +6315,14 @@ function migrateLegacyRegistryIfNeeded() {
5897
6315
  }
5898
6316
  migrationChecked = true;
5899
6317
  ensureRegistryDir();
5900
- if (!fs10.existsSync(LEGACY_REGISTRY_FILE)) {
6318
+ if (!fs11.existsSync(LEGACY_REGISTRY_FILE)) {
5901
6319
  return;
5902
6320
  }
5903
6321
  if (listEntryFiles().length > 0) {
5904
6322
  return;
5905
6323
  }
5906
6324
  try {
5907
- const raw = fs10.readFileSync(LEGACY_REGISTRY_FILE, "utf-8");
6325
+ const raw = fs11.readFileSync(LEGACY_REGISTRY_FILE, "utf-8");
5908
6326
  const data = JSON.parse(raw);
5909
6327
  const packs = Array.isArray(data?.packs) ? data.packs : [];
5910
6328
  for (const pack of packs) {
@@ -5917,7 +6335,7 @@ function migrateLegacyRegistryIfNeeded() {
5917
6335
  } catch {
5918
6336
  }
5919
6337
  }
5920
- fs10.renameSync(LEGACY_REGISTRY_FILE, `${LEGACY_REGISTRY_FILE}.legacy`);
6338
+ fs11.renameSync(LEGACY_REGISTRY_FILE, `${LEGACY_REGISTRY_FILE}.legacy`);
5921
6339
  } catch (err) {
5922
6340
  console.warn(" [Registry] Failed to migrate legacy registry.json:", err);
5923
6341
  }
@@ -5970,7 +6388,7 @@ function deregister(dir, pid) {
5970
6388
  }
5971
6389
 
5972
6390
  // src/runtime/server.ts
5973
- var __dirname = path12.dirname(fileURLToPath2(import.meta.url));
6391
+ var __dirname = path13.dirname(fileURLToPath2(import.meta.url));
5974
6392
  async function startServer(options) {
5975
6393
  const {
5976
6394
  rootDir,
@@ -5985,8 +6403,8 @@ async function startServer(options) {
5985
6403
  const packConfig = loadConfig(canonicalRootDir);
5986
6404
  const baseUrl = dataConfig.baseUrl?.trim() || void 0;
5987
6405
  const modelId = SUPPORTED_PROVIDERS[provider]?.defaultModelId ?? SUPPORTED_PROVIDERS.openai.defaultModelId;
5988
- const packageRoot = path12.resolve(__dirname, "..");
5989
- const webDir = fs13.existsSync(path12.join(rootDir, "web")) ? path12.join(rootDir, "web") : path12.join(packageRoot, "web");
6406
+ const packageRoot = path13.resolve(__dirname, "..");
6407
+ const webDir = fs14.existsSync(path13.join(rootDir, "web")) ? path13.join(rootDir, "web") : path13.join(packageRoot, "web");
5990
6408
  const app = express();
5991
6409
  app.use(express.json());
5992
6410
  app.use(express.static(webDir));
@@ -6154,23 +6572,23 @@ function findMissingSkills(workDir, config) {
6154
6572
  });
6155
6573
  }
6156
6574
  function copyStartTemplates2(workDir) {
6157
- const templateDir = path13.resolve(
6575
+ const templateDir = path14.resolve(
6158
6576
  new URL("../templates", import.meta.url).pathname
6159
6577
  );
6160
6578
  for (const file of ["start.sh", "start.bat"]) {
6161
- const src = path13.join(templateDir, file);
6162
- const dest = path13.join(workDir, file);
6163
- if (fs14.existsSync(src)) {
6164
- fs14.copyFileSync(src, dest);
6579
+ const src = path14.join(templateDir, file);
6580
+ const dest = path14.join(workDir, file);
6581
+ if (fs15.existsSync(src)) {
6582
+ fs15.copyFileSync(src, dest);
6165
6583
  if (file === "start.sh") {
6166
- fs14.chmodSync(dest, 493);
6584
+ fs15.chmodSync(dest, 493);
6167
6585
  }
6168
6586
  }
6169
6587
  }
6170
6588
  }
6171
6589
  async function runCommand(directory) {
6172
- const workDir = directory ? path13.resolve(directory) : process.cwd();
6173
- fs14.mkdirSync(workDir, { recursive: true });
6590
+ const workDir = directory ? path14.resolve(directory) : process.cwd();
6591
+ fs15.mkdirSync(workDir, { recursive: true });
6174
6592
  if (!configExists(workDir)) {
6175
6593
  console.log(chalk4.blue("\n No skillpack.json found. Let's set one up.\n"));
6176
6594
  const { name, description } = await inquirer2.prompt([
@@ -6206,6 +6624,8 @@ async function runCommand(directory) {
6206
6624
  console.warn(chalk4.yellow(` Warning: Some skills could not be installed: ${err}`));
6207
6625
  }
6208
6626
  }
6627
+ syncSkillDescriptions(workDir, config);
6628
+ saveConfig(workDir, config);
6209
6629
  await startServer({
6210
6630
  rootDir: workDir,
6211
6631
  daemonRun: process.env.DAEMON_RUN === "1"
@@ -6213,9 +6633,9 @@ async function runCommand(directory) {
6213
6633
  }
6214
6634
 
6215
6635
  // src/cli.ts
6216
- import fs15 from "fs";
6636
+ import fs16 from "fs";
6217
6637
  var packageJson = JSON.parse(
6218
- fs15.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
6638
+ fs16.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
6219
6639
  );
6220
6640
  var program = new Command();
6221
6641
  program.name("skillpack").description("Assemble, package, and run Agent Skills packs").version(packageJson.version);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cremini/skillpack",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Pack AI Skills into Local Agents",
5
5
  "type": "module",
6
6
  "repository": {
@@ -65,4 +65,4 @@
65
65
  "tsup": "^8.5.1",
66
66
  "typescript": "^5.9.3"
67
67
  }
68
- }
68
+ }
package/web/js/chat.js CHANGED
@@ -282,21 +282,6 @@ function handleAgentEvent(event) {
282
282
  hideLoadingIndicator();
283
283
  }
284
284
 
285
- // Handle bot command results injected by the backend WebSocket response
286
- if (event.type === "command_result") {
287
- const textBlock = getOrCreateTextBlock();
288
- let resText = `Command \`${event.command}\` succeeded.`;
289
- if (event.errorMessage) {
290
- resText = `Command \`${event.command}\` failed: ${event.errorMessage}`;
291
- } else if (event.result) {
292
- resText = `Command \`${event.command}\` result:\n\n${event.result}`;
293
- }
294
- textBlock.dataset.mdContent += resText;
295
- textBlock.innerHTML = renderMarkdown(textBlock.dataset.mdContent);
296
- scrollToBottom();
297
- return;
298
- }
299
-
300
285
  switch (event.type) {
301
286
  case "agent_start":
302
287
  case "message_start":