@cremini/skillpack 1.1.7 → 1.1.8

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.
Files changed (3) hide show
  1. package/README.md +19 -11
  2. package/dist/cli.js +781 -80
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -6,20 +6,23 @@ Skillpack helps teams turn AI skills into trusted local agents that can run in t
6
6
 
7
7
  [skillpack.sh](https://skillpack.sh) is an open-source way to package AI skills into runnable local agents. If skills and tools are like LEGO pieces, a SkillPack is the finished product that assembles them into a complete solution.
8
8
  Instead of juggling prompts, scripts, docs, and one-off automations, Skillpack gives you a simple way to:
9
+
9
10
  - package AI skills into reusable agents
10
11
  - run them locally
11
12
  - keep sensitive data in your own environment
12
13
  - use agents from tools your team already uses, like Slack and Telegram
13
14
 
14
- Skillpack is built for teams that want AI Agents to be deployable, trusted, and easy to use.
15
+ Skillpack is built for teams that want AI Agents to be deployable, trusted, and easy to use.
15
16
 
16
17
  ---
17
18
 
18
19
  ## Quick Start
19
20
 
20
- ### 1. Run a skillpack
21
+ ### 1. Run a skillpack
22
+
21
23
  1. Download the example [Company Deep Research](https://github.com/FinpeakInc/downloads/releases/download/v.0.0.1/Company-Deep-Research.zip)
22
24
  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
25
+
23
26
  ```bash
24
27
  # macOS / Linux
25
28
  ./start.sh
@@ -27,10 +30,10 @@ Skillpack is built for teams that want AI Agents to be deployable, trusted, and
27
30
  # Windows
28
31
  start.bat
29
32
  ```
33
+
30
34
  3. Enter an LLM API key (OpenAI or Claude API Key) in the left menu, use the prompt example to try it!
31
35
  4. (Optional) Refer to the instructions **Slack/Telegram Integrations** below to integrate with Slack and Telegram.
32
36
 
33
-
34
37
  ### 2. Create a new skillpack
35
38
 
36
39
  ```bash
@@ -51,7 +54,7 @@ Step by step:
51
54
  npx @cremini/skillpack create --config ./skillpack.json
52
55
 
53
56
  # From a remote URL (no directory = current directory)
54
- npx @cremini/skillpack create comic-explainer --config https://raw.githubusercontent.com/CreminiAI/skillpack/refs/heads/main/examples/comic_explainer.json
57
+ npx @cremini/skillpack create comic-explainer --config https://raw.githubusercontent.com/CreminiAI/skillpack/refs/heads/main/examples/comic-explainer.json
55
58
  ```
56
59
 
57
60
  Ready to run using "Run a skillpack" part
@@ -105,10 +108,12 @@ The start scripts use `npx @cremini/skillpack run .` so Node.js is the only prer
105
108
  **Telegram configuration**: requires `Bot Token`
106
109
 
107
110
  ### Slack App Setup and how to get `App Token` and `Bot Token`
111
+
108
112
  1. Create a new Slack app at https://api.slack.com/apps
109
113
  2. Enable Socket Mode (Settings → Socket Mode → Enable)
110
114
  3. Generate an App-Level Token with `connections:write` scope. This is **`App Token`**
111
115
  4. Add Bot Token Scopes (OAuth & Permissions):
116
+
112
117
  - `app_mentions:read`
113
118
  - `channels:history`
114
119
  - `channels:read`
@@ -123,32 +128,35 @@ The start scripts use `npx @cremini/skillpack run .` so Node.js is the only prer
123
128
  - `users:read`
124
129
 
125
130
  5. Subscribe to Bot Events (Event Subscriptions):
131
+
126
132
  - `app_mention`
127
133
  - `message.channels`
128
134
  - `message.groups`
129
135
  - `message.im`
130
136
 
131
137
  6. Enable Direct Messages (App Home):
132
- Go to App Home in the left sidebar
133
- Under Show Tabs, enable the Messages Tab
134
- Check Allow users to send Slash commands and messages from the messages tab
138
+ Go to App Home in the left sidebar
139
+ Under Show Tabs, enable the Messages Tab
140
+ Check Allow users to send Slash commands and messages from the messages tab
135
141
 
136
142
  7. Install the app to your workspace. Get the Bot User OAuth Token. This is **`Bot Token`**
137
143
  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)
138
144
  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
139
145
 
140
146
  ### Telegram Setup and how to get `Bot Token`
147
+
141
148
  1. **Open Telegram** and search for the official account **`@BotFather`** (it will have a blue verified checkmark).
142
149
  2. **Start a chat** by tapping "Start" or sending the `/start` command.
143
150
  3. **Send the command** `/newbot` to the BotFather.
144
151
  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`).
145
- 5. **Receive the token**. Once the bot is successfully created, the BotFather will provide you with a message containing your unique API token.
146
- The token will look like a long string of numbers and letters, formatted as `123456789:AABBCCddEeff.... `
152
+ 5. **Receive the token**. Once the bot is successfully created, the BotFather will provide you with a message containing your unique API token.
153
+ The token will look like a long string of numbers and letters, formatted as `123456789:AABBCCddEeff.... `
147
154
  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
148
155
 
149
156
  ### (Optional) Put tokens into data/config.json if you don't use Web UI
157
+
150
158
  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):
151
- The runtime supports **Slack** and **Telegram** in addition to the built-in web UI.
159
+ The runtime supports **Slack** and **Telegram** in addition to the built-in web UI.
152
160
 
153
161
  ```json
154
162
  {
@@ -168,7 +176,7 @@ The runtime supports **Slack** and **Telegram** in addition to the built-in web
168
176
 
169
177
  ## Example Use Cases
170
178
 
171
- The main use case is to **run local agents on your computer and integrate them with Slack or Telegram** so they can work for you and your team — operating entirely on your machine to keep all team data local and private, while continuously improving by learning new skills. Each SkillPack organizes skills around a well-defined job — for example: research a company by gathering information from multiple sources and produce a PowerPoint presentation from the findings.
179
+ The main use case is to **run local agents on your computer and integrate them with Slack or Telegram** so they can work for you and your team — operating entirely on your machine to keep all team data local and private, while continuously improving by learning new skills. Each SkillPack organizes skills around a well-defined job — for example: research a company by gathering information from multiple sources and produce a PowerPoint presentation from the findings.
172
180
 
173
181
  Download [Company Deep Research](https://github.com/FinpeakInc/downloads/releases/download/v.0.0.1/Company-Deep-Research.zip) and try it! More examples can be found at [skillpack.sh](https://skillpack.sh)
174
182
 
package/dist/cli.js CHANGED
@@ -85,6 +85,91 @@ var init_attachment_utils = __esm({
85
85
  }
86
86
  });
87
87
 
88
+ // src/runtime/config.ts
89
+ import fs8 from "fs";
90
+ import path8 from "path";
91
+ var ConfigManager, configManager;
92
+ var init_config = __esm({
93
+ "src/runtime/config.ts"() {
94
+ "use strict";
95
+ ConfigManager = class _ConfigManager {
96
+ static instance;
97
+ configData = {};
98
+ configPath = "";
99
+ constructor() {
100
+ }
101
+ static getInstance() {
102
+ if (!_ConfigManager.instance) {
103
+ _ConfigManager.instance = new _ConfigManager();
104
+ }
105
+ return _ConfigManager.instance;
106
+ }
107
+ load(rootDir) {
108
+ this.configPath = path8.join(rootDir, "data", "config.json");
109
+ if (fs8.existsSync(this.configPath)) {
110
+ try {
111
+ this.configData = JSON.parse(fs8.readFileSync(this.configPath, "utf-8"));
112
+ console.log(" Loaded config from data/config.json");
113
+ } catch (err) {
114
+ console.warn(" Warning: Failed to parse data/config.json:", err);
115
+ }
116
+ }
117
+ let { apiKey = "", provider = "openai" } = this.configData;
118
+ if (!apiKey) {
119
+ if (process.env.OPENAI_API_KEY) {
120
+ apiKey = process.env.OPENAI_API_KEY;
121
+ provider = "openai";
122
+ } else if (process.env.ANTHROPIC_API_KEY) {
123
+ apiKey = process.env.ANTHROPIC_API_KEY;
124
+ provider = "anthropic";
125
+ }
126
+ }
127
+ this.configData.apiKey = apiKey;
128
+ this.configData.provider = provider;
129
+ return this.configData;
130
+ }
131
+ getConfig() {
132
+ return this.configData;
133
+ }
134
+ save(rootDir, updates) {
135
+ const configDir = path8.join(rootDir, "data");
136
+ if (!this.configPath) {
137
+ this.configPath = path8.join(rootDir, "data", "config.json");
138
+ }
139
+ if (!fs8.existsSync(configDir)) {
140
+ fs8.mkdirSync(configDir, { recursive: true });
141
+ }
142
+ if (updates.apiKey !== void 0) this.configData.apiKey = updates.apiKey;
143
+ if (updates.provider !== void 0) this.configData.provider = updates.provider;
144
+ if (updates.adapters !== void 0) {
145
+ const merged = { ...this.configData.adapters || {} };
146
+ for (const [adapterKey, adapterVal] of Object.entries(updates.adapters)) {
147
+ if (adapterVal === null || adapterVal === void 0) {
148
+ delete merged[adapterKey];
149
+ } else {
150
+ merged[adapterKey] = adapterVal;
151
+ }
152
+ }
153
+ this.configData.adapters = merged;
154
+ }
155
+ if (updates.scheduledJobs !== void 0) {
156
+ this.configData.scheduledJobs = updates.scheduledJobs;
157
+ }
158
+ try {
159
+ fs8.writeFileSync(
160
+ this.configPath,
161
+ JSON.stringify(this.configData, null, 2),
162
+ "utf-8"
163
+ );
164
+ } catch (err) {
165
+ console.error("Failed to save config:", err);
166
+ }
167
+ }
168
+ };
169
+ configManager = ConfigManager.getInstance();
170
+ }
171
+ });
172
+
88
173
  // src/runtime/adapters/markdown.ts
89
174
  function unwrapMarkdownSourceBlocks(text) {
90
175
  return text.replace(
@@ -256,6 +341,21 @@ var init_telegram = __esm({
256
341
  console.log("[TelegramAdapter] Stopped");
257
342
  }
258
343
  // -------------------------------------------------------------------------
344
+ // MessageSender – proactive message sending
345
+ // -------------------------------------------------------------------------
346
+ /**
347
+ * Public method: send a message to a specific Telegram chat.
348
+ * channelId format: telegram-<chatId>
349
+ */
350
+ async sendMessage(channelId, text) {
351
+ if (!this.bot) throw new Error("[Telegram] Bot not initialized");
352
+ const chatId = Number(channelId.replace("telegram-", ""));
353
+ if (isNaN(chatId)) {
354
+ throw new Error(`[Telegram] Invalid channelId: ${channelId}`);
355
+ }
356
+ await this.sendLongMessage(chatId, text);
357
+ }
358
+ // -------------------------------------------------------------------------
259
359
  // Message handler
260
360
  // -------------------------------------------------------------------------
261
361
  async handleTelegramMessage(msg) {
@@ -578,6 +678,20 @@ var init_slack = __esm({
578
678
  console.log("[SlackAdapter] Stopped");
579
679
  }
580
680
  // -------------------------------------------------------------------------
681
+ // MessageSender – proactive message sending
682
+ // -------------------------------------------------------------------------
683
+ /**
684
+ * Public method: send a message to a specific Slack channel/DM.
685
+ * channelId formats:
686
+ * - slack-dm-<teamId>-<channelId>
687
+ * - slack-thread-<teamId>-<channel>-<threadTs>
688
+ */
689
+ async sendMessage(channelId, text) {
690
+ if (!this.app) throw new Error("[Slack] App not initialized");
691
+ const route = this.parseChannelId(channelId);
692
+ await this.sendLongMessage(this.app.client, route, text);
693
+ }
694
+ // -------------------------------------------------------------------------
581
695
  // Listener registration
582
696
  // -------------------------------------------------------------------------
583
697
  registerListeners(app) {
@@ -916,6 +1030,30 @@ var init_slack = __esm({
916
1030
  escapeRegExp(value) {
917
1031
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
918
1032
  }
1033
+ /**
1034
+ * Parse a skillpack channelId into a SlackRoute.
1035
+ * Supports:
1036
+ * slack-dm-<teamId>-<channelId> → { channel: <channelId> }
1037
+ * slack-thread-<teamId>-<ch>-<ts> → { channel: <ch>, threadTs: <ts> }
1038
+ */
1039
+ parseChannelId(channelId) {
1040
+ if (channelId.startsWith("slack-thread-")) {
1041
+ const rest = channelId.replace("slack-thread-", "");
1042
+ const parts = rest.split("-");
1043
+ if (parts.length >= 3) {
1044
+ const threadTs = parts.slice(2).join("-");
1045
+ return { channel: parts[1], threadTs };
1046
+ }
1047
+ }
1048
+ if (channelId.startsWith("slack-dm-")) {
1049
+ const rest = channelId.replace("slack-dm-", "");
1050
+ const parts = rest.split("-");
1051
+ if (parts.length >= 2) {
1052
+ return { channel: parts.slice(1).join("-") };
1053
+ }
1054
+ }
1055
+ return { channel: channelId };
1056
+ }
919
1057
  // -------------------------------------------------------------------------
920
1058
  // Attachment extraction & sending
921
1059
  // -------------------------------------------------------------------------
@@ -981,6 +1119,357 @@ var init_slack = __esm({
981
1119
  }
982
1120
  });
983
1121
 
1122
+ // src/runtime/adapters/types.ts
1123
+ var types_exports = {};
1124
+ __export(types_exports, {
1125
+ isMessageSender: () => isMessageSender
1126
+ });
1127
+ function isMessageSender(adapter) {
1128
+ return typeof adapter.sendMessage === "function";
1129
+ }
1130
+ var init_types = __esm({
1131
+ "src/runtime/adapters/types.ts"() {
1132
+ "use strict";
1133
+ }
1134
+ });
1135
+
1136
+ // src/runtime/adapters/scheduler.ts
1137
+ var scheduler_exports = {};
1138
+ __export(scheduler_exports, {
1139
+ SchedulerAdapter: () => SchedulerAdapter
1140
+ });
1141
+ import cron from "node-cron";
1142
+ function isValidTimezone(tz) {
1143
+ try {
1144
+ Intl.DateTimeFormat(void 0, { timeZone: tz });
1145
+ return true;
1146
+ } catch {
1147
+ return false;
1148
+ }
1149
+ }
1150
+ function isValidJobName(name) {
1151
+ return VALID_JOB_NAME.test(name) && name.length <= 64;
1152
+ }
1153
+ var VALID_JOB_NAME, SchedulerAdapter;
1154
+ var init_scheduler = __esm({
1155
+ "src/runtime/adapters/scheduler.ts"() {
1156
+ "use strict";
1157
+ init_config();
1158
+ VALID_JOB_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
1159
+ SchedulerAdapter = class {
1160
+ name = "scheduler";
1161
+ agent;
1162
+ rootDir = "";
1163
+ notifyFn = async () => {
1164
+ };
1165
+ jobs = /* @__PURE__ */ new Map();
1166
+ async start(ctx) {
1167
+ this.agent = ctx.agent;
1168
+ this.rootDir = ctx.rootDir;
1169
+ this.notifyFn = ctx.notify || (async () => {
1170
+ });
1171
+ const config = configManager.getConfig();
1172
+ const jobConfigs = config.scheduledJobs || [];
1173
+ let scheduledCount = 0;
1174
+ let disabledCount = 0;
1175
+ for (const jc of jobConfigs) {
1176
+ const result = this.registerJob(jc);
1177
+ if (result.registered) {
1178
+ if (jc.enabled === false) {
1179
+ disabledCount++;
1180
+ } else {
1181
+ scheduledCount++;
1182
+ }
1183
+ }
1184
+ }
1185
+ const parts = [];
1186
+ if (scheduledCount > 0) parts.push(`${scheduledCount} active`);
1187
+ if (disabledCount > 0) parts.push(`${disabledCount} disabled`);
1188
+ if (parts.length > 0) {
1189
+ console.log(`[SchedulerAdapter] Started with ${parts.join(", ")} job(s)`);
1190
+ } else {
1191
+ console.log("[SchedulerAdapter] Started (no jobs configured)");
1192
+ }
1193
+ }
1194
+ // -------------------------------------------------------------------------
1195
+ // Core: register a job into the managed map
1196
+ // -------------------------------------------------------------------------
1197
+ /**
1198
+ * Register a job: validate, create cron task (if enabled), store in map.
1199
+ * Does NOT persist – callers decide when to persist.
1200
+ */
1201
+ registerJob(jobConfig) {
1202
+ if (!isValidJobName(jobConfig.name)) {
1203
+ const msg = `[Scheduler] Invalid job name "${jobConfig.name}": must match ${VALID_JOB_NAME} and be \u226464 chars`;
1204
+ console.error(msg);
1205
+ return { registered: false, message: msg };
1206
+ }
1207
+ if (!cron.validate(jobConfig.cron)) {
1208
+ const msg = `[Scheduler] Invalid cron expression for job "${jobConfig.name}": ${jobConfig.cron}`;
1209
+ console.error(msg);
1210
+ return { registered: false, message: msg };
1211
+ }
1212
+ if (jobConfig.timezone && !isValidTimezone(jobConfig.timezone)) {
1213
+ const msg = `[Scheduler] Invalid timezone for job "${jobConfig.name}": ${jobConfig.timezone}`;
1214
+ console.error(msg);
1215
+ return { registered: false, message: msg };
1216
+ }
1217
+ this.removeFromMap(jobConfig.name);
1218
+ let task = null;
1219
+ if (jobConfig.enabled !== false) {
1220
+ task = cron.schedule(
1221
+ jobConfig.cron,
1222
+ () => {
1223
+ void this.runJob(jobConfig);
1224
+ },
1225
+ {
1226
+ timezone: jobConfig.timezone
1227
+ }
1228
+ );
1229
+ console.log(
1230
+ `[Scheduler] Job "${jobConfig.name}" scheduled: ${jobConfig.cron}${jobConfig.timezone ? ` (${jobConfig.timezone})` : ""}`
1231
+ );
1232
+ } else {
1233
+ console.log(
1234
+ `[Scheduler] Job "${jobConfig.name}" registered (disabled)`
1235
+ );
1236
+ }
1237
+ this.jobs.set(jobConfig.name, {
1238
+ config: jobConfig,
1239
+ task,
1240
+ running: false,
1241
+ notifyFailed: false
1242
+ });
1243
+ return { registered: true, message: "" };
1244
+ }
1245
+ // -------------------------------------------------------------------------
1246
+ // Job execution
1247
+ // -------------------------------------------------------------------------
1248
+ /**
1249
+ * Execute a scheduled job: call agent.handleMessage and push results.
1250
+ * Returns { text, notifyFailed } so callers can produce accurate status.
1251
+ */
1252
+ async runJob(jobConfig) {
1253
+ const channelId = `scheduler-${jobConfig.name}`;
1254
+ const job = this.jobs.get(jobConfig.name);
1255
+ if (job?.running) {
1256
+ console.warn(
1257
+ `[Scheduler] Job "${jobConfig.name}" is already running, skipping this trigger`
1258
+ );
1259
+ return { text: "", notifyFailed: false };
1260
+ }
1261
+ if (job) job.running = true;
1262
+ console.log(`[Scheduler] Running job "${jobConfig.name}"`);
1263
+ let fullText = "";
1264
+ let agentFailed = false;
1265
+ const pendingFiles = [];
1266
+ const onEvent = (event) => {
1267
+ if (event.type === "text_delta") fullText += event.delta;
1268
+ if (event.type === "file_output") {
1269
+ pendingFiles.push({
1270
+ filePath: event.filePath,
1271
+ caption: event.caption
1272
+ });
1273
+ }
1274
+ };
1275
+ try {
1276
+ const result = await this.agent.handleMessage(
1277
+ channelId,
1278
+ jobConfig.prompt,
1279
+ onEvent
1280
+ );
1281
+ if (result.errorMessage) {
1282
+ fullText = `\u274C \u5B9A\u65F6\u4EFB\u52A1 "${jobConfig.name}" \u6267\u884C\u5931\u8D25\uFF1A${result.errorMessage}`;
1283
+ agentFailed = true;
1284
+ if (job) job.lastError = result.errorMessage;
1285
+ } else {
1286
+ if (job) job.lastError = void 0;
1287
+ }
1288
+ } catch (err) {
1289
+ const errorMsg = err instanceof Error ? err.message : String(err);
1290
+ fullText = `\u274C \u5B9A\u65F6\u4EFB\u52A1 "${jobConfig.name}" \u5F02\u5E38\uFF1A${errorMsg}`;
1291
+ agentFailed = true;
1292
+ if (job) job.lastError = errorMsg;
1293
+ }
1294
+ if (job) {
1295
+ job.lastRunAt = (/* @__PURE__ */ new Date()).toISOString();
1296
+ job.lastResult = fullText.slice(0, 200);
1297
+ }
1298
+ let notifyFailed = false;
1299
+ if (fullText.trim()) {
1300
+ try {
1301
+ await this.notifyFn(
1302
+ jobConfig.notify.adapter,
1303
+ jobConfig.notify.channelId,
1304
+ fullText
1305
+ );
1306
+ } catch (err) {
1307
+ notifyFailed = true;
1308
+ const notifyErr = err instanceof Error ? err.message : String(err);
1309
+ console.error(
1310
+ `[Scheduler] Failed to notify for job "${jobConfig.name}":`,
1311
+ err
1312
+ );
1313
+ if (job) {
1314
+ job.lastError = agentFailed ? `${job.lastError}; Notify also failed: ${notifyErr}` : `Notify failed: ${notifyErr}`;
1315
+ }
1316
+ }
1317
+ }
1318
+ if (job) {
1319
+ job.running = false;
1320
+ job.notifyFailed = notifyFailed;
1321
+ }
1322
+ return { text: fullText, notifyFailed };
1323
+ }
1324
+ // -------------------------------------------------------------------------
1325
+ // Dynamic management API
1326
+ // -------------------------------------------------------------------------
1327
+ /**
1328
+ * Add a new job, persist to config.json.
1329
+ */
1330
+ addJob(jobConfig) {
1331
+ if (this.jobs.has(jobConfig.name)) {
1332
+ return {
1333
+ success: false,
1334
+ message: `Job "${jobConfig.name}" already exists. Remove it first.`
1335
+ };
1336
+ }
1337
+ const result = this.registerJob(jobConfig);
1338
+ if (!result.registered) {
1339
+ return { success: false, message: result.message };
1340
+ }
1341
+ this.persistJobs();
1342
+ const enabled = jobConfig.enabled !== false;
1343
+ return {
1344
+ success: true,
1345
+ message: enabled ? `Job "${jobConfig.name}" created and scheduled.` : `Job "${jobConfig.name}" created (disabled).`
1346
+ };
1347
+ }
1348
+ /**
1349
+ * Remove a job and persist to config.json.
1350
+ */
1351
+ removeJob(name) {
1352
+ if (!this.jobs.has(name)) {
1353
+ return { success: false, message: `Job "${name}" not found.` };
1354
+ }
1355
+ this.removeFromMap(name);
1356
+ this.persistJobs();
1357
+ return { success: true, message: `Job "${name}" removed.` };
1358
+ }
1359
+ /**
1360
+ * Enable or disable a job and persist.
1361
+ */
1362
+ setEnabled(name, enabled) {
1363
+ const job = this.jobs.get(name);
1364
+ if (!job) {
1365
+ return { success: false, message: `Job "${name}" not found.` };
1366
+ }
1367
+ job.config.enabled = enabled;
1368
+ if (enabled && !job.task) {
1369
+ job.task = cron.schedule(
1370
+ job.config.cron,
1371
+ () => {
1372
+ void this.runJob(job.config);
1373
+ },
1374
+ {
1375
+ timezone: job.config.timezone
1376
+ }
1377
+ );
1378
+ } else if (enabled && job.task) {
1379
+ job.task.start();
1380
+ } else if (!enabled && job.task) {
1381
+ job.task.stop();
1382
+ }
1383
+ this.persistJobs();
1384
+ return {
1385
+ success: true,
1386
+ message: `Job "${name}" ${enabled ? "enabled" : "disabled"}.`
1387
+ };
1388
+ }
1389
+ /**
1390
+ * Manually trigger a job (runs immediately, ignoring cron schedule).
1391
+ */
1392
+ async triggerJob(name) {
1393
+ const job = this.jobs.get(name);
1394
+ if (!job) {
1395
+ return { success: false, message: `Job "${name}" not found.` };
1396
+ }
1397
+ const { text, notifyFailed } = await this.runJob(job.config);
1398
+ if (!text) {
1399
+ return {
1400
+ success: true,
1401
+ message: `Job "${name}" triggered but produced no output.`
1402
+ };
1403
+ }
1404
+ if (notifyFailed) {
1405
+ return {
1406
+ success: true,
1407
+ message: `Job "${name}" executed, but notification to ${job.config.notify.adapter} failed. Check logs.`
1408
+ };
1409
+ }
1410
+ return {
1411
+ success: true,
1412
+ message: `Job "${name}" triggered. Result sent to ${job.config.notify.adapter}.`
1413
+ };
1414
+ }
1415
+ /**
1416
+ * List all jobs with their current status.
1417
+ */
1418
+ listJobs() {
1419
+ const result = [];
1420
+ for (const [, job] of this.jobs) {
1421
+ result.push({
1422
+ name: job.config.name,
1423
+ cron: job.config.cron,
1424
+ prompt: job.config.prompt,
1425
+ notify: job.config.notify,
1426
+ enabled: job.config.enabled !== false,
1427
+ timezone: job.config.timezone,
1428
+ lastRunAt: job.lastRunAt,
1429
+ lastError: job.lastError,
1430
+ running: job.running,
1431
+ notifyFailed: job.notifyFailed
1432
+ });
1433
+ }
1434
+ return result;
1435
+ }
1436
+ // -------------------------------------------------------------------------
1437
+ // Internal helpers
1438
+ // -------------------------------------------------------------------------
1439
+ /**
1440
+ * Stop the cron task and remove a job from the map (does NOT persist).
1441
+ */
1442
+ removeFromMap(name) {
1443
+ const existing = this.jobs.get(name);
1444
+ if (existing) {
1445
+ existing.task?.stop();
1446
+ this.jobs.delete(name);
1447
+ }
1448
+ }
1449
+ /**
1450
+ * Persist all current jobs to data/config.json.
1451
+ */
1452
+ persistJobs() {
1453
+ const configs = [];
1454
+ for (const [, job] of this.jobs) {
1455
+ configs.push(job.config);
1456
+ }
1457
+ configManager.save(this.rootDir, { scheduledJobs: configs });
1458
+ }
1459
+ // -------------------------------------------------------------------------
1460
+ // Lifecycle
1461
+ // -------------------------------------------------------------------------
1462
+ async stop() {
1463
+ for (const [, job] of this.jobs) {
1464
+ job.task?.stop();
1465
+ }
1466
+ this.jobs.clear();
1467
+ console.log("[SchedulerAdapter] All jobs stopped.");
1468
+ }
1469
+ };
1470
+ }
1471
+ });
1472
+
984
1473
  // src/cli.ts
985
1474
  import { Command } from "commander";
986
1475
  import chalk5 from "chalk";
@@ -4268,6 +4757,169 @@ function createSendFileTool(fileOutputCallbackRef) {
4268
4757
  };
4269
4758
  }
4270
4759
 
4760
+ // src/runtime/tools/manage-schedule-tool.ts
4761
+ var ManageScheduleParams = Type.Object({
4762
+ action: Type.Union(
4763
+ [
4764
+ Type.Literal("add"),
4765
+ Type.Literal("list"),
4766
+ Type.Literal("remove"),
4767
+ Type.Literal("trigger"),
4768
+ Type.Literal("enable"),
4769
+ Type.Literal("disable")
4770
+ ],
4771
+ { description: "The action to perform." }
4772
+ ),
4773
+ name: Type.Optional(
4774
+ Type.String({
4775
+ description: "Unique name for the scheduled task. Required for add/remove/trigger/enable/disable."
4776
+ })
4777
+ ),
4778
+ cron: Type.Optional(
4779
+ Type.String({
4780
+ description: "Cron expression (5 fields: minute hour day month weekday). Required for add."
4781
+ })
4782
+ ),
4783
+ prompt: Type.Optional(
4784
+ Type.String({
4785
+ description: "The work prompt to execute when the task triggers. Required for add. Describe only what to do each run; do not repeat timing, cron, or 'every N minutes' instructions here."
4786
+ })
4787
+ ),
4788
+ notifyAdapter: Type.Optional(
4789
+ Type.String({
4790
+ description: "Target adapter name for result notification: 'telegram' or 'slack'. Required for add."
4791
+ })
4792
+ ),
4793
+ notifyChannelId: Type.Optional(
4794
+ Type.String({
4795
+ description: "Target channelId for result notification (e.g. 'telegram-123456'). Required for add."
4796
+ })
4797
+ ),
4798
+ timezone: Type.Optional(
4799
+ Type.String({
4800
+ description: "Optional timezone for the cron schedule, e.g. 'Asia/Shanghai', 'America/New_York'."
4801
+ })
4802
+ )
4803
+ });
4804
+ function textResult(text) {
4805
+ return { content: [{ type: "text", text }], details: void 0 };
4806
+ }
4807
+ function createManageScheduleTool(schedulerRef, _rootDirRef) {
4808
+ return {
4809
+ name: "manage_scheduled_task",
4810
+ label: "Manage Scheduled Task",
4811
+ description: [
4812
+ "Manage scheduled tasks (cron jobs) that automatically execute prompts and push results to IM channels.",
4813
+ "",
4814
+ "Actions:",
4815
+ "- add: Create a new scheduled task. Requires: name, cron, prompt, notifyAdapter, notifyChannelId. The prompt must describe only the work for each run, not the schedule itself.",
4816
+ "- list: List all scheduled tasks with their status.",
4817
+ "- remove: Remove a scheduled task by name.",
4818
+ "- trigger: Manually trigger a scheduled task by name (runs immediately).",
4819
+ "- enable: Enable a disabled scheduled task.",
4820
+ "- disable: Disable a scheduled task without removing it.",
4821
+ "",
4822
+ "Cron expression format: '* * * * *' (minute hour day month weekday)",
4823
+ "Examples:",
4824
+ " '0 9 * * 1-5' = every weekday at 9:00 AM",
4825
+ " '0 18 * * 5' = every Friday at 6:00 PM",
4826
+ " '*/30 * * * *' = every 30 minutes",
4827
+ "",
4828
+ "notifyAdapter: 'telegram' or 'slack'",
4829
+ "notifyChannelId: the channel ID where result will be sent (e.g. 'telegram-123456')"
4830
+ ].join("\n"),
4831
+ parameters: ManageScheduleParams,
4832
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
4833
+ const scheduler = schedulerRef.current;
4834
+ if (!scheduler) {
4835
+ return textResult(
4836
+ "Error: Scheduler is not available. The scheduled task system may not be initialized."
4837
+ );
4838
+ }
4839
+ switch (params.action) {
4840
+ case "list": {
4841
+ const jobs = scheduler.listJobs();
4842
+ if (jobs.length === 0) {
4843
+ return textResult("No scheduled tasks configured.");
4844
+ }
4845
+ const lines = jobs.map(
4846
+ (j) => `- **${j.name}**: \`${j.cron}\` \u2192 ${j.notify.adapter}:${j.notify.channelId} [${j.enabled ? "enabled" : "disabled"}]${j.running ? " (running)" : ""}${j.lastRunAt ? ` (last: ${j.lastRunAt})` : ""}`
4847
+ );
4848
+ return textResult(
4849
+ `Scheduled tasks (${jobs.length}):
4850
+ ${lines.join("\n")}`
4851
+ );
4852
+ }
4853
+ case "add": {
4854
+ if (!params.name || !params.cron || !params.prompt) {
4855
+ return textResult(
4856
+ "Error: 'name', 'cron', and 'prompt' are required for adding a task."
4857
+ );
4858
+ }
4859
+ if (!params.notifyAdapter || !params.notifyChannelId) {
4860
+ return textResult(
4861
+ "Error: 'notifyAdapter' and 'notifyChannelId' are required for adding a task."
4862
+ );
4863
+ }
4864
+ const jobConfig = {
4865
+ name: params.name,
4866
+ cron: params.cron,
4867
+ prompt: params.prompt,
4868
+ notify: {
4869
+ adapter: params.notifyAdapter,
4870
+ channelId: params.notifyChannelId
4871
+ },
4872
+ enabled: true,
4873
+ timezone: params.timezone
4874
+ };
4875
+ const result = scheduler.addJob(jobConfig);
4876
+ return textResult(result.message);
4877
+ }
4878
+ case "remove": {
4879
+ if (!params.name) {
4880
+ return textResult(
4881
+ "Error: 'name' is required for removing a task."
4882
+ );
4883
+ }
4884
+ const result = scheduler.removeJob(params.name);
4885
+ return textResult(result.message);
4886
+ }
4887
+ case "trigger": {
4888
+ if (!params.name) {
4889
+ return textResult(
4890
+ "Error: 'name' is required for triggering a task."
4891
+ );
4892
+ }
4893
+ const result = await scheduler.triggerJob(params.name);
4894
+ return textResult(result.message);
4895
+ }
4896
+ case "enable": {
4897
+ if (!params.name) {
4898
+ return textResult(
4899
+ "Error: 'name' is required for enabling a task."
4900
+ );
4901
+ }
4902
+ const result = scheduler.setEnabled(params.name, true);
4903
+ return textResult(result.message);
4904
+ }
4905
+ case "disable": {
4906
+ if (!params.name) {
4907
+ return textResult(
4908
+ "Error: 'name' is required for disabling a task."
4909
+ );
4910
+ }
4911
+ const result = scheduler.setEnabled(params.name, false);
4912
+ return textResult(result.message);
4913
+ }
4914
+ default:
4915
+ return textResult(
4916
+ `Error: Unknown action '${params.action}'. Use: add, list, remove, trigger, enable, disable.`
4917
+ );
4918
+ }
4919
+ }
4920
+ };
4921
+ }
4922
+
4271
4923
  // src/runtime/agent.ts
4272
4924
  var DEBUG = true;
4273
4925
  var log = (...args) => DEBUG && console.log(...args);
@@ -4298,8 +4950,22 @@ var PackAgent = class {
4298
4950
  current: null
4299
4951
  };
4300
4952
  sendFileToolDef = createSendFileTool(this.fileOutputCallbackRef);
4953
+ schedulerRef = { current: null };
4954
+ rootDirRef;
4955
+ scheduleToolDef;
4301
4956
  constructor(options) {
4302
4957
  this.options = options;
4958
+ this.rootDirRef = { current: options.rootDir };
4959
+ this.scheduleToolDef = createManageScheduleTool(
4960
+ this.schedulerRef,
4961
+ this.rootDirRef
4962
+ );
4963
+ }
4964
+ /**
4965
+ * Inject scheduler reference (called by server.ts after adapter init).
4966
+ */
4967
+ setScheduler(scheduler) {
4968
+ this.schedulerRef.current = scheduler;
4303
4969
  }
4304
4970
  /**
4305
4971
  * Lazily create (or return existing) session for a channel.
@@ -4350,7 +5016,7 @@ var PackAgent = class {
4350
5016
  resourceLoader,
4351
5017
  model,
4352
5018
  tools,
4353
- customTools: [this.sendFileToolDef]
5019
+ customTools: [this.sendFileToolDef, this.scheduleToolDef]
4354
5020
  });
4355
5021
  const channelSession = {
4356
5022
  session,
@@ -4555,87 +5221,10 @@ ${text}`;
4555
5221
  };
4556
5222
 
4557
5223
  // src/runtime/adapters/web.ts
5224
+ init_config();
4558
5225
  import fs9 from "fs";
4559
5226
  import path9 from "path";
4560
5227
  import { WebSocketServer } from "ws";
4561
-
4562
- // src/runtime/config.ts
4563
- import fs8 from "fs";
4564
- import path8 from "path";
4565
- var ConfigManager = class _ConfigManager {
4566
- static instance;
4567
- configData = {};
4568
- configPath = "";
4569
- constructor() {
4570
- }
4571
- static getInstance() {
4572
- if (!_ConfigManager.instance) {
4573
- _ConfigManager.instance = new _ConfigManager();
4574
- }
4575
- return _ConfigManager.instance;
4576
- }
4577
- load(rootDir) {
4578
- this.configPath = path8.join(rootDir, "data", "config.json");
4579
- if (fs8.existsSync(this.configPath)) {
4580
- try {
4581
- this.configData = JSON.parse(fs8.readFileSync(this.configPath, "utf-8"));
4582
- console.log(" Loaded config from data/config.json");
4583
- } catch (err) {
4584
- console.warn(" Warning: Failed to parse data/config.json:", err);
4585
- }
4586
- }
4587
- let { apiKey = "", provider = "openai" } = this.configData;
4588
- if (!apiKey) {
4589
- if (process.env.OPENAI_API_KEY) {
4590
- apiKey = process.env.OPENAI_API_KEY;
4591
- provider = "openai";
4592
- } else if (process.env.ANTHROPIC_API_KEY) {
4593
- apiKey = process.env.ANTHROPIC_API_KEY;
4594
- provider = "anthropic";
4595
- }
4596
- }
4597
- this.configData.apiKey = apiKey;
4598
- this.configData.provider = provider;
4599
- return this.configData;
4600
- }
4601
- getConfig() {
4602
- return this.configData;
4603
- }
4604
- save(rootDir, updates) {
4605
- const configDir = path8.join(rootDir, "data");
4606
- if (!this.configPath) {
4607
- this.configPath = path8.join(rootDir, "data", "config.json");
4608
- }
4609
- if (!fs8.existsSync(configDir)) {
4610
- fs8.mkdirSync(configDir, { recursive: true });
4611
- }
4612
- if (updates.apiKey !== void 0) this.configData.apiKey = updates.apiKey;
4613
- if (updates.provider !== void 0) this.configData.provider = updates.provider;
4614
- if (updates.adapters !== void 0) {
4615
- const merged = { ...this.configData.adapters || {} };
4616
- for (const [adapterKey, adapterVal] of Object.entries(updates.adapters)) {
4617
- if (adapterVal === null || adapterVal === void 0) {
4618
- delete merged[adapterKey];
4619
- } else {
4620
- merged[adapterKey] = adapterVal;
4621
- }
4622
- }
4623
- this.configData.adapters = merged;
4624
- }
4625
- try {
4626
- fs8.writeFileSync(
4627
- this.configPath,
4628
- JSON.stringify(this.configData, null, 2),
4629
- "utf-8"
4630
- );
4631
- } catch (err) {
4632
- console.error("Failed to save config:", err);
4633
- }
4634
- }
4635
- };
4636
- var configManager = ConfigManager.getInstance();
4637
-
4638
- // src/runtime/adapters/web.ts
4639
5228
  function getPackConfig(rootDir) {
4640
5229
  const raw = fs9.readFileSync(path9.join(rootDir, "skillpack.json"), "utf-8");
4641
5230
  return JSON.parse(raw);
@@ -4761,6 +5350,75 @@ var WebAdapter = class {
4761
5350
  );
4762
5351
  fs9.createReadStream(resolvedPath).pipe(res);
4763
5352
  });
5353
+ const getScheduler = () => {
5354
+ const schedulerAdapter = ctx.adapterMap?.get("scheduler");
5355
+ if (!schedulerAdapter) return null;
5356
+ return schedulerAdapter;
5357
+ };
5358
+ app.get("/api/scheduler/jobs", (_req, res) => {
5359
+ const scheduler = getScheduler();
5360
+ if (!scheduler) {
5361
+ res.json([]);
5362
+ return;
5363
+ }
5364
+ res.json(scheduler.listJobs());
5365
+ });
5366
+ app.post("/api/scheduler/jobs", (req, res) => {
5367
+ const scheduler = getScheduler();
5368
+ if (!scheduler) {
5369
+ res.status(503).json({ success: false, message: "Scheduler not available" });
5370
+ return;
5371
+ }
5372
+ const { name, cron: cronExpr, prompt, notify, enabled, timezone } = req.body;
5373
+ if (!name || !cronExpr || !prompt || !notify?.adapter || !notify?.channelId) {
5374
+ res.status(400).json({
5375
+ success: false,
5376
+ message: "Required fields: name, cron, prompt, notify.adapter, notify.channelId"
5377
+ });
5378
+ return;
5379
+ }
5380
+ const result = scheduler.addJob({
5381
+ name,
5382
+ cron: cronExpr,
5383
+ prompt,
5384
+ notify,
5385
+ enabled: enabled !== false,
5386
+ timezone
5387
+ });
5388
+ res.json(result);
5389
+ });
5390
+ app.delete("/api/scheduler/jobs/:name", (req, res) => {
5391
+ const scheduler = getScheduler();
5392
+ if (!scheduler) {
5393
+ res.status(503).json({ success: false, message: "Scheduler not available" });
5394
+ return;
5395
+ }
5396
+ const result = scheduler.removeJob(req.params.name);
5397
+ res.json(result);
5398
+ });
5399
+ app.post("/api/scheduler/jobs/:name/trigger", async (req, res) => {
5400
+ const scheduler = getScheduler();
5401
+ if (!scheduler) {
5402
+ res.status(503).json({ success: false, message: "Scheduler not available" });
5403
+ return;
5404
+ }
5405
+ const result = await scheduler.triggerJob(req.params.name);
5406
+ res.json(result);
5407
+ });
5408
+ app.patch("/api/scheduler/jobs/:name", (req, res) => {
5409
+ const scheduler = getScheduler();
5410
+ if (!scheduler) {
5411
+ res.status(503).json({ success: false, message: "Scheduler not available" });
5412
+ return;
5413
+ }
5414
+ const { enabled } = req.body;
5415
+ if (typeof enabled !== "boolean") {
5416
+ res.status(400).json({ success: false, message: "Field 'enabled' (boolean) is required" });
5417
+ return;
5418
+ }
5419
+ const result = scheduler.setEnabled(req.params.name, enabled);
5420
+ res.json(result);
5421
+ });
4764
5422
  this.wss = new WebSocketServer({ noServer: true });
4765
5423
  server.on("upgrade", (request, socket, head) => {
4766
5424
  if (request.url?.startsWith("/api/chat")) {
@@ -4841,6 +5499,9 @@ var WebAdapter = class {
4841
5499
  }
4842
5500
  };
4843
5501
 
5502
+ // src/runtime/server.ts
5503
+ init_config();
5504
+
4844
5505
  // src/runtime/lifecycle.ts
4845
5506
  var SHUTDOWN_EXIT_CODE = 64;
4846
5507
  var RESTART_EXIT_CODE = 75;
@@ -4951,9 +5612,11 @@ async function startServer(options) {
4951
5612
  lifecycleHandler: lifecycle
4952
5613
  });
4953
5614
  const adapters = [];
5615
+ const adapterMap = /* @__PURE__ */ new Map();
4954
5616
  const webAdapter = new WebAdapter();
4955
- await webAdapter.start({ agent, server, app, rootDir, lifecycle });
5617
+ await webAdapter.start({ agent, server, app, rootDir, lifecycle, adapterMap });
4956
5618
  adapters.push(webAdapter);
5619
+ adapterMap.set(webAdapter.name, webAdapter);
4957
5620
  if (dataConfig.adapters?.telegram?.token) {
4958
5621
  try {
4959
5622
  const { TelegramAdapter: TelegramAdapter2 } = await Promise.resolve().then(() => (init_telegram(), telegram_exports));
@@ -4962,6 +5625,7 @@ async function startServer(options) {
4962
5625
  });
4963
5626
  await telegramAdapter.start({ agent, server, app, rootDir, lifecycle });
4964
5627
  adapters.push(telegramAdapter);
5628
+ adapterMap.set(telegramAdapter.name, telegramAdapter);
4965
5629
  } catch (err) {
4966
5630
  console.error("[Telegram] Failed to start:", err);
4967
5631
  }
@@ -4981,11 +5645,48 @@ async function startServer(options) {
4981
5645
  });
4982
5646
  await slackAdapter.start({ agent, server, app, rootDir, lifecycle });
4983
5647
  adapters.push(slackAdapter);
5648
+ adapterMap.set(slackAdapter.name, slackAdapter);
4984
5649
  } catch (err) {
4985
5650
  console.error("[Slack] Failed to start:", err);
4986
5651
  }
4987
5652
  }
4988
5653
  }
5654
+ const { isMessageSender: isMessageSender2 } = await Promise.resolve().then(() => (init_types(), types_exports));
5655
+ const notifyFn = async (adapterName, channelId, text) => {
5656
+ const adapter = adapterMap.get(adapterName);
5657
+ if (!adapter || !isMessageSender2(adapter)) {
5658
+ console.warn(
5659
+ `[Scheduler] Target adapter "${adapterName}" not found or doesn't support sendMessage`
5660
+ );
5661
+ return;
5662
+ }
5663
+ await adapter.sendMessage(channelId, text);
5664
+ };
5665
+ const scheduledJobs = dataConfig.scheduledJobs || [];
5666
+ let schedulerAdapter = null;
5667
+ try {
5668
+ const { SchedulerAdapter: SchedulerAdapter2 } = await Promise.resolve().then(() => (init_scheduler(), scheduler_exports));
5669
+ schedulerAdapter = new SchedulerAdapter2();
5670
+ await schedulerAdapter.start({
5671
+ agent,
5672
+ server,
5673
+ app,
5674
+ rootDir,
5675
+ lifecycle,
5676
+ notify: notifyFn,
5677
+ adapterMap
5678
+ });
5679
+ adapters.push(schedulerAdapter);
5680
+ adapterMap.set(schedulerAdapter.name, schedulerAdapter);
5681
+ if (scheduledJobs.length > 0) {
5682
+ console.log(`[Server] Scheduler started with ${scheduledJobs.length} job(s)`);
5683
+ }
5684
+ } catch (err) {
5685
+ console.error("[Scheduler] Failed to start:", err);
5686
+ }
5687
+ if (schedulerAdapter) {
5688
+ agent.setScheduler(schedulerAdapter);
5689
+ }
4989
5690
  lifecycle.registerAdapters(adapters);
4990
5691
  server.once("listening", () => {
4991
5692
  const address = server.address();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cremini/skillpack",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "Pack AI Skills into Local Agents",
5
5
  "type": "module",
6
6
  "repository": {
@@ -49,6 +49,7 @@
49
49
  "commander": "^14.0.3",
50
50
  "express": "^5.1.0",
51
51
  "inquirer": "^13.3.0",
52
+ "node-cron": "^4.2.1",
52
53
  "node-telegram-bot-api": "^0.66.0",
53
54
  "ws": "^8.19.0"
54
55
  },
@@ -57,6 +58,7 @@
57
58
  "@types/express": "^5.0.0",
58
59
  "@types/inquirer": "^9.0.9",
59
60
  "@types/node": "^25.5.0",
61
+ "@types/node-cron": "^3.0.11",
60
62
  "@types/node-telegram-bot-api": "^0.64.0",
61
63
  "@types/ws": "^8.18.0",
62
64
  "prettier": "^3.8.1",