@cremini/skillpack 1.2.11 → 1.2.13

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
@@ -42,6 +42,11 @@ function validateScheduledJobConfig(value, sourceLabel, index) {
42
42
  );
43
43
  }
44
44
  const job = value;
45
+ if (typeof job.id !== "string" || !job.id.trim()) {
46
+ throw new Error(
47
+ `Invalid job config from ${sourceLabel}: "jobs[${index}].id" is required`
48
+ );
49
+ }
45
50
  if (typeof job.name !== "string" || !job.name.trim()) {
46
51
  throw new Error(
47
52
  `Invalid job config from ${sourceLabel}: "jobs[${index}].name" is required`
@@ -92,16 +97,16 @@ function validateJobFileShape(value, sourceLabel) {
92
97
  if (!Array.isArray(jobFile.jobs)) {
93
98
  throw new Error(`Invalid job config from ${sourceLabel}: "jobs" must be an array`);
94
99
  }
95
- const names = /* @__PURE__ */ new Set();
100
+ const ids = /* @__PURE__ */ new Set();
96
101
  jobFile.jobs.forEach((job, index) => {
97
102
  validateScheduledJobConfig(job, sourceLabel, index);
98
- const normalizedName = job.name.trim().toLowerCase();
99
- if (names.has(normalizedName)) {
103
+ const normalizedId = job.id.trim().toLowerCase();
104
+ if (ids.has(normalizedId)) {
100
105
  throw new Error(
101
- `Invalid job config from ${sourceLabel}: duplicate job name "${job.name}" is not allowed`
106
+ `Invalid job config from ${sourceLabel}: duplicate job id "${job.id}" is not allowed`
102
107
  );
103
108
  }
104
- names.add(normalizedName);
109
+ ids.add(normalizedId);
105
110
  });
106
111
  }
107
112
  function normalizeJobFile(jobFile) {
@@ -115,6 +120,7 @@ function normalizeScheduledJobConfig(job) {
115
120
  const normalizedCron = normalizeJobCron(job.cron);
116
121
  const normalizedTimezone = typeof job.timezone === "string" && job.timezone.trim() ? job.timezone.trim() : void 0;
117
122
  return {
123
+ id: job.id.trim(),
118
124
  name: job.name.trim(),
119
125
  ...normalizedCron ? { cron: normalizedCron } : {},
120
126
  prompt: job.prompt,
@@ -286,8 +292,16 @@ var init_commands = __esm({
286
292
  // src/runtime/adapters/types.ts
287
293
  var types_exports = {};
288
294
  __export(types_exports, {
295
+ detectPlatformFromChannelId: () => detectPlatformFromChannelId,
289
296
  isMessageSender: () => isMessageSender
290
297
  });
298
+ function detectPlatformFromChannelId(channelId) {
299
+ if (channelId.startsWith("telegram-")) return "telegram";
300
+ if (channelId.startsWith("slack-")) return "slack";
301
+ if (channelId.startsWith("feishu-")) return "feishu";
302
+ if (channelId.startsWith("scheduler-")) return "scheduler";
303
+ return "web";
304
+ }
291
305
  function isMessageSender(adapter) {
292
306
  return typeof adapter.sendMessage === "function";
293
307
  }
@@ -1413,6 +1427,297 @@ var init_slack = __esm({
1413
1427
  }
1414
1428
  });
1415
1429
 
1430
+ // src/runtime/adapters/feishu.ts
1431
+ var feishu_exports = {};
1432
+ __export(feishu_exports, {
1433
+ FeishuAdapter: () => FeishuAdapter,
1434
+ normalizeFeishuMessage: () => normalizeFeishuMessage,
1435
+ parseFeishuChannelId: () => parseFeishuChannelId
1436
+ });
1437
+ import fs16 from "fs";
1438
+ import path16 from "path";
1439
+ import * as Lark from "@larksuiteoapi/node-sdk";
1440
+ function parseFeishuChannelId(channelId) {
1441
+ if (!channelId.startsWith("feishu-")) {
1442
+ throw new Error(`[Feishu] Invalid channelId: ${channelId}`);
1443
+ }
1444
+ const chatId = channelId.replace("feishu-", "").trim();
1445
+ if (!chatId) {
1446
+ throw new Error(`[Feishu] Invalid channelId: ${channelId}`);
1447
+ }
1448
+ return chatId;
1449
+ }
1450
+ function normalizeFeishuMessage(message) {
1451
+ if (message.chatType === "group" && !message.mentionedBot) {
1452
+ return { action: "ignore" };
1453
+ }
1454
+ if (message.rawContentType !== "text") {
1455
+ return { action: "unsupported" };
1456
+ }
1457
+ const text = message.content.trim();
1458
+ if (!text) {
1459
+ return { action: "ignore" };
1460
+ }
1461
+ return {
1462
+ action: "handle",
1463
+ text
1464
+ };
1465
+ }
1466
+ var MAX_MESSAGE_LENGTH3, ACK_REACTION3, UNSUPPORTED_MESSAGE_REPLY, FILE_DELIVERY_FAILED_REPLY, FeishuAdapter;
1467
+ var init_feishu = __esm({
1468
+ "src/runtime/adapters/feishu.ts"() {
1469
+ "use strict";
1470
+ init_commands();
1471
+ MAX_MESSAGE_LENGTH3 = 1800;
1472
+ ACK_REACTION3 = "THUMBSUP";
1473
+ UNSUPPORTED_MESSAGE_REPLY = "Only text messages are currently supported. Please send plain text.";
1474
+ FILE_DELIVERY_FAILED_REPLY = "A file was generated, but Feishu could not deliver it.";
1475
+ FeishuAdapter = class {
1476
+ name = "feishu";
1477
+ channel = null;
1478
+ agent = null;
1479
+ ipcBroadcaster = null;
1480
+ options;
1481
+ constructor(options) {
1482
+ this.options = options;
1483
+ }
1484
+ async start(ctx) {
1485
+ this.agent = ctx.agent;
1486
+ this.ipcBroadcaster = ctx.ipcBroadcaster ?? null;
1487
+ this.channel = Lark.createLarkChannel({
1488
+ appId: this.options.appId,
1489
+ appSecret: this.options.appSecret,
1490
+ loggerLevel: Lark.LoggerLevel.info,
1491
+ transport: "websocket",
1492
+ source: "skillpack",
1493
+ policy: {
1494
+ dmMode: "open",
1495
+ requireMention: false
1496
+ }
1497
+ });
1498
+ this.channel.on("message", (message) => {
1499
+ void this.handleIncomingMessage(message).catch((error) => {
1500
+ console.error("[Feishu] Error handling message:", error);
1501
+ });
1502
+ });
1503
+ this.channel.on("error", (error) => {
1504
+ console.error("[Feishu] Channel error:", error);
1505
+ });
1506
+ await this.channel.connect();
1507
+ const botName = this.channel.botIdentity?.name;
1508
+ console.log(
1509
+ botName ? `[FeishuAdapter] Started as ${botName}` : "[FeishuAdapter] Started"
1510
+ );
1511
+ }
1512
+ async stop() {
1513
+ if (this.channel) {
1514
+ await this.channel.disconnect();
1515
+ this.channel = null;
1516
+ }
1517
+ console.log("[FeishuAdapter] Stopped");
1518
+ }
1519
+ async sendMessage(channelId, text) {
1520
+ if (!this.channel) {
1521
+ throw new Error("[Feishu] Channel not initialized");
1522
+ }
1523
+ const chatId = parseFeishuChannelId(channelId);
1524
+ await this.sendLongMessage(chatId, text);
1525
+ }
1526
+ async sendFile(channelId, filePath, caption) {
1527
+ if (!this.channel) {
1528
+ throw new Error("[Feishu] Channel not initialized");
1529
+ }
1530
+ const chatId = parseFeishuChannelId(channelId);
1531
+ const sent = await this.sendFileSafe(chatId, filePath, caption);
1532
+ if (!sent) {
1533
+ throw new Error(`[Feishu] Failed to send file: ${filePath}`);
1534
+ }
1535
+ }
1536
+ async handleIncomingMessage(message) {
1537
+ if (!this.channel || !this.agent) {
1538
+ return;
1539
+ }
1540
+ const decision = normalizeFeishuMessage(message);
1541
+ if (decision.action === "ignore") {
1542
+ return;
1543
+ }
1544
+ await this.tryAckReaction(message.messageId);
1545
+ const channelId = `feishu-${message.chatId}`;
1546
+ if (decision.action === "unsupported") {
1547
+ await this.sendSafe(message.chatId, UNSUPPORTED_MESSAGE_REPLY, message.messageId);
1548
+ return;
1549
+ }
1550
+ const userText = decision.text;
1551
+ this.ipcBroadcaster?.broadcastInbound(
1552
+ channelId,
1553
+ "feishu",
1554
+ {
1555
+ id: message.senderId,
1556
+ username: message.senderName || message.senderId
1557
+ },
1558
+ userText
1559
+ );
1560
+ const command = this.resolveCommand(userText);
1561
+ if (command) {
1562
+ const result = await this.agent.handleCommand(command, channelId);
1563
+ await this.sendSafe(
1564
+ message.chatId,
1565
+ result.message || `/${command} executed.`,
1566
+ message.messageId
1567
+ );
1568
+ return;
1569
+ }
1570
+ let finalText = "";
1571
+ let hasError = false;
1572
+ let errorMessage = "";
1573
+ const pendingFiles = [];
1574
+ const onEvent = (event) => {
1575
+ switch (event.type) {
1576
+ case "text_delta":
1577
+ finalText += event.delta;
1578
+ break;
1579
+ case "file_output":
1580
+ pendingFiles.push({
1581
+ filePath: event.filePath,
1582
+ caption: event.caption
1583
+ });
1584
+ break;
1585
+ }
1586
+ this.ipcBroadcaster?.broadcastAgentEvent(channelId, event);
1587
+ };
1588
+ try {
1589
+ const result = await this.agent.handleMessage(
1590
+ "feishu",
1591
+ channelId,
1592
+ userText,
1593
+ onEvent
1594
+ );
1595
+ if (result.errorMessage) {
1596
+ hasError = true;
1597
+ errorMessage = result.errorMessage;
1598
+ }
1599
+ } catch (error) {
1600
+ hasError = true;
1601
+ errorMessage = error instanceof Error ? error.message : String(error);
1602
+ }
1603
+ if (hasError) {
1604
+ await this.sendSafe(message.chatId, `Error: ${errorMessage}`, message.messageId);
1605
+ return;
1606
+ }
1607
+ if (finalText.trim()) {
1608
+ await this.sendLongMessage(message.chatId, finalText, message.messageId);
1609
+ } else if (pendingFiles.length === 0) {
1610
+ await this.sendSafe(message.chatId, "(No response generated)", message.messageId);
1611
+ }
1612
+ let sentFileCount = 0;
1613
+ for (const file of pendingFiles) {
1614
+ if (await this.sendFileSafe(
1615
+ message.chatId,
1616
+ file.filePath,
1617
+ file.caption,
1618
+ message.messageId
1619
+ )) {
1620
+ sentFileCount += 1;
1621
+ }
1622
+ }
1623
+ if (pendingFiles.length > 0 && sentFileCount === 0) {
1624
+ await this.sendSafe(
1625
+ message.chatId,
1626
+ FILE_DELIVERY_FAILED_REPLY,
1627
+ message.messageId
1628
+ );
1629
+ }
1630
+ }
1631
+ resolveCommand(text) {
1632
+ return resolveCommand(text);
1633
+ }
1634
+ async sendLongMessage(chatId, text, replyTo) {
1635
+ for (const chunk of this.splitMessage(text)) {
1636
+ await this.sendSafe(chatId, chunk, replyTo);
1637
+ }
1638
+ }
1639
+ splitMessage(text) {
1640
+ if (text.length <= MAX_MESSAGE_LENGTH3) {
1641
+ return [text];
1642
+ }
1643
+ const chunks = [];
1644
+ let remaining = text;
1645
+ while (remaining.length > 0) {
1646
+ if (remaining.length <= MAX_MESSAGE_LENGTH3) {
1647
+ chunks.push(remaining);
1648
+ break;
1649
+ }
1650
+ let splitAt = remaining.lastIndexOf("\n\n", MAX_MESSAGE_LENGTH3);
1651
+ if (splitAt < MAX_MESSAGE_LENGTH3 * 0.5) {
1652
+ splitAt = remaining.lastIndexOf("\n", MAX_MESSAGE_LENGTH3);
1653
+ }
1654
+ if (splitAt < MAX_MESSAGE_LENGTH3 * 0.3) {
1655
+ splitAt = remaining.lastIndexOf(" ", MAX_MESSAGE_LENGTH3);
1656
+ }
1657
+ if (splitAt < 1) {
1658
+ splitAt = MAX_MESSAGE_LENGTH3;
1659
+ }
1660
+ chunks.push(remaining.slice(0, splitAt));
1661
+ remaining = remaining.slice(splitAt).trimStart();
1662
+ }
1663
+ return chunks;
1664
+ }
1665
+ async tryAckReaction(messageId) {
1666
+ if (!this.channel) {
1667
+ return;
1668
+ }
1669
+ try {
1670
+ await this.channel.addReaction(messageId, ACK_REACTION3);
1671
+ } catch (error) {
1672
+ console.error("[Feishu] Failed to add ack reaction:", error);
1673
+ }
1674
+ }
1675
+ async sendFileSafe(chatId, filePath, caption, replyTo) {
1676
+ if (!this.channel) {
1677
+ return false;
1678
+ }
1679
+ try {
1680
+ if (!fs16.existsSync(filePath)) {
1681
+ console.error(`[Feishu] File not found for sending: ${filePath}`);
1682
+ return false;
1683
+ }
1684
+ if (caption?.trim()) {
1685
+ await this.sendSafe(chatId, caption, replyTo);
1686
+ }
1687
+ await this.channel.send(
1688
+ chatId,
1689
+ {
1690
+ file: {
1691
+ source: filePath,
1692
+ fileName: path16.basename(filePath)
1693
+ }
1694
+ },
1695
+ replyTo ? { replyTo } : void 0
1696
+ );
1697
+ return true;
1698
+ } catch (error) {
1699
+ console.error("[Feishu] Failed to send file:", error);
1700
+ return false;
1701
+ }
1702
+ }
1703
+ async sendSafe(chatId, text, replyTo) {
1704
+ if (!this.channel) {
1705
+ return;
1706
+ }
1707
+ try {
1708
+ await this.channel.send(
1709
+ chatId,
1710
+ { markdown: text },
1711
+ replyTo ? { replyTo } : void 0
1712
+ );
1713
+ } catch (error) {
1714
+ console.error("[Feishu] Failed to send message:", error);
1715
+ }
1716
+ }
1717
+ };
1718
+ }
1719
+ });
1720
+
1416
1721
  // src/runtime/adapters/scheduler.ts
1417
1722
  var scheduler_exports = {};
1418
1723
  __export(scheduler_exports, {
@@ -1427,19 +1732,19 @@ function isValidTimezone(tz) {
1427
1732
  return false;
1428
1733
  }
1429
1734
  }
1430
- function isValidJobName(name) {
1431
- return VALID_JOB_NAME.test(name) && name.length <= 64;
1735
+ function isValidJobId(id) {
1736
+ return id.length > 0 && id.length <= 128 && !INVALID_JOB_ID_CHARS.test(id);
1432
1737
  }
1433
1738
  function isRecurringJob(jobConfig) {
1434
1739
  return hasJobSchedule(jobConfig);
1435
1740
  }
1436
- var VALID_JOB_NAME, SchedulerAdapter;
1741
+ var INVALID_JOB_ID_CHARS, SchedulerAdapter;
1437
1742
  var init_scheduler = __esm({
1438
1743
  "src/runtime/adapters/scheduler.ts"() {
1439
1744
  "use strict";
1440
1745
  init_job_config();
1441
1746
  init_job_schedule();
1442
- VALID_JOB_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
1747
+ INVALID_JOB_ID_CHARS = /[\\/\r\n]/;
1443
1748
  SchedulerAdapter = class {
1444
1749
  name = "scheduler";
1445
1750
  agent;
@@ -1493,8 +1798,8 @@ var init_scheduler = __esm({
1493
1798
  registerJob(jobConfig) {
1494
1799
  const normalizedConfig = normalizeScheduledJobConfig(jobConfig);
1495
1800
  const normalizedCron = normalizeJobCron(normalizedConfig.cron);
1496
- if (!isValidJobName(normalizedConfig.name)) {
1497
- const msg = `[Scheduler] Invalid job name "${normalizedConfig.name}": must match ${VALID_JOB_NAME} and be \u226464 chars`;
1801
+ if (!isValidJobId(normalizedConfig.id)) {
1802
+ const msg = `[Scheduler] Invalid job id "${normalizedConfig.id}": must be non-empty, must not contain "/", "\\\\", or line breaks, and must be \u2264128 chars`;
1498
1803
  console.error(msg);
1499
1804
  return { registered: false, message: msg };
1500
1805
  }
@@ -1510,7 +1815,7 @@ var init_scheduler = __esm({
1510
1815
  return { registered: false, message: msg };
1511
1816
  }
1512
1817
  }
1513
- this.removeFromMap(normalizedConfig.name);
1818
+ this.removeFromMap(normalizedConfig.id);
1514
1819
  let task = null;
1515
1820
  if (normalizedCron && normalizedConfig.enabled !== false) {
1516
1821
  task = this.createCronTask(normalizedConfig);
@@ -1526,7 +1831,7 @@ var init_scheduler = __esm({
1526
1831
  `[Scheduler] Job "${normalizedConfig.name}" registered as one-time (manual trigger only)`
1527
1832
  );
1528
1833
  }
1529
- this.jobs.set(normalizedConfig.name, {
1834
+ this.jobs.set(normalizedConfig.id, {
1530
1835
  config: normalizedConfig,
1531
1836
  task,
1532
1837
  running: false,
@@ -1542,8 +1847,8 @@ var init_scheduler = __esm({
1542
1847
  * Returns { text, notifyFailed } so callers can produce accurate status.
1543
1848
  */
1544
1849
  async runJob(jobConfig) {
1545
- const channelId = `scheduler-${jobConfig.name}`;
1546
- const job = this.jobs.get(jobConfig.name);
1850
+ const channelId = `scheduler-${jobConfig.id}`;
1851
+ const job = this.jobs.get(jobConfig.id);
1547
1852
  if (job?.running) {
1548
1853
  console.warn(
1549
1854
  `[Scheduler] Job "${jobConfig.name}" is already running, skipping this trigger`
@@ -1659,7 +1964,7 @@ var init_scheduler = __esm({
1659
1964
  * Add a new job, persist to job.json.
1660
1965
  */
1661
1966
  addJob(jobConfig) {
1662
- if (this.jobs.has(jobConfig.name)) {
1967
+ if (this.jobs.has(jobConfig.id)) {
1663
1968
  return {
1664
1969
  success: false,
1665
1970
  message: `Job "${jobConfig.name}" already exists. Remove it first.`
@@ -1680,21 +1985,23 @@ var init_scheduler = __esm({
1680
1985
  /**
1681
1986
  * Remove a job and persist to job.json.
1682
1987
  */
1683
- removeJob(name) {
1684
- if (!this.jobs.has(name)) {
1685
- return { success: false, message: `Job "${name}" not found.` };
1988
+ removeJob(id) {
1989
+ const job = this.jobs.get(id);
1990
+ if (!job) {
1991
+ return { success: false, message: `Job "${id}" not found.` };
1686
1992
  }
1687
- this.removeFromMap(name);
1993
+ this.removeFromMap(id);
1688
1994
  this.persistJobs();
1689
- return { success: true, message: `Job "${name}" removed.` };
1995
+ return { success: true, message: `Job "${job.config.name}" removed.` };
1690
1996
  }
1691
- updateJob(name, updates) {
1692
- const job = this.jobs.get(name);
1997
+ updateJob(id, updates) {
1998
+ const job = this.jobs.get(id);
1693
1999
  if (!job) {
1694
- return { success: false, message: `Job "${name}" not found.` };
2000
+ return { success: false, message: `Job "${id}" not found.` };
1695
2001
  }
1696
2002
  const nextConfig = {
1697
- name,
2003
+ id: job.config.id,
2004
+ name: job.config.name,
1698
2005
  cron: updates.cron,
1699
2006
  prompt: updates.prompt,
1700
2007
  notify: updates.notify,
@@ -1708,21 +2015,21 @@ var init_scheduler = __esm({
1708
2015
  this.persistJobs();
1709
2016
  return {
1710
2017
  success: true,
1711
- message: `Job "${name}" updated.`
2018
+ message: `Job "${job.config.name}" updated.`
1712
2019
  };
1713
2020
  }
1714
2021
  /**
1715
2022
  * Enable or disable a job and persist.
1716
2023
  */
1717
- setEnabled(name, enabled) {
1718
- const job = this.jobs.get(name);
2024
+ setEnabled(id, enabled) {
2025
+ const job = this.jobs.get(id);
1719
2026
  if (!job) {
1720
- return { success: false, message: `Job "${name}" not found.` };
2027
+ return { success: false, message: `Job "${id}" not found.` };
1721
2028
  }
1722
2029
  if (!isRecurringJob(job.config)) {
1723
2030
  return {
1724
2031
  success: false,
1725
- message: `Job "${name}" does not have a schedule and cannot be enabled or disabled.`
2032
+ message: `Job "${job.config.name}" does not have a schedule and cannot be enabled or disabled.`
1726
2033
  };
1727
2034
  }
1728
2035
  job.config.enabled = enabled;
@@ -1736,33 +2043,33 @@ var init_scheduler = __esm({
1736
2043
  this.persistJobs();
1737
2044
  return {
1738
2045
  success: true,
1739
- message: `Job "${name}" ${enabled ? "enabled" : "disabled"}.`
2046
+ message: `Job "${job.config.name}" ${enabled ? "enabled" : "disabled"}.`
1740
2047
  };
1741
2048
  }
1742
2049
  /**
1743
2050
  * Manually trigger a job (runs immediately, ignoring cron schedule).
1744
2051
  */
1745
- async triggerJob(name) {
1746
- const job = this.jobs.get(name);
2052
+ async triggerJob(id) {
2053
+ const job = this.jobs.get(id);
1747
2054
  if (!job) {
1748
- return { success: false, message: `Job "${name}" not found.` };
2055
+ return { success: false, message: `Job "${id}" not found.` };
1749
2056
  }
1750
2057
  const { text, notifyFailed } = await this.runJob(job.config);
1751
2058
  if (!text) {
1752
2059
  return {
1753
2060
  success: true,
1754
- message: `Job "${name}" triggered but produced no output.`
2061
+ message: `Job "${job.config.name}" triggered but produced no output.`
1755
2062
  };
1756
2063
  }
1757
2064
  if (notifyFailed) {
1758
2065
  return {
1759
2066
  success: true,
1760
- message: `Job "${name}" executed, but notification to ${job.config.notify.adapter} failed. Check logs.`
2067
+ message: `Job "${job.config.name}" executed, but notification to ${job.config.notify.adapter} failed. Check logs.`
1761
2068
  };
1762
2069
  }
1763
2070
  return {
1764
2071
  success: true,
1765
- message: `Job "${name}" triggered. Result sent to ${job.config.notify.adapter}.`
2072
+ message: `Job "${job.config.name}" triggered. Result sent to ${job.config.notify.adapter}.`
1766
2073
  };
1767
2074
  }
1768
2075
  /**
@@ -1772,6 +2079,7 @@ var init_scheduler = __esm({
1772
2079
  const result = [];
1773
2080
  for (const [, job] of this.jobs) {
1774
2081
  result.push({
2082
+ id: job.config.id,
1775
2083
  name: job.config.name,
1776
2084
  ...job.config.cron ? { cron: job.config.cron } : {},
1777
2085
  prompt: job.config.prompt,
@@ -1792,11 +2100,11 @@ var init_scheduler = __esm({
1792
2100
  /**
1793
2101
  * Stop the cron task and remove a job from the map (does NOT persist).
1794
2102
  */
1795
- removeFromMap(name) {
1796
- const existing = this.jobs.get(name);
2103
+ removeFromMap(id) {
2104
+ const existing = this.jobs.get(id);
1797
2105
  if (existing) {
1798
2106
  existing.task?.stop();
1799
- this.jobs.delete(name);
2107
+ this.jobs.delete(id);
1800
2108
  }
1801
2109
  }
1802
2110
  /**
@@ -2515,15 +2823,15 @@ async function interactiveCreate(workDir) {
2515
2823
  }
2516
2824
 
2517
2825
  // src/commands/run.ts
2518
- import path17 from "path";
2519
- import fs17 from "fs";
2826
+ import path18 from "path";
2827
+ import fs18 from "fs";
2520
2828
  import inquirer2 from "inquirer";
2521
2829
  import chalk4 from "chalk";
2522
2830
 
2523
2831
  // src/runtime/server.ts
2524
2832
  import express from "express";
2525
- import path16 from "path";
2526
- import fs16 from "fs";
2833
+ import path17 from "path";
2834
+ import fs17 from "fs";
2527
2835
  import { fileURLToPath as fileURLToPath2 } from "url";
2528
2836
  import { createServer } from "http";
2529
2837
  import { exec } from "child_process";
@@ -2531,7 +2839,7 @@ import { exec } from "child_process";
2531
2839
  // src/runtime/agent.ts
2532
2840
  import path11 from "path";
2533
2841
  import fs10 from "fs";
2534
- import { randomUUID as randomUUID3 } from "crypto";
2842
+ import { randomUUID as randomUUID4 } from "crypto";
2535
2843
  import { fileURLToPath } from "url";
2536
2844
  import {
2537
2845
  AuthStorage,
@@ -3085,6 +3393,7 @@ function createSendFileTool(fileOutputCallbackRef) {
3085
3393
  }
3086
3394
 
3087
3395
  // src/runtime/tools/manage-schedule-tool.ts
3396
+ import { randomUUID as randomUUID3 } from "crypto";
3088
3397
  import { Type as Type2 } from "@sinclair/typebox";
3089
3398
  var ManageScheduleParams = Type2.Object({
3090
3399
  action: Type2.Union(
@@ -3100,7 +3409,7 @@ var ManageScheduleParams = Type2.Object({
3100
3409
  ),
3101
3410
  name: Type2.Optional(
3102
3411
  Type2.String({
3103
- description: "Unique name for the scheduled task. Required for add/remove/trigger/enable/disable."
3412
+ description: "Display name for the scheduled task. Required for add/remove/trigger/enable/disable."
3104
3413
  })
3105
3414
  ),
3106
3415
  cron: Type2.Optional(
@@ -3120,7 +3429,7 @@ var ManageScheduleParams = Type2.Object({
3120
3429
  ),
3121
3430
  notifyAdapter: Type2.Optional(
3122
3431
  Type2.String({
3123
- description: "Optional target adapter for notifications. If omitted, the current chat is used when supported (Telegram, Slack, or Web)."
3432
+ description: "Optional target adapter for notifications. If omitted, the current chat is used when supported (Telegram, Slack, Feishu, or Web)."
3124
3433
  })
3125
3434
  ),
3126
3435
  notifyChannelId: Type2.Optional(
@@ -3132,6 +3441,27 @@ var ManageScheduleParams = Type2.Object({
3132
3441
  function textResult(text) {
3133
3442
  return { content: [{ type: "text", text }], details: void 0 };
3134
3443
  }
3444
+ function resolveJobId(scheduler, nameOrId) {
3445
+ const jobs = scheduler.listJobs();
3446
+ const byId = jobs.find((job) => job.id === nameOrId);
3447
+ if (byId) {
3448
+ return { ok: true, jobId: byId.id };
3449
+ }
3450
+ const byName = jobs.filter((job) => job.name === nameOrId);
3451
+ if (byName.length === 1) {
3452
+ return { ok: true, jobId: byName[0].id };
3453
+ }
3454
+ if (byName.length > 1) {
3455
+ return {
3456
+ ok: false,
3457
+ message: `Error: Multiple scheduled tasks share the name '${nameOrId}'. Rename one of them before using this action.`
3458
+ };
3459
+ }
3460
+ return {
3461
+ ok: false,
3462
+ message: `Error: Scheduled task '${nameOrId}' was not found.`
3463
+ };
3464
+ }
3135
3465
  function getDefaultNotifyTarget(adapter, channelId) {
3136
3466
  if (adapter === "telegram" && channelId.startsWith("telegram-")) {
3137
3467
  return { adapter: "telegram", channelId };
@@ -3139,12 +3469,15 @@ function getDefaultNotifyTarget(adapter, channelId) {
3139
3469
  if (adapter === "slack" && channelId.startsWith("slack-")) {
3140
3470
  return { adapter: "slack", channelId };
3141
3471
  }
3472
+ if (adapter === "feishu" && channelId.startsWith("feishu-")) {
3473
+ return { adapter: "feishu", channelId };
3474
+ }
3142
3475
  if (adapter === "web") {
3143
3476
  return { adapter: "web", channelId };
3144
3477
  }
3145
3478
  return null;
3146
3479
  }
3147
- function createManageScheduleTool(schedulerRef, adapter, channelId) {
3480
+ function createManageScheduleTool(schedulerRef, adapter, channelId, generateJobId = randomUUID3) {
3148
3481
  return {
3149
3482
  name: "manage_scheduled_task",
3150
3483
  label: "Manage Scheduled Task",
@@ -3152,11 +3485,11 @@ function createManageScheduleTool(schedulerRef, adapter, channelId) {
3152
3485
  "Manage scheduled tasks (cron jobs) that automatically execute prompts and push results to IM channels.",
3153
3486
  "",
3154
3487
  "Actions:",
3155
- "- add: Create a new scheduled task. Requires: name, cron, prompt. Notifications default to the current Telegram, Slack, or Web chat. You can override the destination with notifyAdapter + notifyChannelId. The prompt must describe only the work for each run, not the schedule itself.",
3488
+ "- add: Create a new scheduled task. Requires: name, cron, prompt. Notifications default to the current Telegram, Slack, Feishu, or Web chat. You can override the destination with notifyAdapter + notifyChannelId. The prompt must describe only the work for each run, not the schedule itself.",
3156
3489
  "- list: List all scheduled tasks with their status.",
3157
- "- remove: Remove a scheduled task by name.",
3158
- "- trigger: Manually trigger a scheduled task by name (runs immediately).",
3159
- "- enable: Enable a disabled scheduled task.",
3490
+ "- remove: Remove a scheduled task by display name.",
3491
+ "- trigger: Manually trigger a scheduled task by display name (runs immediately).",
3492
+ "- enable: Enable a disabled scheduled task by display name.",
3160
3493
  "- disable: Disable a scheduled task without removing it.",
3161
3494
  "",
3162
3495
  "Cron expression format: '* * * * *' (minute hour day month weekday)",
@@ -3208,6 +3541,7 @@ ${lines.join("\n")}`
3208
3541
  );
3209
3542
  }
3210
3543
  const jobConfig = {
3544
+ id: generateJobId(),
3211
3545
  name: params.name,
3212
3546
  cron: params.cron,
3213
3547
  prompt: params.prompt,
@@ -3224,7 +3558,11 @@ ${lines.join("\n")}`
3224
3558
  "Error: 'name' is required for removing a task."
3225
3559
  );
3226
3560
  }
3227
- const result = scheduler.removeJob(params.name);
3561
+ const resolved = resolveJobId(scheduler, params.name);
3562
+ if (!resolved.ok) {
3563
+ return textResult(resolved.message);
3564
+ }
3565
+ const result = scheduler.removeJob(resolved.jobId);
3228
3566
  return textResult(result.message);
3229
3567
  }
3230
3568
  case "trigger": {
@@ -3233,7 +3571,11 @@ ${lines.join("\n")}`
3233
3571
  "Error: 'name' is required for triggering a task."
3234
3572
  );
3235
3573
  }
3236
- const result = await scheduler.triggerJob(params.name);
3574
+ const resolved = resolveJobId(scheduler, params.name);
3575
+ if (!resolved.ok) {
3576
+ return textResult(resolved.message);
3577
+ }
3578
+ const result = await scheduler.triggerJob(resolved.jobId);
3237
3579
  return textResult(result.message);
3238
3580
  }
3239
3581
  case "enable": {
@@ -3242,7 +3584,11 @@ ${lines.join("\n")}`
3242
3584
  "Error: 'name' is required for enabling a task."
3243
3585
  );
3244
3586
  }
3245
- const result = scheduler.setEnabled(params.name, true);
3587
+ const resolved = resolveJobId(scheduler, params.name);
3588
+ if (!resolved.ok) {
3589
+ return textResult(resolved.message);
3590
+ }
3591
+ const result = scheduler.setEnabled(resolved.jobId, true);
3246
3592
  return textResult(result.message);
3247
3593
  }
3248
3594
  case "disable": {
@@ -3251,7 +3597,11 @@ ${lines.join("\n")}`
3251
3597
  "Error: 'name' is required for disabling a task."
3252
3598
  );
3253
3599
  }
3254
- const result = scheduler.setEnabled(params.name, false);
3600
+ const resolved = resolveJobId(scheduler, params.name);
3601
+ if (!resolved.ok) {
3602
+ return textResult(resolved.message);
3603
+ }
3604
+ const result = scheduler.setEnabled(resolved.jobId, false);
3255
3605
  return textResult(result.message);
3256
3606
  }
3257
3607
  default:
@@ -3325,6 +3675,7 @@ function readInstalledSkills(configPath) {
3325
3675
  }
3326
3676
 
3327
3677
  // src/runtime/agent.ts
3678
+ init_types();
3328
3679
  var DEBUG = true;
3329
3680
  var log = (...args) => DEBUG && console.log(...args);
3330
3681
  var write = (data) => DEBUG && process.stdout.write(data);
@@ -3464,9 +3815,8 @@ function getAssistantDiagnostics(message) {
3464
3815
  return { stopReason, errorMessage, hasText: text.length > 0, toolCalls };
3465
3816
  }
3466
3817
  function getLifecycleTrigger(channelId) {
3467
- if (channelId.startsWith("telegram-")) return "telegram";
3468
- if (channelId.startsWith("slack-")) return "slack";
3469
- return "web";
3818
+ const platform = detectPlatformFromChannelId(channelId);
3819
+ return platform === "scheduler" ? "web" : platform;
3470
3820
  }
3471
3821
  var PackAgent = class {
3472
3822
  options;
@@ -3664,12 +4014,36 @@ var PackAgent = class {
3664
4014
  const run = async () => {
3665
4015
  cs.running = true;
3666
4016
  let turnHadVisibleOutput = false;
3667
- const runId = randomUUID3();
4017
+ let sawAgentStart = false;
4018
+ let sawAgentEnd = false;
4019
+ const runId = randomUUID4();
3668
4020
  let unsubscribe = () => void 0;
4021
+ const waitForQueuedAgentEvents = async () => {
4022
+ const maybeQueue = cs.session._agentEventQueue;
4023
+ if (!maybeQueue || typeof maybeQueue.then !== "function") {
4024
+ return;
4025
+ }
4026
+ try {
4027
+ await maybeQueue;
4028
+ } catch (error) {
4029
+ log("[PackAgent] Waiting for queued agent events failed:", error);
4030
+ }
4031
+ };
3669
4032
  try {
3670
- cs.fileOutputCallbackRef.current = (event) => {
4033
+ const forwardAgentEvent = (event) => {
4034
+ if (event.type === "agent_start") {
4035
+ sawAgentStart = true;
4036
+ } else if (event.type === "agent_end") {
4037
+ if (sawAgentEnd) {
4038
+ return;
4039
+ }
4040
+ sawAgentEnd = true;
4041
+ }
3671
4042
  onEvent(event);
3672
4043
  };
4044
+ cs.fileOutputCallbackRef.current = (event) => {
4045
+ forwardAgentEvent(event);
4046
+ };
3673
4047
  cs.delegatedToolRunContextRef.current = {
3674
4048
  runId,
3675
4049
  channelId,
@@ -3681,7 +4055,7 @@ var PackAgent = class {
3681
4055
  log("\n=== [AGENT SESSION START] ===");
3682
4056
  log("System Prompt:\n", cs.session.systemPrompt);
3683
4057
  log("============================\n");
3684
- onEvent({ type: "agent_start" });
4058
+ forwardAgentEvent({ type: "agent_start" });
3685
4059
  break;
3686
4060
  case "message_start":
3687
4061
  log(`
@@ -3689,7 +4063,7 @@ var PackAgent = class {
3689
4063
  if (event.message?.role === "user") {
3690
4064
  log(JSON.stringify(event.message.content, null, 2));
3691
4065
  }
3692
- onEvent({
4066
+ forwardAgentEvent({
3693
4067
  type: "message_start",
3694
4068
  role: event.message?.role ?? ""
3695
4069
  });
@@ -3698,13 +4072,13 @@ var PackAgent = class {
3698
4072
  if (event.assistantMessageEvent?.type === "text_delta") {
3699
4073
  turnHadVisibleOutput = true;
3700
4074
  write(event.assistantMessageEvent.delta);
3701
- onEvent({
4075
+ forwardAgentEvent({
3702
4076
  type: "text_delta",
3703
4077
  delta: event.assistantMessageEvent.delta
3704
4078
  });
3705
4079
  } else if (event.assistantMessageEvent?.type === "thinking_delta") {
3706
4080
  turnHadVisibleOutput = true;
3707
- onEvent({
4081
+ forwardAgentEvent({
3708
4082
  type: "thinking_delta",
3709
4083
  delta: event.assistantMessageEvent.delta
3710
4084
  });
@@ -3724,14 +4098,17 @@ var PackAgent = class {
3724
4098
  }
3725
4099
  }
3726
4100
  }
3727
- onEvent({ type: "message_end", role: event.message?.role ?? "" });
4101
+ forwardAgentEvent({
4102
+ type: "message_end",
4103
+ role: event.message?.role ?? ""
4104
+ });
3728
4105
  break;
3729
4106
  case "tool_execution_start":
3730
4107
  turnHadVisibleOutput = true;
3731
4108
  log(`
3732
4109
  >>> [Tool Start: ${event.toolName}] >>>`);
3733
4110
  log("Args:", JSON.stringify(event.args, null, 2));
3734
- onEvent({
4111
+ forwardAgentEvent({
3735
4112
  type: "tool_start",
3736
4113
  toolCallId: event.toolCallId ?? "",
3737
4114
  toolName: event.toolName,
@@ -3742,7 +4119,7 @@ var PackAgent = class {
3742
4119
  turnHadVisibleOutput = true;
3743
4120
  log(`<<< [Tool End: ${event.toolName}] <<<`);
3744
4121
  log(`Error: ${event.isError ? "Yes" : "No"}`);
3745
- onEvent({
4122
+ forwardAgentEvent({
3746
4123
  type: "tool_end",
3747
4124
  toolCallId: event.toolCallId ?? "",
3748
4125
  toolName: event.toolName,
@@ -3752,7 +4129,7 @@ var PackAgent = class {
3752
4129
  break;
3753
4130
  case "agent_end":
3754
4131
  log("\n=== [AGENT SESSION END] ===\n");
3755
- onEvent({ type: "agent_end" });
4132
+ forwardAgentEvent({ type: "agent_end" });
3756
4133
  break;
3757
4134
  }
3758
4135
  });
@@ -3799,6 +4176,12 @@ ${text}`;
3799
4176
  }
3800
4177
  return { stopReason: diagnostics?.stopReason ?? "unknown" };
3801
4178
  } finally {
4179
+ await waitForQueuedAgentEvents();
4180
+ if (sawAgentStart && !sawAgentEnd) {
4181
+ sawAgentEnd = true;
4182
+ log(`[PackAgent] Synthesizing terminal agent_end for ${channelId}`);
4183
+ onEvent({ type: "agent_end" });
4184
+ }
3802
4185
  cs.running = false;
3803
4186
  cs.fileOutputCallbackRef.current = null;
3804
4187
  cs.delegatedToolRunContextRef.current = null;
@@ -3878,12 +4261,14 @@ ${text}`;
3878
4261
  };
3879
4262
 
3880
4263
  // src/runtime/adapters/web.ts
4264
+ import { randomUUID as randomUUID5 } from "crypto";
3881
4265
  import fs12 from "fs";
3882
4266
  import path13 from "path";
3883
4267
  import { WebSocketServer } from "ws";
3884
4268
  init_commands();
3885
4269
 
3886
4270
  // src/runtime/services/conversation.ts
4271
+ init_types();
3887
4272
  import fs11 from "fs";
3888
4273
  import path12 from "path";
3889
4274
  import {
@@ -4267,10 +4652,7 @@ var ConversationService = class {
4267
4652
  return parts[parts.length - 1] || filePath;
4268
4653
  }
4269
4654
  detectPlatform(channelId) {
4270
- if (channelId.startsWith("telegram-")) return "telegram";
4271
- if (channelId.startsWith("slack-")) return "slack";
4272
- if (channelId.startsWith("scheduler-")) return "scheduler";
4273
- return "web";
4655
+ return detectPlatformFromChannelId(channelId);
4274
4656
  }
4275
4657
  isLegacyWebConversation(channelId) {
4276
4658
  return channelId.startsWith("web-");
@@ -4298,7 +4680,9 @@ function getRuntimeConfigSignature(config) {
4298
4680
  apiProtocol: config.apiProtocol || "",
4299
4681
  telegramToken: config.adapters?.telegram?.token || "",
4300
4682
  slackBotToken: config.adapters?.slack?.botToken || "",
4301
- slackAppToken: config.adapters?.slack?.appToken || ""
4683
+ slackAppToken: config.adapters?.slack?.appToken || "",
4684
+ feishuAppId: config.adapters?.feishu?.appId || "",
4685
+ feishuAppSecret: config.adapters?.feishu?.appSecret || ""
4302
4686
  });
4303
4687
  }
4304
4688
  function parsePositiveInt(value, fallback) {
@@ -4509,7 +4893,15 @@ var WebAdapter = class {
4509
4893
  res.status(503).json({ success: false, message: "Scheduler not available" });
4510
4894
  return;
4511
4895
  }
4512
- const { name, cron: cronExpr, prompt, notify, enabled, timezone } = req.body;
4896
+ const {
4897
+ id,
4898
+ name,
4899
+ cron: cronExpr,
4900
+ prompt,
4901
+ notify,
4902
+ enabled,
4903
+ timezone
4904
+ } = req.body;
4513
4905
  if (!name || !prompt || !notify?.adapter || !notify?.channelId) {
4514
4906
  res.status(400).json({
4515
4907
  success: false,
@@ -4518,6 +4910,7 @@ var WebAdapter = class {
4518
4910
  return;
4519
4911
  }
4520
4912
  const result = scheduler.addJob({
4913
+ id: typeof id === "string" && id.trim() ? id.trim() : randomUUID5(),
4521
4914
  name,
4522
4915
  ...typeof cronExpr === "string" ? { cron: cronExpr } : {},
4523
4916
  prompt,
@@ -4527,25 +4920,25 @@ var WebAdapter = class {
4527
4920
  });
4528
4921
  res.json(result);
4529
4922
  });
4530
- app.delete("/api/scheduler/jobs/:name", (req, res) => {
4923
+ app.delete("/api/scheduler/jobs/:jobId", (req, res) => {
4531
4924
  const scheduler = getScheduler();
4532
4925
  if (!scheduler) {
4533
4926
  res.status(503).json({ success: false, message: "Scheduler not available" });
4534
4927
  return;
4535
4928
  }
4536
- const result = scheduler.removeJob(req.params.name);
4929
+ const result = scheduler.removeJob(req.params.jobId);
4537
4930
  res.json(result);
4538
4931
  });
4539
- app.post("/api/scheduler/jobs/:name/trigger", async (req, res) => {
4932
+ app.post("/api/scheduler/jobs/:jobId/trigger", async (req, res) => {
4540
4933
  const scheduler = getScheduler();
4541
4934
  if (!scheduler) {
4542
4935
  res.status(503).json({ success: false, message: "Scheduler not available" });
4543
4936
  return;
4544
4937
  }
4545
- const result = await scheduler.triggerJob(req.params.name);
4938
+ const result = await scheduler.triggerJob(req.params.jobId);
4546
4939
  res.json(result);
4547
4940
  });
4548
- app.patch("/api/scheduler/jobs/:name", (req, res) => {
4941
+ app.patch("/api/scheduler/jobs/:jobId", (req, res) => {
4549
4942
  const scheduler = getScheduler();
4550
4943
  if (!scheduler) {
4551
4944
  res.status(503).json({ success: false, message: "Scheduler not available" });
@@ -4556,7 +4949,7 @@ var WebAdapter = class {
4556
4949
  res.status(400).json({ success: false, message: "Field 'enabled' (boolean) is required" });
4557
4950
  return;
4558
4951
  }
4559
- const result = scheduler.setEnabled(req.params.name, enabled);
4952
+ const result = scheduler.setEnabled(req.params.jobId, enabled);
4560
4953
  res.json(result);
4561
4954
  });
4562
4955
  this.wss = new WebSocketServer({ noServer: true });
@@ -4877,7 +5270,7 @@ var IpcAdapter = class {
4877
5270
  this.replyError(request.id, "Scheduler adapter is not available");
4878
5271
  return;
4879
5272
  }
4880
- const result = scheduler.updateJob(request.name, request.updates);
5273
+ const result = scheduler.updateJob(request.jobId, request.updates);
4881
5274
  if (!result.success) {
4882
5275
  this.replyError(request.id, result.message);
4883
5276
  return;
@@ -4891,7 +5284,7 @@ var IpcAdapter = class {
4891
5284
  this.replyError(request.id, "Scheduler adapter is not available");
4892
5285
  return;
4893
5286
  }
4894
- const result = scheduler.setEnabled(request.name, request.enabled);
5287
+ const result = scheduler.setEnabled(request.jobId, request.enabled);
4895
5288
  if (!result.success) {
4896
5289
  this.replyError(request.id, result.message);
4897
5290
  return;
@@ -4905,7 +5298,7 @@ var IpcAdapter = class {
4905
5298
  this.replyError(request.id, "Scheduler adapter is not available");
4906
5299
  return;
4907
5300
  }
4908
- const result = await scheduler.triggerJob(request.name);
5301
+ const result = await scheduler.triggerJob(request.jobId);
4909
5302
  if (!result.success) {
4910
5303
  this.replyError(request.id, result.message);
4911
5304
  return;
@@ -4919,7 +5312,7 @@ var IpcAdapter = class {
4919
5312
  this.replyError(request.id, "Scheduler adapter is not available");
4920
5313
  return;
4921
5314
  }
4922
- const result = scheduler.removeJob(request.name);
5315
+ const result = scheduler.removeJob(request.jobId);
4923
5316
  if (!result.success) {
4924
5317
  this.replyError(request.id, result.message);
4925
5318
  return;
@@ -4941,10 +5334,7 @@ var IpcAdapter = class {
4941
5334
  return adapter;
4942
5335
  }
4943
5336
  detectPlatform(channelId) {
4944
- if (channelId.startsWith("telegram-")) return "telegram";
4945
- if (channelId.startsWith("slack-")) return "slack";
4946
- if (channelId.startsWith("scheduler-")) return "scheduler";
4947
- return "web";
5337
+ return detectPlatformFromChannelId(channelId);
4948
5338
  }
4949
5339
  sendIpc(payload) {
4950
5340
  if (typeof process.send === "function") {
@@ -5189,7 +5579,7 @@ function deregister(dir, pid) {
5189
5579
  }
5190
5580
 
5191
5581
  // src/runtime/server.ts
5192
- var __dirname = path16.dirname(fileURLToPath2(import.meta.url));
5582
+ var __dirname = path17.dirname(fileURLToPath2(import.meta.url));
5193
5583
  async function startServer(options) {
5194
5584
  const {
5195
5585
  rootDir,
@@ -5205,8 +5595,8 @@ async function startServer(options) {
5205
5595
  const baseUrl = dataConfig.baseUrl?.trim() || void 0;
5206
5596
  const modelId = dataConfig.modelId?.trim() || (SUPPORTED_PROVIDERS[provider]?.defaultModelId ?? SUPPORTED_PROVIDERS.openai.defaultModelId);
5207
5597
  const apiProtocol = dataConfig.apiProtocol;
5208
- const packageRoot = path16.resolve(__dirname, "..");
5209
- const webDir = fs16.existsSync(path16.join(rootDir, "web")) ? path16.join(rootDir, "web") : path16.join(packageRoot, "web");
5598
+ const packageRoot = path17.resolve(__dirname, "..");
5599
+ const webDir = fs17.existsSync(path17.join(rootDir, "web")) ? path17.join(rootDir, "web") : path17.join(packageRoot, "web");
5210
5600
  const app = express();
5211
5601
  app.use(express.json());
5212
5602
  app.use(express.static(webDir));
@@ -5318,6 +5708,35 @@ async function startServer(options) {
5318
5708
  }
5319
5709
  }
5320
5710
  }
5711
+ const feishuConfig = dataConfig.adapters?.feishu;
5712
+ if (feishuConfig?.appId || feishuConfig?.appSecret) {
5713
+ if (!feishuConfig.appId || !feishuConfig.appSecret) {
5714
+ console.warn(
5715
+ "[Feishu] Skipped: both adapters.feishu.appId and adapters.feishu.appSecret are required."
5716
+ );
5717
+ } else {
5718
+ try {
5719
+ const { FeishuAdapter: FeishuAdapter2 } = await Promise.resolve().then(() => (init_feishu(), feishu_exports));
5720
+ const feishuAdapter = new FeishuAdapter2({
5721
+ appId: feishuConfig.appId,
5722
+ appSecret: feishuConfig.appSecret
5723
+ });
5724
+ await feishuAdapter.start({
5725
+ agent,
5726
+ server,
5727
+ app,
5728
+ rootDir,
5729
+ lifecycle,
5730
+ adapterMap,
5731
+ ipcBroadcaster
5732
+ });
5733
+ adapters.push(feishuAdapter);
5734
+ adapterMap.set(feishuAdapter.name, feishuAdapter);
5735
+ } catch (err) {
5736
+ console.error("[Feishu] Failed to start:", err);
5737
+ }
5738
+ }
5739
+ }
5321
5740
  const { isMessageSender: isMessageSender2 } = await Promise.resolve().then(() => (init_types(), types_exports));
5322
5741
  const notifyFn = async (adapterName, channelId, text) => {
5323
5742
  const adapter = adapterMap.get(adapterName);
@@ -5429,23 +5848,23 @@ function findMissingSkills(workDir, config) {
5429
5848
  });
5430
5849
  }
5431
5850
  function copyStartTemplates2(workDir) {
5432
- const templateDir = path17.resolve(
5851
+ const templateDir = path18.resolve(
5433
5852
  new URL("../templates", import.meta.url).pathname
5434
5853
  );
5435
5854
  for (const file of ["start.sh", "start.bat"]) {
5436
- const src = path17.join(templateDir, file);
5437
- const dest = path17.join(workDir, file);
5438
- if (fs17.existsSync(src)) {
5439
- fs17.copyFileSync(src, dest);
5855
+ const src = path18.join(templateDir, file);
5856
+ const dest = path18.join(workDir, file);
5857
+ if (fs18.existsSync(src)) {
5858
+ fs18.copyFileSync(src, dest);
5440
5859
  if (file === "start.sh") {
5441
- fs17.chmodSync(dest, 493);
5860
+ fs18.chmodSync(dest, 493);
5442
5861
  }
5443
5862
  }
5444
5863
  }
5445
5864
  }
5446
5865
  async function runCommand(directory) {
5447
- const workDir = directory ? path17.resolve(directory) : process.cwd();
5448
- fs17.mkdirSync(workDir, { recursive: true });
5866
+ const workDir = directory ? path18.resolve(directory) : process.cwd();
5867
+ fs18.mkdirSync(workDir, { recursive: true });
5449
5868
  if (!configExists(workDir)) {
5450
5869
  console.log(chalk4.blue("\n No skillpack.json found. Let's set one up.\n"));
5451
5870
  const { name, description } = await inquirer2.prompt([
@@ -5489,14 +5908,14 @@ async function runCommand(directory) {
5489
5908
  }
5490
5909
 
5491
5910
  // src/cli.ts
5492
- import fs18 from "fs";
5493
- import path18 from "path";
5911
+ import fs19 from "fs";
5912
+ import path19 from "path";
5494
5913
  import { fileURLToPath as fileURLToPath3 } from "url";
5495
5914
  var packageJson = JSON.parse(
5496
- fs18.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
5915
+ fs19.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
5497
5916
  );
5498
5917
  var program = new Command();
5499
- var cliFilePath = path18.resolve(fileURLToPath3(import.meta.url));
5918
+ var cliFilePath = path19.resolve(fileURLToPath3(import.meta.url));
5500
5919
  program.name("skillpack").description("Assemble, package, and run Agent Skills packs").version(packageJson.version);
5501
5920
  program.command("create [directory]").description("Create a skills pack interactively").option("--config <path-or-url>", "Initialize from a local or remote skillpack.json").action(async (directory, options) => {
5502
5921
  await createCommand(directory, options);
@@ -5517,7 +5936,7 @@ program.command("zip").description("Package the current pack as a zip file (skil
5517
5936
  function normalizeUserArgs(argv) {
5518
5937
  if (argv.length === 0) return argv;
5519
5938
  const firstArg = argv[0];
5520
- if (firstArg && path18.resolve(firstArg) === cliFilePath) {
5939
+ if (firstArg && path19.resolve(firstArg) === cliFilePath) {
5521
5940
  return argv.slice(1);
5522
5941
  }
5523
5942
  return argv;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cremini/skillpack",
3
- "version": "1.2.11",
3
+ "version": "1.2.13",
4
4
  "description": "Pack AI Skills into Local Agents",
5
5
  "type": "module",
6
6
  "repository": {
@@ -44,6 +44,7 @@
44
44
  "author": "CreminiAI",
45
45
  "license": "MIT",
46
46
  "dependencies": {
47
+ "@larksuiteoapi/node-sdk": "^1.63.1",
47
48
  "@mariozechner/pi-coding-agent": "^0.57.1",
48
49
  "@sinclair/typebox": "^0.34.41",
49
50
  "@slack/bolt": "^4.6.0",
@@ -70,4 +71,4 @@
70
71
  "tsx": "^4.21.0",
71
72
  "typescript": "^5.9.3"
72
73
  }
73
- }
74
+ }
package/web/index.html CHANGED
@@ -164,6 +164,17 @@
164
164
  <input type="password" id="chatapps-slack-app-token" placeholder="xapp-..." class="form-input" />
165
165
  </div>
166
166
  </div>
167
+ <div class="settings-section">
168
+ <h3 class="section-title">Feishu</h3>
169
+ <div class="form-group">
170
+ <label>App ID</label>
171
+ <input type="password" id="chatapps-feishu-app-id" placeholder="cli_a1b2c3d4..." class="form-input" />
172
+ </div>
173
+ <div class="form-group">
174
+ <label>App Secret</label>
175
+ <input type="password" id="chatapps-feishu-app-secret" placeholder="your-feishu-app-secret" class="form-input" />
176
+ </div>
177
+ </div>
167
178
  </div>
168
179
  </div>
169
180
  <div class="settings-modal-footer">
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Chat Apps (IM Bots) Dialog Module
3
3
  *
4
- * 负责 IM Bots(Telegram / Slack)Token 的配置管理。
4
+ * 负责 IM Bots(Telegram / Slack / Feishu)配置管理。
5
5
  * 独立的 Dialog,从原 SettingDialog 的 IM Bots 部分拆分出来。
6
6
  */
7
7
  import { state } from "./config.js";
@@ -16,8 +16,22 @@ let restartBtn;
16
16
  let telegramTokenInput;
17
17
  let slackBotTokenInput;
18
18
  let slackAppTokenInput;
19
+ let feishuAppIdInput;
20
+ let feishuAppSecretInput;
19
21
  let statusEl;
20
22
 
23
+ function hasConfiguredChatApp(adapters = {}) {
24
+ const telegramConfigured = Boolean(adapters.telegram?.token);
25
+ const slackConfigured = Boolean(
26
+ adapters.slack?.botToken && adapters.slack?.appToken,
27
+ );
28
+ const feishuConfigured = Boolean(
29
+ adapters.feishu?.appId && adapters.feishu?.appSecret,
30
+ );
31
+
32
+ return telegramConfigured || slackConfigured || feishuConfigured;
33
+ }
34
+
21
35
  // --- Public API ---
22
36
 
23
37
  export function initChatAppsDialog() {
@@ -29,6 +43,8 @@ export function initChatAppsDialog() {
29
43
  telegramTokenInput = document.getElementById("chatapps-telegram-token");
30
44
  slackBotTokenInput = document.getElementById("chatapps-slack-bot-token");
31
45
  slackAppTokenInput = document.getElementById("chatapps-slack-app-token");
46
+ feishuAppIdInput = document.getElementById("chatapps-feishu-app-id");
47
+ feishuAppSecretInput = document.getElementById("chatapps-feishu-app-secret");
32
48
  statusEl = document.getElementById("chatapps-status");
33
49
 
34
50
  if (!dialog) return;
@@ -54,9 +70,7 @@ export function updateChatAppsButton() {
54
70
  if (!openBtn) return;
55
71
  const config = state.config;
56
72
  const adapters = config?.adapters || {};
57
- const hasAnyToken =
58
- (adapters.telegram && adapters.telegram.token) ||
59
- (adapters.slack && (adapters.slack.botToken || adapters.slack.appToken));
73
+ const hasAnyToken = hasConfiguredChatApp(adapters);
60
74
 
61
75
  if (hasAnyToken) {
62
76
  openBtn.classList.add("connected");
@@ -99,6 +113,14 @@ function populateForm() {
99
113
  slackAppTokenInput.value = "";
100
114
  }
101
115
 
116
+ if (adapters.feishu) {
117
+ feishuAppIdInput.value = adapters.feishu.appId || "";
118
+ feishuAppSecretInput.value = adapters.feishu.appSecret || "";
119
+ } else {
120
+ feishuAppIdInput.value = "";
121
+ feishuAppSecretInput.value = "";
122
+ }
123
+
102
124
  // Restart required status
103
125
  if (state.restartRequired) {
104
126
  setStatus(
@@ -116,6 +138,8 @@ async function handleSave() {
116
138
  const telegramToken = telegramTokenInput.value.trim();
117
139
  const slackBotToken = slackBotTokenInput.value.trim();
118
140
  const slackAppToken = slackAppTokenInput.value.trim();
141
+ const feishuAppId = feishuAppIdInput.value.trim();
142
+ const feishuAppSecret = feishuAppSecretInput.value.trim();
119
143
 
120
144
  // 始终写入所有 adapter 键,空值也要显式传递,让后端能感知「清空」操作
121
145
  const adapters = {
@@ -127,6 +151,13 @@ async function handleSave() {
127
151
  appToken: slackAppToken || undefined,
128
152
  }
129
153
  : null,
154
+ feishu:
155
+ feishuAppId || feishuAppSecret
156
+ ? {
157
+ appId: feishuAppId || undefined,
158
+ appSecret: feishuAppSecret || undefined,
159
+ }
160
+ : null,
130
161
  };
131
162
 
132
163
  const updates = { adapters };