@cremini/skillpack 1.1.7 → 1.1.8-beta.1

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/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(
@@ -204,7 +289,7 @@ var telegram_exports = {};
204
289
  __export(telegram_exports, {
205
290
  TelegramAdapter: () => TelegramAdapter
206
291
  });
207
- import fs10 from "fs";
292
+ import fs11 from "fs";
208
293
  import TelegramBot from "node-telegram-bot-api";
209
294
  var COMMANDS2, MAX_MESSAGE_LENGTH, ACK_REACTION, TelegramAdapter;
210
295
  var init_telegram = __esm({
@@ -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) {
@@ -499,7 +599,7 @@ var init_telegram = __esm({
499
599
  async sendFileSafe(chatId, filePath, caption) {
500
600
  if (!this.bot) return;
501
601
  try {
502
- if (!fs10.existsSync(filePath)) {
602
+ if (!fs11.existsSync(filePath)) {
503
603
  console.error(`[Telegram] File not found for sending: ${filePath}`);
504
604
  return;
505
605
  }
@@ -519,8 +619,8 @@ var slack_exports = {};
519
619
  __export(slack_exports, {
520
620
  SlackAdapter: () => SlackAdapter
521
621
  });
522
- import fs11 from "fs";
523
- import path10 from "path";
622
+ import fs12 from "fs";
623
+ import path11 from "path";
524
624
  import { App, LogLevel } from "@slack/bolt";
525
625
  var INLINE_COMMANDS, SLASH_COMMANDS, MAX_MESSAGE_LENGTH2, ACK_REACTION2, SlackAdapter;
526
626
  var init_slack = __esm({
@@ -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
  // -------------------------------------------------------------------------
@@ -959,12 +1097,12 @@ var init_slack = __esm({
959
1097
  */
960
1098
  async sendFileSafe(client, route, filePath, caption) {
961
1099
  try {
962
- if (!fs11.existsSync(filePath)) {
1100
+ if (!fs12.existsSync(filePath)) {
963
1101
  console.error(`[Slack] File not found for sending: ${filePath}`);
964
1102
  return;
965
1103
  }
966
- const filename = path10.basename(filePath);
967
- const fileContent = fs11.readFileSync(filePath);
1104
+ const filename = path11.basename(filePath);
1105
+ const fileContent = fs12.readFileSync(filePath);
968
1106
  await client.files.uploadV2({
969
1107
  channel_id: route.channel,
970
1108
  thread_ts: route.threadTs,
@@ -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";
@@ -1547,15 +2036,15 @@ async function interactiveCreate(workDir) {
1547
2036
  }
1548
2037
 
1549
2038
  // src/commands/run.ts
1550
- import path12 from "path";
1551
- import fs13 from "fs";
2039
+ import path13 from "path";
2040
+ import fs14 from "fs";
1552
2041
  import inquirer2 from "inquirer";
1553
2042
  import chalk4 from "chalk";
1554
2043
 
1555
2044
  // src/runtime/server.ts
1556
2045
  import express from "express";
1557
- import path11 from "path";
1558
- import fs12 from "fs";
2046
+ import path12 from "path";
2047
+ import fs13 from "fs";
1559
2048
  import { fileURLToPath } from "url";
1560
2049
  import { createServer } from "http";
1561
2050
  import { exec } from "child_process";
@@ -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);
@@ -4680,8 +5269,7 @@ var WebAdapter = class {
4680
5269
  hasApiKey: !!conf.apiKey,
4681
5270
  apiKey: conf.apiKey || "",
4682
5271
  provider: conf.provider || "openai",
4683
- adapters: conf.adapters || {},
4684
- runtimeControl: lifecycle.getRuntimeControl()
5272
+ adapters: conf.adapters || {}
4685
5273
  });
4686
5274
  });
4687
5275
  app.get("/api/skills", (_req, res) => {
@@ -4710,22 +5298,12 @@ var WebAdapter = class {
4710
5298
  success: true,
4711
5299
  provider: newConf.provider,
4712
5300
  adapters: newConf.adapters,
4713
- requiresRestart,
4714
- runtimeControl: lifecycle.getRuntimeControl()
5301
+ requiresRestart
4715
5302
  });
4716
5303
  });
4717
5304
  app.post("/api/runtime/restart", async (_req, res) => {
4718
- const runtimeControl = lifecycle.getRuntimeControl();
4719
- if (!runtimeControl.canManagedRestart) {
4720
- res.status(409).json({
4721
- success: false,
4722
- message: "Managed restart is unavailable for this process.",
4723
- runtimeControl
4724
- });
4725
- return;
4726
- }
4727
5305
  const result = await lifecycle.requestRestart("web");
4728
- res.status(202).json({ ...result, runtimeControl });
5306
+ res.status(202).json(result);
4729
5307
  });
4730
5308
  app.delete("/api/chat", (_req, res) => {
4731
5309
  res.json({ success: true });
@@ -4761,6 +5339,75 @@ var WebAdapter = class {
4761
5339
  );
4762
5340
  fs9.createReadStream(resolvedPath).pipe(res);
4763
5341
  });
5342
+ const getScheduler = () => {
5343
+ const schedulerAdapter = ctx.adapterMap?.get("scheduler");
5344
+ if (!schedulerAdapter) return null;
5345
+ return schedulerAdapter;
5346
+ };
5347
+ app.get("/api/scheduler/jobs", (_req, res) => {
5348
+ const scheduler = getScheduler();
5349
+ if (!scheduler) {
5350
+ res.json([]);
5351
+ return;
5352
+ }
5353
+ res.json(scheduler.listJobs());
5354
+ });
5355
+ app.post("/api/scheduler/jobs", (req, res) => {
5356
+ const scheduler = getScheduler();
5357
+ if (!scheduler) {
5358
+ res.status(503).json({ success: false, message: "Scheduler not available" });
5359
+ return;
5360
+ }
5361
+ const { name, cron: cronExpr, prompt, notify, enabled, timezone } = req.body;
5362
+ if (!name || !cronExpr || !prompt || !notify?.adapter || !notify?.channelId) {
5363
+ res.status(400).json({
5364
+ success: false,
5365
+ message: "Required fields: name, cron, prompt, notify.adapter, notify.channelId"
5366
+ });
5367
+ return;
5368
+ }
5369
+ const result = scheduler.addJob({
5370
+ name,
5371
+ cron: cronExpr,
5372
+ prompt,
5373
+ notify,
5374
+ enabled: enabled !== false,
5375
+ timezone
5376
+ });
5377
+ res.json(result);
5378
+ });
5379
+ app.delete("/api/scheduler/jobs/:name", (req, res) => {
5380
+ const scheduler = getScheduler();
5381
+ if (!scheduler) {
5382
+ res.status(503).json({ success: false, message: "Scheduler not available" });
5383
+ return;
5384
+ }
5385
+ const result = scheduler.removeJob(req.params.name);
5386
+ res.json(result);
5387
+ });
5388
+ app.post("/api/scheduler/jobs/:name/trigger", async (req, res) => {
5389
+ const scheduler = getScheduler();
5390
+ if (!scheduler) {
5391
+ res.status(503).json({ success: false, message: "Scheduler not available" });
5392
+ return;
5393
+ }
5394
+ const result = await scheduler.triggerJob(req.params.name);
5395
+ res.json(result);
5396
+ });
5397
+ app.patch("/api/scheduler/jobs/:name", (req, res) => {
5398
+ const scheduler = getScheduler();
5399
+ if (!scheduler) {
5400
+ res.status(503).json({ success: false, message: "Scheduler not available" });
5401
+ return;
5402
+ }
5403
+ const { enabled } = req.body;
5404
+ if (typeof enabled !== "boolean") {
5405
+ res.status(400).json({ success: false, message: "Field 'enabled' (boolean) is required" });
5406
+ return;
5407
+ }
5408
+ const result = scheduler.setEnabled(req.params.name, enabled);
5409
+ res.json(result);
5410
+ });
4764
5411
  this.wss = new WebSocketServer({ noServer: true });
4765
5412
  server.on("upgrade", (request, socket, head) => {
4766
5413
  if (request.url?.startsWith("/api/chat")) {
@@ -4841,33 +5488,25 @@ var WebAdapter = class {
4841
5488
  }
4842
5489
  };
4843
5490
 
5491
+ // src/runtime/server.ts
5492
+ init_config();
5493
+
4844
5494
  // src/runtime/lifecycle.ts
4845
5495
  var SHUTDOWN_EXIT_CODE = 64;
4846
5496
  var RESTART_EXIT_CODE = 75;
4847
5497
  var STOP_TIMEOUT_MS = 3e3;
4848
- function detectProcessManager() {
4849
- return process.env.PACK_ROOT ? "wrapper" : "none";
4850
- }
4851
5498
  var Lifecycle = class {
4852
5499
  server;
4853
5500
  exitFn;
4854
- processManager;
4855
5501
  adapters = [];
4856
5502
  stopReason = null;
4857
5503
  constructor(server, exitFn = (code) => process.exit(code)) {
4858
5504
  this.server = server;
4859
5505
  this.exitFn = exitFn;
4860
- this.processManager = detectProcessManager();
4861
5506
  }
4862
5507
  registerAdapters(adapters) {
4863
5508
  this.adapters = adapters;
4864
5509
  }
4865
- getRuntimeControl() {
4866
- return {
4867
- canManagedRestart: this.processManager === "wrapper",
4868
- processManager: this.processManager
4869
- };
4870
- }
4871
5510
  async requestRestart(trigger) {
4872
5511
  return this.requestStop("restart", trigger);
4873
5512
  }
@@ -4923,25 +5562,197 @@ var Lifecycle = class {
4923
5562
  }
4924
5563
  };
4925
5564
 
5565
+ // src/runtime/registry.ts
5566
+ import crypto from "crypto";
5567
+ import fs10 from "fs";
5568
+ import os from "os";
5569
+ import path10 from "path";
5570
+ var SKILLPACK_HOME = path10.join(os.homedir(), ".skillpack");
5571
+ var LEGACY_REGISTRY_FILE = path10.join(SKILLPACK_HOME, "registry.json");
5572
+ var REGISTRY_DIR = path10.join(SKILLPACK_HOME, "registry.d");
5573
+ var migrationChecked = false;
5574
+ function ensureHomeDir() {
5575
+ if (!fs10.existsSync(SKILLPACK_HOME)) {
5576
+ fs10.mkdirSync(SKILLPACK_HOME, { recursive: true });
5577
+ }
5578
+ }
5579
+ function ensureRegistryDir() {
5580
+ ensureHomeDir();
5581
+ if (!fs10.existsSync(REGISTRY_DIR)) {
5582
+ fs10.mkdirSync(REGISTRY_DIR, { recursive: true });
5583
+ }
5584
+ }
5585
+ function canonicalizeDir(dir) {
5586
+ const resolved = path10.resolve(dir);
5587
+ try {
5588
+ return fs10.realpathSync(resolved);
5589
+ } catch {
5590
+ return resolved;
5591
+ }
5592
+ }
5593
+ function hashDir(dir) {
5594
+ return crypto.createHash("md5").update(canonicalizeDir(dir)).digest("hex");
5595
+ }
5596
+ function getEntryPathForCanonicalDir(dir) {
5597
+ return path10.join(REGISTRY_DIR, `${hashDir(dir)}.json`);
5598
+ }
5599
+ function getEntryPath(dir) {
5600
+ ensureRegistryReady();
5601
+ return getEntryPathForCanonicalDir(canonicalizeDir(dir));
5602
+ }
5603
+ function listEntryFiles() {
5604
+ ensureRegistryReady();
5605
+ return fs10.readdirSync(REGISTRY_DIR).filter((file) => file.endsWith(".json")).sort().map((file) => path10.join(REGISTRY_DIR, file));
5606
+ }
5607
+ function readEntryFile(filePath) {
5608
+ try {
5609
+ const raw = fs10.readFileSync(filePath, "utf-8");
5610
+ const data = JSON.parse(raw);
5611
+ 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") {
5612
+ return null;
5613
+ }
5614
+ return {
5615
+ dir: canonicalizeDir(data.dir),
5616
+ name: data.name,
5617
+ version: data.version,
5618
+ port: data.port,
5619
+ pid: data.pid,
5620
+ status: data.status,
5621
+ startedAt: data.startedAt,
5622
+ stoppedAt: data.stoppedAt,
5623
+ updatedAt: data.updatedAt
5624
+ };
5625
+ } catch {
5626
+ return null;
5627
+ }
5628
+ }
5629
+ function createTmpPath(entryPath) {
5630
+ const suffix = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
5631
+ return `${entryPath}.tmp.${suffix}`;
5632
+ }
5633
+ function writeEntryFile(entry) {
5634
+ ensureRegistryReady();
5635
+ const normalized = {
5636
+ ...entry,
5637
+ dir: canonicalizeDir(entry.dir),
5638
+ updatedAt: entry.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
5639
+ };
5640
+ const entryPath = getEntryPathForCanonicalDir(normalized.dir);
5641
+ const tmpPath = createTmpPath(entryPath);
5642
+ fs10.writeFileSync(tmpPath, JSON.stringify(normalized, null, 2), "utf-8");
5643
+ fs10.renameSync(tmpPath, entryPath);
5644
+ }
5645
+ function migrateLegacyRegistryIfNeeded() {
5646
+ if (migrationChecked) {
5647
+ return;
5648
+ }
5649
+ migrationChecked = true;
5650
+ ensureRegistryDir();
5651
+ if (!fs10.existsSync(LEGACY_REGISTRY_FILE)) {
5652
+ return;
5653
+ }
5654
+ if (listEntryFiles().length > 0) {
5655
+ return;
5656
+ }
5657
+ try {
5658
+ const raw = fs10.readFileSync(LEGACY_REGISTRY_FILE, "utf-8");
5659
+ const data = JSON.parse(raw);
5660
+ const packs = Array.isArray(data?.packs) ? data.packs : [];
5661
+ for (const pack of packs) {
5662
+ try {
5663
+ writeEntryFile({
5664
+ ...pack,
5665
+ dir: canonicalizeDir(pack.dir),
5666
+ updatedAt: pack.updatedAt ?? pack.stoppedAt ?? pack.startedAt ?? (/* @__PURE__ */ new Date()).toISOString()
5667
+ });
5668
+ } catch {
5669
+ }
5670
+ }
5671
+ fs10.renameSync(LEGACY_REGISTRY_FILE, `${LEGACY_REGISTRY_FILE}.legacy`);
5672
+ } catch (err) {
5673
+ console.warn(" [Registry] Failed to migrate legacy registry.json:", err);
5674
+ }
5675
+ }
5676
+ function ensureRegistryReady() {
5677
+ ensureRegistryDir();
5678
+ migrateLegacyRegistryIfNeeded();
5679
+ }
5680
+ function readEntry(dir) {
5681
+ ensureRegistryReady();
5682
+ return readEntryFile(getEntryPath(dir));
5683
+ }
5684
+ function register(opts) {
5685
+ try {
5686
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5687
+ const entry = {
5688
+ dir: canonicalizeDir(opts.dir),
5689
+ name: opts.name,
5690
+ version: opts.version,
5691
+ port: opts.port,
5692
+ pid: process.pid,
5693
+ status: "running",
5694
+ startedAt: now,
5695
+ updatedAt: now
5696
+ };
5697
+ writeEntryFile(entry);
5698
+ console.log(` [Registry] Registered "${opts.name}" (pid ${process.pid})`);
5699
+ } catch (err) {
5700
+ console.warn(" [Registry] Failed to register:", err);
5701
+ }
5702
+ }
5703
+ function deregister(dir, pid) {
5704
+ try {
5705
+ const entry = readEntry(dir);
5706
+ if (!entry || entry.pid !== pid) {
5707
+ return;
5708
+ }
5709
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5710
+ writeEntryFile({
5711
+ ...entry,
5712
+ pid: null,
5713
+ status: "stopped",
5714
+ stoppedAt: now,
5715
+ updatedAt: now
5716
+ });
5717
+ console.log(` [Registry] Deregistered "${entry.name}"`);
5718
+ } catch (err) {
5719
+ console.warn(" [Registry] Failed to deregister:", err);
5720
+ }
5721
+ }
5722
+
4926
5723
  // src/runtime/server.ts
4927
- var __dirname = path11.dirname(fileURLToPath(import.meta.url));
5724
+ var __dirname = path12.dirname(fileURLToPath(import.meta.url));
4928
5725
  async function startServer(options) {
4929
5726
  const {
4930
5727
  rootDir,
4931
5728
  host = process.env.HOST || "127.0.0.1",
4932
5729
  port = Number(process.env.PORT) || 26313,
4933
- firstRun = true
5730
+ daemonRun = false
4934
5731
  } = options;
4935
5732
  const dataConfig = configManager.load(rootDir);
4936
5733
  const apiKey = dataConfig.apiKey || "";
4937
5734
  const provider = dataConfig.provider || "openai";
5735
+ const canonicalRootDir = canonicalizeDir(rootDir);
5736
+ const packConfig = loadConfig(canonicalRootDir);
4938
5737
  const modelId = provider === "anthropic" ? "claude-opus-4-6" : "gpt-5.4";
4939
- const packageRoot = path11.resolve(__dirname, "..");
4940
- const webDir = fs12.existsSync(path11.join(rootDir, "web")) ? path11.join(rootDir, "web") : path11.join(packageRoot, "web");
5738
+ const packageRoot = path12.resolve(__dirname, "..");
5739
+ const webDir = fs13.existsSync(path12.join(rootDir, "web")) ? path12.join(rootDir, "web") : path12.join(packageRoot, "web");
4941
5740
  const app = express();
4942
5741
  app.use(express.json());
4943
5742
  app.use(express.static(webDir));
4944
5743
  const server = createServer(app);
5744
+ app.get("/api/health", (_req, res) => {
5745
+ const address = server.address();
5746
+ const actualPort = typeof address === "string" ? port : address?.port ?? port;
5747
+ res.json({
5748
+ status: "ok",
5749
+ dir: canonicalRootDir,
5750
+ name: packConfig.name,
5751
+ version: packConfig.version,
5752
+ port: actualPort,
5753
+ pid: process.pid
5754
+ });
5755
+ });
4945
5756
  const lifecycle = new Lifecycle(server);
4946
5757
  const agent = new PackAgent({
4947
5758
  apiKey,
@@ -4951,9 +5762,11 @@ async function startServer(options) {
4951
5762
  lifecycleHandler: lifecycle
4952
5763
  });
4953
5764
  const adapters = [];
5765
+ const adapterMap = /* @__PURE__ */ new Map();
4954
5766
  const webAdapter = new WebAdapter();
4955
- await webAdapter.start({ agent, server, app, rootDir, lifecycle });
5767
+ await webAdapter.start({ agent, server, app, rootDir, lifecycle, adapterMap });
4956
5768
  adapters.push(webAdapter);
5769
+ adapterMap.set(webAdapter.name, webAdapter);
4957
5770
  if (dataConfig.adapters?.telegram?.token) {
4958
5771
  try {
4959
5772
  const { TelegramAdapter: TelegramAdapter2 } = await Promise.resolve().then(() => (init_telegram(), telegram_exports));
@@ -4962,6 +5775,7 @@ async function startServer(options) {
4962
5775
  });
4963
5776
  await telegramAdapter.start({ agent, server, app, rootDir, lifecycle });
4964
5777
  adapters.push(telegramAdapter);
5778
+ adapterMap.set(telegramAdapter.name, telegramAdapter);
4965
5779
  } catch (err) {
4966
5780
  console.error("[Telegram] Failed to start:", err);
4967
5781
  }
@@ -4981,11 +5795,48 @@ async function startServer(options) {
4981
5795
  });
4982
5796
  await slackAdapter.start({ agent, server, app, rootDir, lifecycle });
4983
5797
  adapters.push(slackAdapter);
5798
+ adapterMap.set(slackAdapter.name, slackAdapter);
4984
5799
  } catch (err) {
4985
5800
  console.error("[Slack] Failed to start:", err);
4986
5801
  }
4987
5802
  }
4988
5803
  }
5804
+ const { isMessageSender: isMessageSender2 } = await Promise.resolve().then(() => (init_types(), types_exports));
5805
+ const notifyFn = async (adapterName, channelId, text) => {
5806
+ const adapter = adapterMap.get(adapterName);
5807
+ if (!adapter || !isMessageSender2(adapter)) {
5808
+ console.warn(
5809
+ `[Scheduler] Target adapter "${adapterName}" not found or doesn't support sendMessage`
5810
+ );
5811
+ return;
5812
+ }
5813
+ await adapter.sendMessage(channelId, text);
5814
+ };
5815
+ const scheduledJobs = dataConfig.scheduledJobs || [];
5816
+ let schedulerAdapter = null;
5817
+ try {
5818
+ const { SchedulerAdapter: SchedulerAdapter2 } = await Promise.resolve().then(() => (init_scheduler(), scheduler_exports));
5819
+ schedulerAdapter = new SchedulerAdapter2();
5820
+ await schedulerAdapter.start({
5821
+ agent,
5822
+ server,
5823
+ app,
5824
+ rootDir,
5825
+ lifecycle,
5826
+ notify: notifyFn,
5827
+ adapterMap
5828
+ });
5829
+ adapters.push(schedulerAdapter);
5830
+ adapterMap.set(schedulerAdapter.name, schedulerAdapter);
5831
+ if (scheduledJobs.length > 0) {
5832
+ console.log(`[Server] Scheduler started with ${scheduledJobs.length} job(s)`);
5833
+ }
5834
+ } catch (err) {
5835
+ console.error("[Scheduler] Failed to start:", err);
5836
+ }
5837
+ if (schedulerAdapter) {
5838
+ agent.setScheduler(schedulerAdapter);
5839
+ }
4989
5840
  lifecycle.registerAdapters(adapters);
4990
5841
  server.once("listening", () => {
4991
5842
  const address = server.address();
@@ -4995,7 +5846,17 @@ async function startServer(options) {
4995
5846
  Skills Pack Server`);
4996
5847
  console.log(` Running at ${url}
4997
5848
  `);
4998
- if (firstRun) {
5849
+ try {
5850
+ register({
5851
+ dir: canonicalRootDir,
5852
+ name: packConfig.name,
5853
+ version: packConfig.version,
5854
+ port: typeof actualPort === "number" ? actualPort : port
5855
+ });
5856
+ } catch (err) {
5857
+ console.warn(" [Registry] Could not register pack:", err);
5858
+ }
5859
+ if (!daemonRun) {
4999
5860
  const cmd = process.platform === "darwin" ? `open ${url}` : process.platform === "win32" ? `start ${url}` : `xdg-open ${url}`;
5000
5861
  exec(cmd, (err) => {
5001
5862
  if (err) console.warn(` Could not open browser: ${err.message}`);
@@ -5003,9 +5864,11 @@ async function startServer(options) {
5003
5864
  }
5004
5865
  });
5005
5866
  process.on("SIGINT", () => {
5867
+ deregister(canonicalRootDir, process.pid);
5006
5868
  void lifecycle.requestShutdown("signal");
5007
5869
  });
5008
5870
  process.on("SIGTERM", () => {
5871
+ deregister(canonicalRootDir, process.pid);
5009
5872
  void lifecycle.requestShutdown("signal");
5010
5873
  });
5011
5874
  await new Promise((resolve, reject) => {
@@ -5040,23 +5903,23 @@ function findMissingSkills(workDir, config) {
5040
5903
  });
5041
5904
  }
5042
5905
  function copyStartTemplates2(workDir) {
5043
- const templateDir = path12.resolve(
5906
+ const templateDir = path13.resolve(
5044
5907
  new URL("../templates", import.meta.url).pathname
5045
5908
  );
5046
5909
  for (const file of ["start.sh", "start.bat"]) {
5047
- const src = path12.join(templateDir, file);
5048
- const dest = path12.join(workDir, file);
5049
- if (fs13.existsSync(src)) {
5050
- fs13.copyFileSync(src, dest);
5910
+ const src = path13.join(templateDir, file);
5911
+ const dest = path13.join(workDir, file);
5912
+ if (fs14.existsSync(src)) {
5913
+ fs14.copyFileSync(src, dest);
5051
5914
  if (file === "start.sh") {
5052
- fs13.chmodSync(dest, 493);
5915
+ fs14.chmodSync(dest, 493);
5053
5916
  }
5054
5917
  }
5055
5918
  }
5056
5919
  }
5057
5920
  async function runCommand(directory) {
5058
- const workDir = directory ? path12.resolve(directory) : process.cwd();
5059
- fs13.mkdirSync(workDir, { recursive: true });
5921
+ const workDir = directory ? path13.resolve(directory) : process.cwd();
5922
+ fs14.mkdirSync(workDir, { recursive: true });
5060
5923
  if (!configExists(workDir)) {
5061
5924
  console.log(chalk4.blue("\n No skillpack.json found. Let's set one up.\n"));
5062
5925
  const { name, description } = await inquirer2.prompt([
@@ -5092,13 +5955,16 @@ async function runCommand(directory) {
5092
5955
  console.warn(chalk4.yellow(` Warning: Some skills could not be installed: ${err}`));
5093
5956
  }
5094
5957
  }
5095
- await startServer({ rootDir: workDir, firstRun: true });
5958
+ await startServer({
5959
+ rootDir: workDir,
5960
+ daemonRun: process.env.DAEMON_RUN === "1"
5961
+ });
5096
5962
  }
5097
5963
 
5098
5964
  // src/cli.ts
5099
- import fs14 from "fs";
5965
+ import fs15 from "fs";
5100
5966
  var packageJson = JSON.parse(
5101
- fs14.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
5967
+ fs15.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
5102
5968
  );
5103
5969
  var program = new Command();
5104
5970
  program.name("skillpack").description("Assemble, package, and run Agent Skills packs").version(packageJson.version);