@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.
- package/README.md +19 -11
- package/dist/cli.js +781 -80
- 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/
|
|
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.
|
|
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",
|