loopable-cli 0.1.0 → 0.2.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.
Files changed (101) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/_connectorId-CkygNLny.js +1 -0
  3. package/.output/public/assets/_loopId-Cshlc5Ja.js +1 -0
  4. package/.output/public/assets/_taskId-BKDIJ_Fg.js +1 -0
  5. package/.output/public/assets/alert-BT2EVMJP.js +1 -0
  6. package/.output/public/assets/arrow-left-CWUZt6k2.js +1 -0
  7. package/.output/public/assets/arrow-right-dy_oOg_c.js +1 -0
  8. package/.output/public/assets/connection-status-DCqqmY_6.js +1 -0
  9. package/.output/public/assets/connector-icon-BUoK6Cru.js +1 -0
  10. package/.output/public/assets/connectors-CeLBtEtg.js +1 -0
  11. package/.output/public/assets/{external-link-D46X-bXa.js → external-link-DC11orFA.js} +1 -1
  12. package/.output/public/assets/gaps-BtFoD7mF.js +1 -0
  13. package/.output/public/assets/inbox-BYNjkNDJ.js +1 -0
  14. package/.output/public/assets/index-0yP3Jn_G.js +18 -0
  15. package/.output/public/assets/{loader-CcBl2u8m.js → loader-B4Epchm_.js} +1 -1
  16. package/.output/public/assets/loop-form-DnSJNc1k.js +1 -0
  17. package/.output/public/assets/loops-1CvbqO6m.js +1 -0
  18. package/.output/public/assets/new-m5NlqYQT.js +1 -0
  19. package/.output/public/assets/play-DD4xQgvi.js +1 -0
  20. package/.output/public/assets/preload-helper-CfdAYKD7.js +3 -0
  21. package/.output/public/assets/refresh-cw-C3xBnKfy.js +1 -0
  22. package/.output/public/assets/rolldown-runtime-CbXtAM7H.js +1 -0
  23. package/.output/public/assets/runners-D8QVl-Y6.js +1 -0
  24. package/.output/public/assets/select-Cppu-hti.js +1 -0
  25. package/.output/public/assets/setting-fields-bJ_gTG7Q.js +3 -0
  26. package/.output/public/assets/styles-ClTGaXU0.css +2 -0
  27. package/.output/public/assets/switch-hRIbNGOj.js +1 -0
  28. package/.output/public/assets/task-list-BAxjcVgr.js +1 -0
  29. package/.output/public/assets/textarea-Fyuzi6Vp.js +1 -0
  30. package/.output/public/assets/{trash-2-D6u6NpzO.js → trash-2-BXZzBsoV.js} +1 -1
  31. package/.output/public/assets/useValueChanged-VVyq4hb9.js +1 -0
  32. package/.output/server/{_connectorId-8nb5yOzG.mjs → _connectorId-C9yydJrC.mjs} +92 -11
  33. package/.output/server/_libs/@base-ui/react+[...].mjs +427 -427
  34. package/.output/server/{_loopId-BsUL1SeL.mjs → _loopId-B0VG-eBC.mjs} +44 -15
  35. package/.output/server/_ssr/{callback-CUINvl7M.mjs → access-DTttr563.mjs} +39 -5
  36. package/.output/server/_ssr/{agents-DAOZPgO2.mjs → agents-C7B6RJ6-.mjs} +3 -3
  37. package/.output/server/_ssr/{agents-Btql102J.mjs → agents-DG643yoJ.mjs} +4 -4
  38. package/.output/server/_ssr/{alert-NEiOCTOl.mjs → alert-DQvzE7bu.mjs} +1 -1
  39. package/.output/server/_ssr/{client-CKkOfh-s.mjs → client-BtVRVhQ2.mjs} +4 -2
  40. package/.output/server/_ssr/{connection-status-Bi5peUAk.mjs → connection-status-BZr3Y-IC.mjs} +2 -2
  41. package/.output/server/_ssr/{connections-qnw2GVlP.mjs → connections-Df-aDpfJ.mjs} +816 -17
  42. package/.output/server/_ssr/connector-icon-WvxwZ5TF.mjs +56 -0
  43. package/.output/server/_ssr/{connectors-BHNwuIMj.mjs → connectors-BU0IiRge.mjs} +7 -6
  44. package/.output/server/_ssr/{connectors-C28g0i4c.mjs → connectors-e0mf2pMs.mjs} +14 -3
  45. package/.output/server/_ssr/{gaps-RAFSXLZf.mjs → gaps-Cz76ZicM.mjs} +6 -6
  46. package/.output/server/_ssr/inbox-CAUBa1I3.mjs +114 -0
  47. package/.output/server/_ssr/{loop-form-DlObSewZ.mjs → loop-form-FqSaXiHl.mjs} +8 -7
  48. package/.output/server/_ssr/{loops-APFcP0y6.mjs → loops-D8WuK7Ke.mjs} +200 -7
  49. package/.output/server/_ssr/{loops-xt2Bn7_q.mjs → loops-Dqoebe8q.mjs} +8 -8
  50. package/.output/server/_ssr/{new-DfKyd2Wa.mjs → new-bBsVaWD6.mjs} +5 -5
  51. package/.output/server/_ssr/{router-3WmpE5b_.mjs → router-54IDWTVz.mjs} +68 -62
  52. package/.output/server/_ssr/{runners-By-mwc3F.mjs → runners-Cj6z9Mqh.mjs} +26 -3
  53. package/.output/server/_ssr/{runners-DtXs4Q8a.mjs → runners-D2UAdLyj.mjs} +3 -3
  54. package/.output/server/_ssr/{runners-DI_NxpHC.mjs → runners-DGWaVvWi.mjs} +4 -4
  55. package/.output/server/_ssr/{secrets-Hqti-niS.mjs → secrets-CRnpwGBh.mjs} +2 -2
  56. package/.output/server/_ssr/{setting-fields-8vKFj4df.mjs → setting-fields-DzBlslan.mjs} +6 -13
  57. package/.output/server/_ssr/ssr.mjs +45 -33
  58. package/.output/server/_ssr/{start-Bdc2GJUI.mjs → start-Vq3Oan3v.mjs} +3 -4
  59. package/.output/server/_ssr/{switch-ReNglgH6.mjs → switch-1pnd_bPJ.mjs} +2 -2
  60. package/.output/server/_ssr/{task-list-CUITIqkb.mjs → task-list-_qgBS9LU.mjs} +9 -8
  61. package/.output/server/_ssr/{tasks-vcoodp_D.mjs → tasks-BDqznm57.mjs} +98 -8
  62. package/.output/server/_ssr/{tasks-SAjNovX6.mjs → tasks-Qtml5TlK.mjs} +15 -6
  63. package/.output/server/_ssr/textarea-CHJTKtsJ.mjs +14 -0
  64. package/.output/server/{_tanstack-start-manifest_v-C0cP5F0M.mjs → _tanstack-start-manifest_v-CvheE7ZF.mjs} +54 -48
  65. package/.output/server/{_taskId-CXS3lod0.mjs → _taskId-V4QLBO2N.mjs} +17 -10
  66. package/.output/server/index.mjs +173 -166
  67. package/README.md +26 -21
  68. package/dist/cli.mjs +7146 -16
  69. package/dist/dispatcher.mjs +844 -33
  70. package/drizzle/0024_prompt_tasks.sql +44 -0
  71. package/drizzle/meta/_journal.json +7 -0
  72. package/package.json +21 -17
  73. package/.output/public/assets/_connectorId-BWjYn5tm.js +0 -1
  74. package/.output/public/assets/_loopId-Ds4RkyEc.js +0 -1
  75. package/.output/public/assets/_taskId-BOzICXDf.js +0 -1
  76. package/.output/public/assets/alert-Bn9-Zm0l.js +0 -1
  77. package/.output/public/assets/arrow-left-BF9juV9T.js +0 -1
  78. package/.output/public/assets/arrow-right-CzqvnOY1.js +0 -1
  79. package/.output/public/assets/button-HCY6DCY-.js +0 -1
  80. package/.output/public/assets/connection-status-CYZB8kLy.js +0 -1
  81. package/.output/public/assets/connector-icon-BZ_MUJV_.js +0 -1
  82. package/.output/public/assets/connectors-BvzttfSu.js +0 -1
  83. package/.output/public/assets/connectors-DdJNwRN_.js +0 -1
  84. package/.output/public/assets/gaps-D-I6xiDc.js +0 -1
  85. package/.output/public/assets/inbox-BUUu9xxE.js +0 -1
  86. package/.output/public/assets/index-ztzex0FN.js +0 -10
  87. package/.output/public/assets/loop-form-dB3CLcZb.js +0 -1
  88. package/.output/public/assets/loops-DRunQrxL.js +0 -1
  89. package/.output/public/assets/loops-oSdqMd29.js +0 -1
  90. package/.output/public/assets/manifests-DNMqf7Vi.js +0 -7
  91. package/.output/public/assets/new-uyFxVjuB.js +0 -1
  92. package/.output/public/assets/preload-helper-ORbuV86A.js +0 -3
  93. package/.output/public/assets/runners-DSU3r5Ww.js +0 -1
  94. package/.output/public/assets/setting-fields-ixkpdBb-.js +0 -3
  95. package/.output/public/assets/styles-DZL5V1o7.css +0 -2
  96. package/.output/public/assets/switch-d5ummmHY.js +0 -1
  97. package/.output/public/assets/task-list-BpOStt9v.js +0 -1
  98. package/.output/public/assets/tasks-B8MsKgic.js +0 -1
  99. package/.output/server/_ssr/access-zIl15o8y.mjs +0 -42
  100. package/.output/server/_ssr/connector-icon-DZXQLLFo.mjs +0 -37
  101. package/.output/server/_ssr/inbox-k5zHergU.mjs +0 -35
@@ -1,10 +1,10 @@
1
1
  import { d as and, f as eq, h as lt, l as asc } from "../_libs/drizzle-orm.mjs";
2
- import { a as connections, i as authAttempts, s as db } from "./client-CKkOfh-s.mjs";
3
- import { i as readCredential, s as writeCredential, t as deleteCredential } from "./secrets-Hqti-niS.mjs";
2
+ import { a as connections, i as authAttempts, s as db } from "./client-BtVRVhQ2.mjs";
3
+ import { i as readCredential, s as writeCredential, t as deleteCredential } from "./secrets-CRnpwGBh.mjs";
4
4
  import { createHash, randomBytes, randomUUID } from "node:crypto";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
- //#region node_modules/.nitro/vite/services/ssr/assets/connections-qnw2GVlP.js
7
+ //#region node_modules/.nitro/vite/services/ssr/assets/connections-Df-aDpfJ.js
8
8
  function defineManifest(manifest) {
9
9
  return manifest;
10
10
  }
@@ -468,7 +468,7 @@ var weekday = {
468
468
  })),
469
469
  default: "1"
470
470
  };
471
- var folder$1 = {
471
+ var folder$3 = {
472
472
  key: "folder",
473
473
  kind: "text",
474
474
  label: "Folder to work in",
@@ -515,7 +515,7 @@ var scheduleManifest = defineManifest({
515
515
  every,
516
516
  at,
517
517
  weekday,
518
- folder$1
518
+ folder$3
519
519
  ],
520
520
  prompt: RECURRING_PROMPT,
521
521
  guidancePlaceholder: "Anything that holds every time this runs. For example: this repository releases on Thursdays, so say if anything is unmerged by Wednesday evening.",
@@ -530,6 +530,159 @@ var scheduleManifest = defineManifest({
530
530
  accepts: ["text", "review"]
531
531
  }]
532
532
  });
533
+ var folder$2 = {
534
+ key: "folder",
535
+ kind: "text",
536
+ label: "Folder to work in",
537
+ help: "An absolute path to a checkout on this host. The agent runs here, so it can read the code you are asking about.",
538
+ placeholder: "/Users/you/code/your-project"
539
+ };
540
+ var channel = {
541
+ key: "channel",
542
+ kind: "text",
543
+ label: "Channel",
544
+ help: "Leave empty to reply in the thread that asked. Set a channel ID when the work started somewhere else.",
545
+ placeholder: "C0123456789"
546
+ };
547
+ var ASK_PROMPT$2 = [
548
+ "Someone has sent you a message in Slack. Do what it asks, working in this directory,",
549
+ "and then answer them.",
550
+ "",
551
+ "You are answering into Slack. Be brief: a couple of short paragraphs at most,",
552
+ "no headings, no bullet lists unless you are genuinely listing things. Plain sentences.",
553
+ "",
554
+ "If the message asks a question about this code, read enough of it to answer",
555
+ "properly rather than guessing. If it asks for a change, make the change and",
556
+ "say in one line what you did. If the message is too vague to act on, say what",
557
+ "you need to know instead of assuming."
558
+ ].join("\n");
559
+ var slackManifest = defineManifest({
560
+ id: "slack",
561
+ name: "Slack bot",
562
+ tagline: "Paste a bot token from a Slack app — not a Slack login. @mention or DM the bot, and it answers in the thread.",
563
+ docsUrl: "https://api.slack.com/apps",
564
+ icon: "Slack",
565
+ accent: "bg-[#4a154b] text-white",
566
+ auth: {
567
+ kind: "token",
568
+ helpUrl: "https://api.slack.com/apps",
569
+ note: "This is a Slack app in the workspace, not a Slack account sign-in. Create an app at api.slack.com/apps, add bot scopes (channels:history, channels:read, groups:history, groups:read, im:history, im:read, mpim:history, mpim:read, chat:write), Install to Workspace, then paste the Bot User OAuth Token. Invite the bot to any channel it should watch.",
570
+ fields: [{
571
+ key: "botToken",
572
+ label: "Bot User OAuth Token",
573
+ placeholder: "xoxb-...",
574
+ secret: true
575
+ }]
576
+ },
577
+ workflows: [{
578
+ id: "slack.ask",
579
+ name: "Do what I ask the bot",
580
+ summary: "Runs your agent when someone @mentions the bot or DMs it, and answers in the thread.",
581
+ trigger: "the bot is @mentioned, or someone DMs it",
582
+ writes: "a reply in the Slack thread",
583
+ runsIn: "folder",
584
+ settings: [folder$2],
585
+ prompt: ASK_PROMPT$2,
586
+ guidancePlaceholder: "Anything the agent should know about this project before it starts. For example: run pnpm test before saying a change works.",
587
+ answer: "text",
588
+ actionId: "slack.reply",
589
+ pollEveryMs: 6e4
590
+ }],
591
+ actions: [{
592
+ id: "slack.reply",
593
+ name: "Reply in Slack",
594
+ summary: "Post a message back to the thread that asked, or to a channel you name.",
595
+ target: [channel],
596
+ accepts: ["text"]
597
+ }],
598
+ settings: [],
599
+ allowsMultipleAccounts: false,
600
+ byHand: { kind: "none" }
601
+ });
602
+ var folder$1 = {
603
+ key: "folder",
604
+ kind: "text",
605
+ label: "Folder to work in",
606
+ help: "An absolute path to a checkout on this host. The agent runs here, so it can read the code you are asking about.",
607
+ placeholder: "/Users/you/code/your-project"
608
+ };
609
+ var chat = {
610
+ key: "chat",
611
+ kind: "text",
612
+ label: "Chat ID",
613
+ help: "Leave empty to reply to the message that asked. Set a chat ID (oc_…) when the work started somewhere else.",
614
+ placeholder: "oc_..."
615
+ };
616
+ var ASK_PROMPT$1 = [
617
+ "Someone has sent you a message in Feishu. Do what it asks, working in this directory,",
618
+ "and then answer them.",
619
+ "",
620
+ "You are answering into Feishu. Be brief: a couple of short paragraphs at most,",
621
+ "no headings, no bullet lists unless you are genuinely listing things. Plain sentences.",
622
+ "",
623
+ "If the message asks a question about this code, read enough of it to answer",
624
+ "properly rather than guessing. If it asks for a change, make the change and",
625
+ "say in one line what you did. If the message is too vague to act on, say what",
626
+ "you need to know instead of assuming."
627
+ ].join("\n");
628
+ var feishuManifest = defineManifest({
629
+ id: "feishu",
630
+ name: "Feishu / Lark bot",
631
+ tagline: "Paste App ID and App Secret from a Feishu or Lark app — not a Feishu login. @mention the bot in a group, and it answers in the thread.",
632
+ docsUrl: "https://open.feishu.cn/app",
633
+ icon: "Feishu",
634
+ accent: "bg-[#3370ff] text-white",
635
+ auth: {
636
+ kind: "token",
637
+ helpUrl: "https://open.feishu.cn/app",
638
+ note: "This is a custom Feishu or Lark app, not a Feishu account sign-in. Create an app, enable Bot, add permissions (im:chat:readonly, im:message, im:message.group_msg, im:message:send_as_bot), create a version and publish it, then paste App ID and App Secret. Invite the bot to any group it should watch. Put lark in Open platform for Lark (international).",
639
+ fields: [
640
+ {
641
+ key: "appId",
642
+ label: "App ID",
643
+ placeholder: "cli_...",
644
+ secret: false
645
+ },
646
+ {
647
+ key: "appSecret",
648
+ label: "App Secret",
649
+ placeholder: "App Secret",
650
+ secret: true
651
+ },
652
+ {
653
+ key: "host",
654
+ label: "Open platform",
655
+ placeholder: "feishu or lark",
656
+ secret: false,
657
+ optional: true
658
+ }
659
+ ]
660
+ },
661
+ workflows: [{
662
+ id: "feishu.ask",
663
+ name: "Do what I ask the bot",
664
+ summary: "Runs your agent when someone @mentions the bot in a group it is in, and answers in the thread.",
665
+ trigger: "the bot is @mentioned in a group it is in",
666
+ writes: "a reply in the Feishu thread",
667
+ runsIn: "folder",
668
+ settings: [folder$1],
669
+ prompt: ASK_PROMPT$1,
670
+ guidancePlaceholder: "Anything the agent should know about this project before it starts. For example: run pnpm test before saying a change works.",
671
+ answer: "text",
672
+ actionId: "feishu.reply",
673
+ pollEveryMs: 6e4
674
+ }],
675
+ actions: [{
676
+ id: "feishu.reply",
677
+ name: "Reply in Feishu",
678
+ summary: "Post a message back to the thread that asked, or to a chat you name.",
679
+ target: [chat],
680
+ accepts: ["text"]
681
+ }],
682
+ settings: [],
683
+ allowsMultipleAccounts: false,
684
+ byHand: { kind: "none" }
685
+ });
533
686
  /**
534
687
  * Where the agent runs. Asking a phone "what broke the build" is only worth
535
688
  * anything if the agent can read the build, so this workflow works in a real
@@ -600,6 +753,8 @@ var ASK_PROMPT = [
600
753
  */
601
754
  var CONNECTOR_MANIFESTS = [
602
755
  githubManifest,
756
+ slackManifest,
757
+ feishuManifest,
603
758
  gmailManifest,
604
759
  {
605
760
  id: "wechat",
@@ -642,15 +797,22 @@ function connectorManifest(id) {
642
797
  return CONNECTOR_MANIFESTS.find((manifest) => manifest.id === id);
643
798
  }
644
799
  /**
645
- * Whether this connector is somewhere you have an account.
800
+ * Whether this connector is somewhere you have a Connection.
646
801
  *
647
802
  * Asked in three places that would otherwise each assume every connector is:
648
- * the page listing accounts, the check for what stands between a loop and
803
+ * the page listing connections, the check for what stands between a loop and
649
804
  * running, and the fetch of a credential for a run.
650
805
  */
651
806
  function needsAccount(id) {
652
807
  return connectorManifest(id)?.auth.kind !== "none";
653
808
  }
809
+ /**
810
+ * What one Connection of this connector is called in the UI.
811
+ * A Slack or Feishu bot is a pasted app credential, not a login.
812
+ */
813
+ function connectionNoun(id) {
814
+ return connectorManifest(id)?.auth.kind === "token" ? "bot" : "account";
815
+ }
654
816
  function connectorWorkflow(connectorId, workflowId) {
655
817
  return connectorManifest(connectorId)?.workflows.find((entry) => entry.id === workflowId);
656
818
  }
@@ -1464,7 +1626,7 @@ function mailRef(id) {
1464
1626
  * Where to send someone who wants to see the mail itself. Gmail has no
1465
1627
  * per-message API link, but the web client will open one by id.
1466
1628
  */
1467
- function permalink(id) {
1629
+ function permalink$2(id) {
1468
1630
  return `https://mail.google.com/mail/u/0/#all/${id}`;
1469
1631
  }
1470
1632
  function headerOf(message, name) {
@@ -1532,7 +1694,7 @@ function attachmentNames(message) {
1532
1694
  return names;
1533
1695
  }
1534
1696
  /** Enough of a message to pick it out of a list. */
1535
- function summarise$1(message) {
1697
+ function summarise$3(message) {
1536
1698
  const subject = headerOf(message, "Subject") || "(no subject)";
1537
1699
  const who = senderName(headerOf(message, "From"));
1538
1700
  const title = who ? `${who}: ${subject}` : subject;
@@ -1611,8 +1773,8 @@ async function pollGmail(input) {
1611
1773
  key: mailRef(id),
1612
1774
  kind: "email",
1613
1775
  ref: mailRef(id),
1614
- title: summarise$1(message),
1615
- url: permalink(id),
1776
+ title: summarise$3(message),
1777
+ url: permalink$2(id),
1616
1778
  preview: (message.snippet ?? "").trim() || void 0,
1617
1779
  payload: { messageId: id }
1618
1780
  });
@@ -1737,7 +1899,7 @@ async function usableToken(credential) {
1737
1899
  };
1738
1900
  }
1739
1901
  /** The mailbox, as a person would recognise it. */
1740
- function accountOf$1(emailAddress) {
1902
+ function accountOf$3(emailAddress) {
1741
1903
  return {
1742
1904
  id: emailAddress,
1743
1905
  label: emailAddress
@@ -1778,8 +1940,8 @@ var gmailRuntime = defineRuntime({
1778
1940
  return {
1779
1941
  kind: "email",
1780
1942
  ref: mailRef(message.id),
1781
- title: summarise$1(message),
1782
- url: permalink(message.id),
1943
+ title: summarise$3(message),
1944
+ url: permalink$2(message.id),
1783
1945
  context: [mailContext(message)]
1784
1946
  };
1785
1947
  },
@@ -1817,12 +1979,12 @@ var gmailRuntime = defineRuntime({
1817
1979
  });
1818
1980
  return {
1819
1981
  credential,
1820
- account: accountOf$1((await getProfile(credential.accessToken)).emailAddress)
1982
+ account: accountOf$3((await getProfile(credential.accessToken)).emailAddress)
1821
1983
  };
1822
1984
  },
1823
1985
  async identity(credential) {
1824
1986
  const { accessToken, refreshed } = await usableToken(credential);
1825
- const account = accountOf$1((await getProfile(accessToken)).emailAddress);
1987
+ const account = accountOf$3((await getProfile(accessToken)).emailAddress);
1826
1988
  return refreshed ? {
1827
1989
  account,
1828
1990
  renewedCredential: refreshed
@@ -1930,6 +2092,641 @@ var scheduleRuntime = {
1930
2092
  }
1931
2093
  };
1932
2094
  /**
2095
+ * Slack Web API, called with a bot token pasted from a workspace-installed app.
2096
+ * Loopable polls; Slack never has to reach this machine.
2097
+ */
2098
+ var API$1 = "https://slack.com/api";
2099
+ async function slack(path, token, init = {}) {
2100
+ const url = new URL(`${API$1}/${path}`);
2101
+ for (const [key, value] of Object.entries(init.query ?? {})) url.searchParams.set(key, value);
2102
+ let response;
2103
+ try {
2104
+ response = await fetch(url.href, {
2105
+ method: init.method ?? "GET",
2106
+ headers: {
2107
+ Authorization: `Bearer ${token}`,
2108
+ ...init.body ? { "Content-Type": "application/json" } : {}
2109
+ },
2110
+ body: init.body ? JSON.stringify(init.body) : void 0
2111
+ });
2112
+ } catch (error) {
2113
+ throw new TransientError(`Slack is unreachable: ${error.message}`);
2114
+ }
2115
+ if (response.status >= 500 || response.status === 429) throw new TransientError(`Slack answered ${response.status}`);
2116
+ if (!response.ok) throw new Error(`Slack answered ${response.status}`);
2117
+ return await response.json();
2118
+ }
2119
+ function refuse$1(error) {
2120
+ if (error === "invalid_auth" || error === "not_authed" || error === "token_revoked" || error === "account_inactive") throw new Error("Slack did not accept that token.");
2121
+ throw new Error(error ? `Slack refused: ${error}` : "Slack refused the request.");
2122
+ }
2123
+ async function authTest(botToken) {
2124
+ const body = await slack("auth.test", botToken);
2125
+ if (!body.ok) refuse$1(body.error);
2126
+ const teamUrl = typeof body.url === "string" ? body.url : "";
2127
+ const team = typeof body.team === "string" ? body.team : "";
2128
+ const teamId = typeof body.team_id === "string" ? body.team_id : "";
2129
+ const botUserId = typeof body.user_id === "string" ? body.user_id : "";
2130
+ const botId = typeof body.bot_id === "string" ? body.bot_id : "";
2131
+ if (!teamId || !botUserId) throw new Error("Slack did not return which workspace this bot belongs to.");
2132
+ return {
2133
+ botToken,
2134
+ teamId,
2135
+ team,
2136
+ teamUrl,
2137
+ botUserId,
2138
+ botId
2139
+ };
2140
+ }
2141
+ async function listConversations(credential) {
2142
+ const body = await slack("conversations.list", credential.botToken, { query: {
2143
+ types: "public_channel,private_channel,im,mpim",
2144
+ exclude_archived: "true",
2145
+ limit: "200"
2146
+ } });
2147
+ if (!body.ok) refuse$1(body.error);
2148
+ return (Array.isArray(body.channels) ? body.channels : []).map((raw) => {
2149
+ const channel = raw;
2150
+ if (!channel.id) return null;
2151
+ return {
2152
+ id: channel.id,
2153
+ name: channel.name ?? "",
2154
+ isIm: channel.is_im === true
2155
+ };
2156
+ }).filter((channel) => channel !== null);
2157
+ }
2158
+ async function history$1(credential, channelId, oldest) {
2159
+ const query = {
2160
+ channel: channelId,
2161
+ limit: "100",
2162
+ inclusive: "false"
2163
+ };
2164
+ if (oldest) query.oldest = oldest;
2165
+ const body = await slack("conversations.history", credential.botToken, { query });
2166
+ if (!body.ok) {
2167
+ if (body.error === "not_in_channel" || body.error === "channel_not_found") return [];
2168
+ refuse$1(body.error);
2169
+ }
2170
+ const messages = Array.isArray(body.messages) ? body.messages : [];
2171
+ const parsed = [];
2172
+ for (const raw of messages) {
2173
+ const message = raw;
2174
+ if (!message.ts) continue;
2175
+ parsed.push({
2176
+ ts: message.ts,
2177
+ ...message.user ? { user: message.user } : {},
2178
+ ...message.bot_id ? { botId: message.bot_id } : {},
2179
+ text: message.text ?? "",
2180
+ ...message.thread_ts ? { threadTs: message.thread_ts } : {}
2181
+ });
2182
+ }
2183
+ return parsed.sort((a, b) => Number(a.ts) - Number(b.ts));
2184
+ }
2185
+ async function postMessage(credential, input) {
2186
+ const body = await slack("chat.postMessage", credential.botToken, {
2187
+ method: "POST",
2188
+ body: {
2189
+ channel: input.channel,
2190
+ text: input.text,
2191
+ ...input.threadTs ? { thread_ts: input.threadTs } : {}
2192
+ }
2193
+ });
2194
+ if (!body.ok) refuse$1(body.error);
2195
+ const ts = typeof body.ts === "string" ? body.ts : "";
2196
+ const channel = typeof body.channel === "string" ? body.channel : input.channel;
2197
+ if (!ts) throw new Error("Slack accepted the message but did not say where it landed.");
2198
+ return {
2199
+ ts,
2200
+ channel
2201
+ };
2202
+ }
2203
+ function permalink$1(credential, channel, ts) {
2204
+ return `${credential.teamUrl.replace(/\/$/, "")}/archives/${channel}/p${ts.replace(".", "")}`;
2205
+ }
2206
+ function asAsked$2(value) {
2207
+ const record = value;
2208
+ if (!record || typeof record.channel !== "string" || typeof record.ts !== "string") throw new Error("This task has lost the message it was about.");
2209
+ return {
2210
+ channel: record.channel,
2211
+ ts: record.ts,
2212
+ user: typeof record.user === "string" ? record.user : void 0,
2213
+ text: typeof record.text === "string" ? record.text : "",
2214
+ threadTs: typeof record.threadTs === "string" ? record.threadTs : record.ts
2215
+ };
2216
+ }
2217
+ function asCursor$1(value) {
2218
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2219
+ const out = {};
2220
+ for (const [key, entry] of Object.entries(value)) if (typeof entry === "string") out[key] = entry;
2221
+ return out;
2222
+ }
2223
+ function summarise$2(text) {
2224
+ const line = text.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
2225
+ if (!line) return "Slack message";
2226
+ return line.length > 80 ? `${line.slice(0, 79)}…` : line;
2227
+ }
2228
+ function isAsk$1(input) {
2229
+ if (input.user === input.botUserId || input.messageBotId && input.messageBotId === input.botId) return false;
2230
+ if (input.isIm) return Boolean(input.text.trim());
2231
+ return input.text.includes(`<@${input.botUserId}>`);
2232
+ }
2233
+ function destination$1(target, source) {
2234
+ if (typeof target.channel === "string" && target.channel.trim()) return { channel: target.channel.trim() };
2235
+ if (source.connectorId !== "slack") throw new Error("There is no Slack channel behind this task. Choose one on the loop.");
2236
+ const carry = source.carry ?? {};
2237
+ if (!carry.channel) throw new Error("There is no Slack channel behind this task. Choose one on the loop.");
2238
+ return {
2239
+ channel: carry.channel,
2240
+ threadTs: carry.ts
2241
+ };
2242
+ }
2243
+ function accountOf$2(credential) {
2244
+ return {
2245
+ id: credential.teamId,
2246
+ label: credential.team || credential.teamId,
2247
+ url: credential.teamUrl || void 0
2248
+ };
2249
+ }
2250
+ var slackRuntime = {
2251
+ async readiness() {
2252
+ return { ready: true };
2253
+ },
2254
+ async poll({ workflowId, credential, cursor }) {
2255
+ if (workflowId !== "slack.ask") throw new Error(`Slack cannot watch for ${workflowId}.`);
2256
+ const account = credential;
2257
+ const previous = asCursor$1(cursor);
2258
+ const channels = await listConversations(account);
2259
+ const next = { ...previous };
2260
+ const signals = [];
2261
+ for (const channel of channels) {
2262
+ const oldest = previous[channel.id];
2263
+ const messages = await history$1(account, channel.id, oldest);
2264
+ if (messages.length === 0) {
2265
+ if (!next[channel.id]) next[channel.id] = String(Date.now() / 1e3);
2266
+ continue;
2267
+ }
2268
+ next[channel.id] = messages[messages.length - 1].ts;
2269
+ if (!oldest) continue;
2270
+ for (const message of messages) {
2271
+ if (!isAsk$1({
2272
+ text: message.text,
2273
+ user: message.user,
2274
+ messageBotId: message.botId,
2275
+ isIm: channel.isIm,
2276
+ botUserId: account.botUserId,
2277
+ botId: account.botId
2278
+ })) continue;
2279
+ const asked = {
2280
+ channel: channel.id,
2281
+ ts: message.ts,
2282
+ user: message.user,
2283
+ text: message.text,
2284
+ threadTs: message.threadTs ?? message.ts
2285
+ };
2286
+ signals.push({
2287
+ key: `${channel.id}:${message.ts}`,
2288
+ kind: "message",
2289
+ ref: `${channel.id}:${message.ts}`,
2290
+ title: summarise$2(message.text),
2291
+ url: permalink$1(account, channel.id, message.ts),
2292
+ payload: asked
2293
+ });
2294
+ }
2295
+ }
2296
+ return {
2297
+ signals,
2298
+ cursor: next
2299
+ };
2300
+ },
2301
+ async resolveWorkItem({ payload }) {
2302
+ const asked = asAsked$2(payload);
2303
+ return {
2304
+ kind: "message",
2305
+ ref: `${asked.channel}:${asked.ts}`,
2306
+ title: summarise$2(asked.text),
2307
+ url: "",
2308
+ carry: {
2309
+ channel: asked.channel,
2310
+ ts: asked.threadTs
2311
+ },
2312
+ context: [{
2313
+ name: "MESSAGE.md",
2314
+ body: [
2315
+ `# What you were asked`,
2316
+ ``,
2317
+ asked.text,
2318
+ ``
2319
+ ].join("\n")
2320
+ }]
2321
+ };
2322
+ },
2323
+ async applyAction({ actionId, target, source, body, credential }) {
2324
+ if (actionId !== "slack.reply") throw new Error(`Slack cannot ${actionId}.`);
2325
+ const account = credential;
2326
+ const to = destination$1(target, source);
2327
+ const posted = await postMessage(account, {
2328
+ channel: to.channel,
2329
+ text: body.trim(),
2330
+ threadTs: to.threadTs
2331
+ });
2332
+ return { url: permalink$1(account, posted.channel, posted.ts) };
2333
+ },
2334
+ auth: {
2335
+ async connectWithFields(fields) {
2336
+ const botToken = fields.botToken?.trim() ?? "";
2337
+ if (!botToken.startsWith("xoxb-")) throw new Error("Paste the Bot User OAuth Token from your Slack app. It starts with xoxb-.");
2338
+ const credential = await authTest(botToken);
2339
+ return {
2340
+ credential,
2341
+ account: accountOf$2(credential)
2342
+ };
2343
+ },
2344
+ async identity(credential) {
2345
+ return { account: accountOf$2(await authTest(credential.botToken)) };
2346
+ }
2347
+ }
2348
+ };
2349
+ /**
2350
+ * Feishu / Lark Open API, called with a custom app's id and secret.
2351
+ * Loopable polls groups the bot is in; Feishu never has to reach this machine.
2352
+ */
2353
+ var API = {
2354
+ feishu: "https://open.feishu.cn/open-apis",
2355
+ lark: "https://open.larksuite.com/open-apis"
2356
+ };
2357
+ var APPLINK = {
2358
+ feishu: "https://applink.feishu.cn/client/chat/open",
2359
+ lark: "https://applink.larksuite.com/client/chat/open"
2360
+ };
2361
+ function parseHost(value) {
2362
+ const raw = (value ?? "feishu").trim().toLowerCase();
2363
+ if (!raw || raw === "feishu" || raw === "feishu.cn" || raw.includes("feishu.cn")) return "feishu";
2364
+ if (raw === "lark" || raw === "larksuite" || raw.includes("larksuite.com") || raw.includes("larkoffice.com")) return "lark";
2365
+ throw new Error("Open platform must be feishu (China) or lark (international).");
2366
+ }
2367
+ function refuse(code, msg) {
2368
+ if (code === 10003 || code === 10012 || code === 10014) throw new Error("Feishu did not accept those app credentials.");
2369
+ throw new Error(msg ? `Feishu refused: ${msg}` : "Feishu refused the request.");
2370
+ }
2371
+ async function call$1(host, path, init = {}) {
2372
+ const url = new URL(`${API[host]}${path}`);
2373
+ for (const [key, value] of Object.entries(init.query ?? {})) url.searchParams.set(key, value);
2374
+ let response;
2375
+ try {
2376
+ response = await fetch(url.href, {
2377
+ method: init.method ?? "GET",
2378
+ headers: {
2379
+ "Content-Type": "application/json; charset=utf-8",
2380
+ ...init.token ? { Authorization: `Bearer ${init.token}` } : {}
2381
+ },
2382
+ body: init.body ? JSON.stringify(init.body) : void 0
2383
+ });
2384
+ } catch (error) {
2385
+ throw new TransientError(`Feishu is unreachable: ${error.message}`);
2386
+ }
2387
+ if (response.status >= 500 || response.status === 429) throw new TransientError(`Feishu answered ${response.status}`);
2388
+ let body;
2389
+ try {
2390
+ body = await response.json();
2391
+ } catch {
2392
+ throw new Error(`Feishu answered ${response.status}`);
2393
+ }
2394
+ return body;
2395
+ }
2396
+ function dataOf(body) {
2397
+ if (body.code && body.code !== 0) refuse(body.code, body.msg);
2398
+ return body.data ?? {};
2399
+ }
2400
+ async function tenantAccessToken(host, appId, appSecret) {
2401
+ const body = await call$1(host, "/auth/v3/tenant_access_token/internal", {
2402
+ method: "POST",
2403
+ body: {
2404
+ app_id: appId,
2405
+ app_secret: appSecret
2406
+ }
2407
+ });
2408
+ if (body.code && body.code !== 0) refuse(body.code, body.msg);
2409
+ const token = body.tenant_access_token ?? "";
2410
+ const expire = typeof body.expire === "number" ? body.expire : 7200;
2411
+ if (!token) throw new Error("Feishu did not return a tenant token.");
2412
+ return {
2413
+ token,
2414
+ expiresAt: Date.now() + expire * 1e3 - 6e4
2415
+ };
2416
+ }
2417
+ async function botInfo(credential) {
2418
+ const body = await call$1(credential.host, "/bot/v3/info", { token: credential.tenantAccessToken });
2419
+ if (body.code && body.code !== 0) refuse(body.code, body.msg);
2420
+ const bot = body.bot ?? {};
2421
+ const openId = typeof bot.open_id === "string" ? bot.open_id : "";
2422
+ const name = typeof bot.app_name === "string" ? bot.app_name : "";
2423
+ const avatarUrl = typeof bot.avatar_url === "string" ? bot.avatar_url : void 0;
2424
+ if (!openId) throw new Error("Feishu did not return which bot this app is.");
2425
+ return {
2426
+ openId,
2427
+ name,
2428
+ ...avatarUrl ? { avatarUrl } : {}
2429
+ };
2430
+ }
2431
+ async function listChats(credential) {
2432
+ const chats = [];
2433
+ let pageToken = "";
2434
+ do {
2435
+ const query = { page_size: "100" };
2436
+ if (pageToken) query.page_token = pageToken;
2437
+ const data = dataOf(await call$1(credential.host, "/im/v1/chats", {
2438
+ token: credential.tenantAccessToken,
2439
+ query
2440
+ }));
2441
+ const items = Array.isArray(data.items) ? data.items : [];
2442
+ for (const raw of items) {
2443
+ const chat = raw;
2444
+ if (!chat.chat_id) continue;
2445
+ chats.push({
2446
+ id: chat.chat_id,
2447
+ name: chat.name ?? ""
2448
+ });
2449
+ }
2450
+ pageToken = data.has_more === true && typeof data.page_token === "string" ? data.page_token : "";
2451
+ } while (pageToken);
2452
+ return chats;
2453
+ }
2454
+ function textOf$1(content) {
2455
+ if (!content) return "";
2456
+ try {
2457
+ const parsed = JSON.parse(content);
2458
+ return typeof parsed.text === "string" ? parsed.text : content;
2459
+ } catch {
2460
+ return content;
2461
+ }
2462
+ }
2463
+ function mentionsOf(raw) {
2464
+ if (!Array.isArray(raw)) return [];
2465
+ return raw.map((entry) => {
2466
+ const mention = entry;
2467
+ return typeof mention.id === "string" ? mention.id : "";
2468
+ }).filter(Boolean);
2469
+ }
2470
+ async function history(credential, chatId, oldestMs) {
2471
+ const parsed = [];
2472
+ let pageToken = "";
2473
+ do {
2474
+ const query = {
2475
+ container_id_type: "chat",
2476
+ container_id: chatId,
2477
+ sort_type: oldestMs ? "ByCreateTimeAsc" : "ByCreateTimeDesc",
2478
+ page_size: oldestMs ? "50" : "1"
2479
+ };
2480
+ if (oldestMs) query.start_time = String(Math.floor(Number(oldestMs) / 1e3));
2481
+ if (pageToken) query.page_token = pageToken;
2482
+ const body = await call$1(credential.host, "/im/v1/messages", {
2483
+ token: credential.tenantAccessToken,
2484
+ query
2485
+ });
2486
+ if (body.code && body.code !== 0) {
2487
+ if (body.code === 230002 || body.code === 231203) return parsed;
2488
+ refuse(body.code, body.msg);
2489
+ }
2490
+ const data = body.data ?? {};
2491
+ const items = Array.isArray(data.items) ? data.items : [];
2492
+ for (const raw of items) {
2493
+ const message = raw;
2494
+ if (!message.message_id || message.deleted) continue;
2495
+ parsed.push({
2496
+ id: message.message_id,
2497
+ chatId: message.chat_id ?? chatId,
2498
+ createTime: message.create_time ?? "",
2499
+ text: textOf$1(message.body?.content),
2500
+ ...message.sender?.id ? { senderId: message.sender.id } : {},
2501
+ ...message.sender?.sender_type ? { senderType: message.sender.sender_type } : {},
2502
+ mentionIds: mentionsOf(message.mentions)
2503
+ });
2504
+ }
2505
+ pageToken = oldestMs && data.has_more === true && typeof data.page_token === "string" ? data.page_token : "";
2506
+ } while (pageToken);
2507
+ return parsed.sort((a, b) => Number(a.createTime) - Number(b.createTime));
2508
+ }
2509
+ async function replyMessage(credential, messageId, text) {
2510
+ const data = dataOf(await call$1(credential.host, `/im/v1/messages/${messageId}/reply`, {
2511
+ token: credential.tenantAccessToken,
2512
+ method: "POST",
2513
+ body: {
2514
+ msg_type: "text",
2515
+ content: JSON.stringify({ text })
2516
+ }
2517
+ }));
2518
+ const id = typeof data.message_id === "string" ? data.message_id : "";
2519
+ const chatId = typeof data.chat_id === "string" ? data.chat_id : "";
2520
+ if (!id) throw new Error("Feishu accepted the reply but did not say where it landed.");
2521
+ return {
2522
+ id,
2523
+ chatId
2524
+ };
2525
+ }
2526
+ async function sendMessage$1(credential, chatId, text) {
2527
+ const data = dataOf(await call$1(credential.host, "/im/v1/messages", {
2528
+ token: credential.tenantAccessToken,
2529
+ method: "POST",
2530
+ query: { receive_id_type: "chat_id" },
2531
+ body: {
2532
+ receive_id: chatId,
2533
+ msg_type: "text",
2534
+ content: JSON.stringify({ text })
2535
+ }
2536
+ }));
2537
+ const id = typeof data.message_id === "string" ? data.message_id : "";
2538
+ const landed = typeof data.chat_id === "string" ? data.chat_id : chatId;
2539
+ if (!id) throw new Error("Feishu accepted the message but did not say where it landed.");
2540
+ return {
2541
+ id,
2542
+ chatId: landed
2543
+ };
2544
+ }
2545
+ function permalink(host, chatId) {
2546
+ return `${APPLINK[host]}?openChatId=${chatId}`;
2547
+ }
2548
+ function asAsked$1(value) {
2549
+ const record = value;
2550
+ if (!record || typeof record.chatId !== "string" || typeof record.messageId !== "string") throw new Error("This task has lost the message it was about.");
2551
+ return {
2552
+ chatId: record.chatId,
2553
+ messageId: record.messageId,
2554
+ user: typeof record.user === "string" ? record.user : void 0,
2555
+ text: typeof record.text === "string" ? record.text : ""
2556
+ };
2557
+ }
2558
+ function asCursor(value) {
2559
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2560
+ const out = {};
2561
+ for (const [key, entry] of Object.entries(value)) if (typeof entry === "string") out[key] = entry;
2562
+ return out;
2563
+ }
2564
+ function summarise$1(text) {
2565
+ const line = text.replace(/@_user_\d+/g, "").replace(/\s+/g, " ").trim();
2566
+ if (!line) return "Feishu message";
2567
+ return line.length > 80 ? `${line.slice(0, 79)}…` : line;
2568
+ }
2569
+ function isAsk(input) {
2570
+ if (input.senderType === "app" || input.senderId === input.botOpenId) return false;
2571
+ return input.mentionIds.includes(input.botOpenId);
2572
+ }
2573
+ function destination(target, source) {
2574
+ if (typeof target.chat === "string" && target.chat.trim()) return { chatId: target.chat.trim() };
2575
+ if (source.connectorId !== "feishu") throw new Error("There is no Feishu chat behind this task. Choose one on the loop.");
2576
+ const carry = source.carry ?? {};
2577
+ if (!carry.chatId && !carry.messageId) throw new Error("There is no Feishu chat behind this task. Choose one on the loop.");
2578
+ return {
2579
+ chatId: carry.chatId,
2580
+ messageId: carry.messageId
2581
+ };
2582
+ }
2583
+ function accountOf$1(credential) {
2584
+ return {
2585
+ id: credential.botOpenId,
2586
+ label: credential.botName || credential.appId,
2587
+ ...credential.avatarUrl ? { avatarUrl: credential.avatarUrl } : {}
2588
+ };
2589
+ }
2590
+ /**
2591
+ * Tenant tokens last two hours. Minting on every poll would trip Feishu's
2592
+ * rate limit, so a still-valid one is reused and a fresh one is stored.
2593
+ */
2594
+ async function usable(credential) {
2595
+ if (Date.now() < credential.expiresAt) return { credential };
2596
+ const minted = await tenantAccessToken(credential.host, credential.appId, credential.appSecret);
2597
+ const refreshed = {
2598
+ ...credential,
2599
+ tenantAccessToken: minted.token,
2600
+ expiresAt: minted.expiresAt
2601
+ };
2602
+ return {
2603
+ credential: refreshed,
2604
+ refreshed
2605
+ };
2606
+ }
2607
+ var feishuRuntime = {
2608
+ async readiness() {
2609
+ return { ready: true };
2610
+ },
2611
+ async poll({ workflowId, credential, cursor }) {
2612
+ if (workflowId !== "feishu.ask") throw new Error(`Feishu cannot watch for ${workflowId}.`);
2613
+ const { credential: account } = await usable(credential);
2614
+ const previous = asCursor(cursor);
2615
+ const chats = await listChats(account);
2616
+ const next = { ...previous };
2617
+ const signals = [];
2618
+ for (const chat of chats) {
2619
+ const oldest = previous[chat.id];
2620
+ const messages = await history(account, chat.id, oldest);
2621
+ if (messages.length === 0) {
2622
+ if (!next[chat.id]) next[chat.id] = String(Date.now());
2623
+ continue;
2624
+ }
2625
+ next[chat.id] = messages[messages.length - 1].createTime;
2626
+ if (!oldest) continue;
2627
+ for (const message of messages) {
2628
+ if (oldest && Number(message.createTime) <= Number(oldest)) continue;
2629
+ if (!isAsk({
2630
+ senderId: message.senderId,
2631
+ senderType: message.senderType,
2632
+ mentionIds: message.mentionIds,
2633
+ botOpenId: account.botOpenId
2634
+ })) continue;
2635
+ const asked = {
2636
+ chatId: message.chatId,
2637
+ messageId: message.id,
2638
+ user: message.senderId,
2639
+ text: message.text
2640
+ };
2641
+ signals.push({
2642
+ key: `${message.chatId}:${message.id}`,
2643
+ kind: "message",
2644
+ ref: message.id,
2645
+ title: summarise$1(message.text),
2646
+ url: permalink(account.host, message.chatId),
2647
+ payload: asked
2648
+ });
2649
+ }
2650
+ }
2651
+ return {
2652
+ signals,
2653
+ cursor: next
2654
+ };
2655
+ },
2656
+ async resolveWorkItem({ payload }) {
2657
+ const asked = asAsked$1(payload);
2658
+ return {
2659
+ kind: "message",
2660
+ ref: asked.messageId,
2661
+ title: summarise$1(asked.text),
2662
+ url: "",
2663
+ carry: {
2664
+ chatId: asked.chatId,
2665
+ messageId: asked.messageId
2666
+ },
2667
+ context: [{
2668
+ name: "MESSAGE.md",
2669
+ body: [
2670
+ `# What you were asked`,
2671
+ ``,
2672
+ asked.text,
2673
+ ``
2674
+ ].join("\n")
2675
+ }]
2676
+ };
2677
+ },
2678
+ async applyAction({ actionId, target, source, body, credential }) {
2679
+ if (actionId !== "feishu.reply") throw new Error(`Feishu cannot ${actionId}.`);
2680
+ const { credential: account } = await usable(credential);
2681
+ const to = destination(target, source);
2682
+ const text = body.trim();
2683
+ const posted = to.messageId && source.connectorId === "feishu" ? await replyMessage(account, to.messageId, text) : await sendMessage$1(account, to.chatId ?? "", text);
2684
+ return { url: permalink(account.host, posted.chatId || to.chatId || "") };
2685
+ },
2686
+ auth: {
2687
+ async connectWithFields(fields) {
2688
+ const appId = fields.appId?.trim() ?? "";
2689
+ const appSecret = fields.appSecret?.trim() ?? "";
2690
+ if (!appId) throw new Error("Paste the App ID from your Feishu or Lark app.");
2691
+ if (!appSecret) throw new Error("Paste the App Secret from your Feishu or Lark app.");
2692
+ const host = parseHost(fields.host);
2693
+ const minted = await tenantAccessToken(host, appId, appSecret);
2694
+ const bot = await botInfo({
2695
+ host,
2696
+ tenantAccessToken: minted.token
2697
+ });
2698
+ const stored = {
2699
+ appId,
2700
+ appSecret,
2701
+ host,
2702
+ botOpenId: bot.openId,
2703
+ botName: bot.name,
2704
+ ...bot.avatarUrl ? { avatarUrl: bot.avatarUrl } : {},
2705
+ tenantAccessToken: minted.token,
2706
+ expiresAt: minted.expiresAt
2707
+ };
2708
+ return {
2709
+ credential: stored,
2710
+ account: accountOf$1(stored)
2711
+ };
2712
+ },
2713
+ async identity(credential) {
2714
+ const { credential: fresh, refreshed } = await usable(credential);
2715
+ const bot = await botInfo(fresh);
2716
+ const updated = {
2717
+ ...fresh,
2718
+ botOpenId: bot.openId,
2719
+ botName: bot.name,
2720
+ ...bot.avatarUrl ? { avatarUrl: bot.avatarUrl } : {}
2721
+ };
2722
+ return {
2723
+ account: accountOf$1(updated),
2724
+ ...refreshed ? { renewedCredential: updated } : {}
2725
+ };
2726
+ }
2727
+ }
2728
+ };
2729
+ /**
1933
2730
  * Tencent's iLink bot API, as documented for clients that are not OpenClaw:
1934
2731
  * https://github.com/Tencent/openclaw-weixin/blob/main/docs/protocol.md
1935
2732
  *
@@ -2264,6 +3061,8 @@ function accountOf(credential) {
2264
3061
  /** Server-only registry. Never import this from a component. */
2265
3062
  var RUNTIMES = {
2266
3063
  github: githubRuntime,
3064
+ slack: slackRuntime,
3065
+ feishu: feishuRuntime,
2267
3066
  gmail: gmailRuntime,
2268
3067
  wechat: {
2269
3068
  async readiness() {
@@ -2539,4 +3338,4 @@ async function checkConnection(id) {
2539
3338
  return toView(await getConnection(id));
2540
3339
  }
2541
3340
  //#endregion
2542
- export { connectorManifest as a, credentialForConnector as c, removeConnection as d, saveAuthorizedConnection as f, updateConnectionSettings as h, connectorAction as i, listConnections as l, takeAuthAttempt as m, allWorkflows as n, connectorRuntime as o, startAuthAttempt as p, checkConnection as r, connectorWorkflow as s, CONNECTOR_MANIFESTS as t, needsAccount as u };
3341
+ export { connectorAction as a, connectorWorkflow as c, needsAccount as d, removeConnection as f, updateConnectionSettings as g, takeAuthAttempt as h, connectionNoun as i, credentialForConnector as l, startAuthAttempt as m, allWorkflows as n, connectorManifest as o, saveAuthorizedConnection as p, checkConnection as r, connectorRuntime as s, CONNECTOR_MANIFESTS as t, listConnections as u };