@phuetz/code-buddy 1.1.0 → 1.2.0

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 (291) hide show
  1. package/README.md +160 -165
  2. package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
  3. package/dist/agent/codebuddy-agent.d.ts +1 -1
  4. package/dist/agent/codebuddy-agent.js +69 -14
  5. package/dist/agent/execution/agent-executor.js +122 -12
  6. package/dist/agent/execution/tool-dependency-graph.js +7 -0
  7. package/dist/agent/execution/tool-hooks.d.ts +6 -0
  8. package/dist/agent/execution/tool-hooks.js +13 -5
  9. package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
  10. package/dist/agent/execution/tool-selection-strategy.js +67 -10
  11. package/dist/agent/extended-thinking.d.ts +8 -0
  12. package/dist/agent/extended-thinking.js +27 -0
  13. package/dist/agent/hermes-browser-backends.js +151 -11
  14. package/dist/agent/hermes-claw-migrate.d.ts +59 -0
  15. package/dist/agent/hermes-claw-migrate.js +427 -26
  16. package/dist/agent/hermes-parity-manifest.js +78 -71
  17. package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
  18. package/dist/agent/hermes-runtime-lifecycle.js +468 -0
  19. package/dist/agent/middleware/index.d.ts +1 -0
  20. package/dist/agent/middleware/index.js +1 -0
  21. package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
  22. package/dist/agent/middleware/visual-validation-middleware.js +67 -0
  23. package/dist/agent/model-benchmark.d.ts +77 -0
  24. package/dist/agent/model-benchmark.js +309 -0
  25. package/dist/agent/model-tier.d.ts +14 -0
  26. package/dist/agent/model-tier.js +71 -0
  27. package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
  28. package/dist/agent/multi-agent/multi-agent-system.js +3 -1
  29. package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
  30. package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
  31. package/dist/agent/multi-agent/session-tools.js +22 -5
  32. package/dist/agent/reasoning/mcts.js +20 -6
  33. package/dist/agent/session-end-flush.d.ts +6 -1
  34. package/dist/agent/session-end-flush.js +20 -4
  35. package/dist/agent/streaming/streaming-handler.d.ts +7 -0
  36. package/dist/agent/streaming/streaming-handler.js +8 -1
  37. package/dist/agent/tool-executor.js +1 -0
  38. package/dist/agent/tool-handler.d.ts +5 -0
  39. package/dist/agent/tool-handler.js +130 -4
  40. package/dist/browser-automation/browser-operator-executor.js +15 -0
  41. package/dist/browser-automation/browser-use-runner.d.ts +96 -0
  42. package/dist/browser-automation/browser-use-runner.js +492 -0
  43. package/dist/browser-automation/camofox-runner.d.ts +107 -0
  44. package/dist/browser-automation/camofox-runner.js +287 -0
  45. package/dist/channels/dingtalk/index.js +3 -0
  46. package/dist/channels/discord/client.d.ts +9 -0
  47. package/dist/channels/discord/client.js +12 -0
  48. package/dist/channels/feishu/index.d.ts +167 -1
  49. package/dist/channels/feishu/index.js +432 -7
  50. package/dist/channels/gateway-lifecycle.d.ts +132 -0
  51. package/dist/channels/gateway-lifecycle.js +219 -0
  52. package/dist/channels/google-chat/index.d.ts +5 -1
  53. package/dist/channels/google-chat/index.js +5 -1
  54. package/dist/channels/imessage/index.d.ts +14 -0
  55. package/dist/channels/imessage/index.js +64 -17
  56. package/dist/channels/index.d.ts +4 -0
  57. package/dist/channels/index.js +4 -0
  58. package/dist/channels/irc/index.d.ts +73 -3
  59. package/dist/channels/irc/index.js +446 -11
  60. package/dist/channels/line/index.js +3 -0
  61. package/dist/channels/mattermost/index.d.ts +51 -4
  62. package/dist/channels/mattermost/index.js +303 -20
  63. package/dist/channels/nextcloud-talk/index.d.ts +89 -8
  64. package/dist/channels/nextcloud-talk/index.js +367 -16
  65. package/dist/channels/nostr/index.d.ts +121 -1
  66. package/dist/channels/nostr/index.js +396 -8
  67. package/dist/channels/ntfy/index.js +3 -0
  68. package/dist/channels/qq/index.js +3 -0
  69. package/dist/channels/slash-parity.d.ts +117 -0
  70. package/dist/channels/slash-parity.js +185 -0
  71. package/dist/channels/synology-chat/index.js +3 -0
  72. package/dist/channels/teams/index.d.ts +3 -0
  73. package/dist/channels/teams/index.js +3 -0
  74. package/dist/channels/telegram/client.d.ts +9 -0
  75. package/dist/channels/telegram/client.js +12 -0
  76. package/dist/channels/twilio-voice/index.js +3 -0
  77. package/dist/channels/webchat/index.d.ts +7 -1
  78. package/dist/channels/webchat/index.js +7 -1
  79. package/dist/channels/wecom/index.js +3 -0
  80. package/dist/channels/weixin/index.js +3 -0
  81. package/dist/channels/zalo/index.js +3 -0
  82. package/dist/cli/config-loader.js +1 -1
  83. package/dist/codebuddy/client.d.ts +30 -1
  84. package/dist/codebuddy/client.js +167 -20
  85. package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
  86. package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
  87. package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
  88. package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
  89. package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
  90. package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
  91. package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
  92. package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
  93. package/dist/codebuddy/tool-definitions/index.js +2 -1
  94. package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
  95. package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
  96. package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
  97. package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
  98. package/dist/codebuddy/tools.d.ts +1 -1
  99. package/dist/codebuddy/tools.js +27 -24
  100. package/dist/commands/cli/hermes-commands.js +108 -0
  101. package/dist/commands/cli/native-engine-commands.js +102 -3
  102. package/dist/commands/cli/utility-commands.js +45 -0
  103. package/dist/commands/enhanced-command-handler.js +1 -1
  104. package/dist/commands/goal-cli.d.ts +30 -0
  105. package/dist/commands/goal-cli.js +210 -27
  106. package/dist/commands/handlers/goal-handler.d.ts +9 -2
  107. package/dist/commands/handlers/goal-handler.js +36 -9
  108. package/dist/commands/handlers/infra-handlers.js +36 -0
  109. package/dist/commands/handlers/memory-handlers.js +96 -1
  110. package/dist/commands/headless-slash.d.ts +2 -0
  111. package/dist/commands/headless-slash.js +12 -0
  112. package/dist/commands/llm-provider-resolution.d.ts +5 -3
  113. package/dist/commands/llm-provider-resolution.js +87 -33
  114. package/dist/commands/ollama.d.ts +25 -0
  115. package/dist/commands/ollama.js +100 -0
  116. package/dist/commands/provider.d.ts +5 -0
  117. package/dist/commands/provider.js +106 -55
  118. package/dist/commands/spec-next.js +2 -1
  119. package/dist/commands/spec-plan.js +5 -16
  120. package/dist/commands/tunnel.d.ts +2 -0
  121. package/dist/commands/tunnel.js +48 -0
  122. package/dist/config/config-resolver.d.ts +2 -1
  123. package/dist/config/config-resolver.js +54 -41
  124. package/dist/config/constants.d.ts +28 -0
  125. package/dist/config/constants.js +7 -0
  126. package/dist/config/env-schema.js +756 -0
  127. package/dist/config/model-tools.js +4 -4
  128. package/dist/config/toml-config.d.ts +16 -0
  129. package/dist/config/toml-config.js +3 -0
  130. package/dist/context/context-manager-v2.js +1 -0
  131. package/dist/daemon/autonomous-daemon.d.ts +1 -1
  132. package/dist/daemon/autonomous-daemon.js +3 -3
  133. package/dist/daemon/autonomous-loop.js +11 -2
  134. package/dist/daemon/colab-goal.js +9 -1
  135. package/dist/daemon/cron-agent-bridge.d.ts +12 -3
  136. package/dist/daemon/cron-agent-bridge.js +25 -9
  137. package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
  138. package/dist/desktop/codebuddy-engine-adapter.js +257 -80
  139. package/dist/desktop/engine-adapter.d.ts +14 -0
  140. package/dist/desktop-automation/automation-manager.js +16 -0
  141. package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
  142. package/dist/desktop-automation/omniparser-runner.js +115 -0
  143. package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
  144. package/dist/desktop-automation/smart-snapshot.js +72 -5
  145. package/dist/doctor/index.js +40 -23
  146. package/dist/events/types.d.ts +11 -0
  147. package/dist/fleet/colab-store.js +14 -2
  148. package/dist/fleet/peer-session-bridge.js +50 -12
  149. package/dist/fleet/peer-tool-bridge.js +14 -0
  150. package/dist/goals/goal-decomposer.d.ts +27 -0
  151. package/dist/goals/goal-decomposer.js +293 -0
  152. package/dist/goals/goal-judge-client.d.ts +15 -0
  153. package/dist/goals/goal-judge-client.js +45 -0
  154. package/dist/goals/goal-judge.d.ts +2 -0
  155. package/dist/goals/goal-judge.js +35 -6
  156. package/dist/goals/goal-loop.d.ts +14 -0
  157. package/dist/goals/goal-loop.js +47 -2
  158. package/dist/goals/goal-manager.d.ts +6 -0
  159. package/dist/goals/goal-manager.js +67 -14
  160. package/dist/goals/goal-state.d.ts +13 -0
  161. package/dist/goals/goal-state.js +83 -13
  162. package/dist/goals/goal-store.d.ts +3 -0
  163. package/dist/goals/goal-store.js +31 -15
  164. package/dist/hooks/user-hooks.js +17 -3
  165. package/dist/index.js +464 -25
  166. package/dist/input/text-to-speech.d.ts +2 -6
  167. package/dist/input/text-to-speech.js +2 -27
  168. package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
  169. package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
  170. package/dist/integrations/tailscale.d.ts +13 -0
  171. package/dist/integrations/tailscale.js +87 -34
  172. package/dist/knowledge/workspace-indexer.js +53 -9
  173. package/dist/mcp/client.js +1 -0
  174. package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
  175. package/dist/mcp/mcp-desktop-tools.js +158 -0
  176. package/dist/mcp/mcp-server.js +2 -0
  177. package/dist/memory/enhanced-memory.d.ts +10 -0
  178. package/dist/memory/enhanced-memory.js +33 -2
  179. package/dist/memory/index.d.ts +3 -1
  180. package/dist/memory/index.js +4 -1
  181. package/dist/memory/memory-auto-proposer.d.ts +23 -0
  182. package/dist/memory/memory-auto-proposer.js +308 -0
  183. package/dist/memory/memory-candidate-queue.d.ts +92 -0
  184. package/dist/memory/memory-candidate-queue.js +261 -0
  185. package/dist/memory/persistent-memory.d.ts +45 -2
  186. package/dist/memory/persistent-memory.js +236 -40
  187. package/dist/ml/bayesian-qualifier.d.ts +1 -1
  188. package/dist/ml/bayesian-qualifier.js +35 -2
  189. package/dist/openclaw/gateway-bridge.js +5 -0
  190. package/dist/plugins/marketplace.d.ts +1 -0
  191. package/dist/plugins/marketplace.js +7 -0
  192. package/dist/prompts/execution-discipline.d.ts +14 -0
  193. package/dist/prompts/execution-discipline.js +29 -0
  194. package/dist/prompts/variation-injector.js +9 -2
  195. package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
  196. package/dist/protocols/acp/acp-agentic-runner.js +115 -18
  197. package/dist/protocols/acp/acp-session-store.d.ts +23 -0
  198. package/dist/protocols/acp/acp-session-store.js +77 -0
  199. package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
  200. package/dist/protocols/acp/acp-stdio-server.js +71 -16
  201. package/dist/providers/active-llm-registry.d.ts +37 -0
  202. package/dist/providers/active-llm-registry.js +186 -0
  203. package/dist/providers/auxiliary-provider.d.ts +25 -0
  204. package/dist/providers/auxiliary-provider.js +192 -0
  205. package/dist/providers/codex-oauth.d.ts +1 -1
  206. package/dist/providers/codex-oauth.js +27 -4
  207. package/dist/providers/index.d.ts +3 -0
  208. package/dist/providers/index.js +4 -0
  209. package/dist/providers/provider-catalog.d.ts +70 -0
  210. package/dist/providers/provider-catalog.js +738 -0
  211. package/dist/providers/provider-fallback.d.ts +35 -0
  212. package/dist/providers/provider-fallback.js +171 -0
  213. package/dist/providers/xai-oauth.d.ts +128 -0
  214. package/dist/providers/xai-oauth.js +735 -0
  215. package/dist/scheduler/cron-scheduler.d.ts +15 -3
  216. package/dist/scheduler/cron-scheduler.js +53 -7
  217. package/dist/scheduler/script-runner.d.ts +2 -0
  218. package/dist/scheduler/script-runner.js +19 -4
  219. package/dist/scheduler/watchdog-handlers.js +7 -20
  220. package/dist/search/usearch-index.js +7 -2
  221. package/dist/security/tool-policy/tool-groups.js +2 -0
  222. package/dist/server/index.js +17 -2
  223. package/dist/server/routes/mobile.d.ts +12 -4
  224. package/dist/server/routes/mobile.js +116 -1
  225. package/dist/server/tls-config.d.ts +35 -0
  226. package/dist/server/tls-config.js +142 -0
  227. package/dist/server/tunnel-manager.d.ts +20 -0
  228. package/dist/server/tunnel-manager.js +58 -0
  229. package/dist/server/websocket/fleet-bridge.js +5 -0
  230. package/dist/services/prompt-builder.d.ts +1 -0
  231. package/dist/services/prompt-builder.js +49 -8
  232. package/dist/shared/engine-types.d.ts +15 -1
  233. package/dist/sidecar/sidecar-bridge.d.ts +1 -0
  234. package/dist/sidecar/sidecar-bridge.js +21 -10
  235. package/dist/spec/spec-store.js +8 -1
  236. package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
  237. package/dist/talk-mode/providers/audioreader-tts.js +8 -8
  238. package/dist/templates/project-scaffolding.js +3 -2
  239. package/dist/themes/theme-schema.d.ts +10 -10
  240. package/dist/tools/application-profiles.js +38 -0
  241. package/dist/tools/bash/bash-tool.d.ts +1 -0
  242. package/dist/tools/bash/bash-tool.js +16 -2
  243. package/dist/tools/bash/command-validator.js +3 -0
  244. package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
  245. package/dist/tools/bash/rtk-rewrite.js +100 -0
  246. package/dist/tools/bash/streaming-executor.js +14 -1
  247. package/dist/tools/computer-control-tool.d.ts +16 -1
  248. package/dist/tools/computer-control-tool.js +317 -5
  249. package/dist/tools/document-generator.d.ts +14 -0
  250. package/dist/tools/document-generator.js +79 -0
  251. package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
  252. package/dist/tools/execute-code-rpc-invoker.js +46 -5
  253. package/dist/tools/metadata.js +28 -0
  254. package/dist/tools/office-macro-tool.d.ts +10 -0
  255. package/dist/tools/office-macro-tool.js +93 -0
  256. package/dist/tools/registry/index.d.ts +2 -1
  257. package/dist/tools/registry/index.js +2 -1
  258. package/dist/tools/registry/memory-tools.d.ts +20 -0
  259. package/dist/tools/registry/memory-tools.js +225 -3
  260. package/dist/tools/registry/multimodal-tools.js +2 -2
  261. package/dist/tools/registry/vision-tools.d.ts +33 -0
  262. package/dist/tools/registry/vision-tools.js +308 -6
  263. package/dist/tools/registry/windows-tools.d.ts +2 -0
  264. package/dist/tools/registry/windows-tools.js +47 -0
  265. package/dist/tools/route-peer-tool.js +14 -0
  266. package/dist/tools/screenshot-tool.js +14 -2
  267. package/dist/tools/text-to-speech-tool.d.ts +1 -1
  268. package/dist/tools/text-to-speech-tool.js +2 -12
  269. package/dist/tools/tool-selector.js +3 -59
  270. package/dist/utils/config-validation/schema.d.ts +40 -7
  271. package/dist/utils/config-validation/schema.js +130 -1
  272. package/dist/utils/cost-tracker.js +21 -1
  273. package/dist/utils/disk-guard.d.ts +180 -0
  274. package/dist/utils/disk-guard.js +404 -0
  275. package/dist/utils/interactive-setup.js +35 -2
  276. package/dist/utils/model-utils.js +11 -2
  277. package/dist/utils/output-sanitizer.js +11 -0
  278. package/dist/utils/provider-detector.d.ts +8 -7
  279. package/dist/utils/provider-detector.js +19 -77
  280. package/dist/utils/settings-manager.d.ts +7 -0
  281. package/dist/utils/token-display.d.ts +3 -3
  282. package/dist/utils/token-display.js +22 -4
  283. package/dist/wizard/onboarding.d.ts +39 -0
  284. package/dist/wizard/onboarding.js +433 -21
  285. package/dist/wizard/provider-onboarding.d.ts +8 -23
  286. package/dist/wizard/provider-onboarding.js +39 -158
  287. package/package.json +4 -2
  288. package/dist/plugins/provider-onboarding.d.ts +0 -23
  289. package/dist/plugins/provider-onboarding.js +0 -116
  290. package/dist/utils/kokoro-tts.d.ts +0 -8
  291. package/dist/utils/kokoro-tts.js +0 -67
@@ -9,7 +9,148 @@
9
9
  * identity-aware headers, full thread context.
10
10
  */
11
11
  import { logger } from '../../utils/logger.js';
12
- import { BaseChannel } from '../core.js';
12
+ import { BaseChannel, } from '../core.js';
13
+ /** Map a Feishu `message_type` onto a Code Buddy {@link ContentType}. */
14
+ function feishuMessageTypeToContentType(messageType) {
15
+ switch (messageType) {
16
+ case 'image':
17
+ return 'image';
18
+ case 'audio':
19
+ return 'audio';
20
+ case 'media':
21
+ return 'video';
22
+ case 'file':
23
+ return 'file';
24
+ case 'sticker':
25
+ return 'sticker';
26
+ // 'text', 'post', 'interactive', 'share_chat', … all surface as text.
27
+ default:
28
+ return 'text';
29
+ }
30
+ }
31
+ /**
32
+ * Best-effort plain-text extraction from a Feishu message `content` JSON string.
33
+ *
34
+ * - `text` → `{ "text": "hello" }`
35
+ * - `post` → `{ "<locale>": { "title": "...", "content": [[{tag,text}, …]] } }`
36
+ * - otherwise → the raw JSON string (so callers still see *something*).
37
+ */
38
+ function extractFeishuText(messageType, contentJson) {
39
+ if (!contentJson)
40
+ return '';
41
+ let parsed;
42
+ try {
43
+ parsed = JSON.parse(contentJson);
44
+ }
45
+ catch {
46
+ // Not JSON (shouldn't happen for real events) — fall back to the raw string.
47
+ return contentJson;
48
+ }
49
+ if (parsed && typeof parsed === 'object') {
50
+ const obj = parsed;
51
+ // text / share_chat / etc.
52
+ if (typeof obj.text === 'string')
53
+ return obj.text;
54
+ // post: locale-keyed rich text. Walk the first locale's `content` matrix
55
+ // and concatenate every `text` segment.
56
+ if (messageType === 'post' || (!('text' in obj) && hasPostShape(obj))) {
57
+ const localeKey = Object.keys(obj)[0];
58
+ const post = localeKey ? obj[localeKey] : undefined;
59
+ const rows = post?.['content'];
60
+ if (Array.isArray(rows)) {
61
+ const pieces = [];
62
+ const title = typeof post?.['title'] === 'string' ? post['title'] : '';
63
+ if (title)
64
+ pieces.push(title);
65
+ for (const row of rows) {
66
+ if (!Array.isArray(row))
67
+ continue;
68
+ for (const seg of row) {
69
+ if (seg && typeof seg === 'object') {
70
+ const t = seg.text;
71
+ if (typeof t === 'string')
72
+ pieces.push(t);
73
+ }
74
+ }
75
+ }
76
+ return pieces.join(' ').trim();
77
+ }
78
+ }
79
+ }
80
+ // Unknown structure — return the raw JSON so nothing is silently dropped.
81
+ return contentJson;
82
+ }
83
+ function hasPostShape(obj) {
84
+ const firstKey = Object.keys(obj)[0];
85
+ if (!firstKey)
86
+ return false;
87
+ const v = obj[firstKey];
88
+ return !!v && typeof v === 'object' && 'content' in v;
89
+ }
90
+ /**
91
+ * Parse a Lark `im.message.receive_v1` event into a Code Buddy
92
+ * {@link InboundMessage}.
93
+ *
94
+ * Envelope-tolerant: accepts either the unwrapped event body (what the SDK's
95
+ * `EventDispatcher` hands the handler) or the full `{ schema, header, event }`
96
+ * webhook envelope.
97
+ *
98
+ * Returns `null` when the event carries no usable message (e.g. a non-message
99
+ * event slipped through, or no chat/text could be resolved).
100
+ */
101
+ export function parseFeishuMessageEvent(raw) {
102
+ if (!raw || typeof raw !== 'object')
103
+ return null;
104
+ // Unwrap `{ event: … }` if present; otherwise treat `raw` as the body.
105
+ const body = 'event' in raw && raw.event ? raw.event : raw;
106
+ const message = body.message;
107
+ if (!message)
108
+ return null;
109
+ const chatId = message.chat_id;
110
+ if (!chatId)
111
+ return null;
112
+ const senderId = body.sender?.sender_id?.open_id ??
113
+ body.sender?.sender_id?.union_id ??
114
+ body.sender?.sender_id?.user_id ??
115
+ 'unknown';
116
+ const content = extractFeishuText(message.message_type, message.content);
117
+ const createTimeMs = message.create_time ? Number(message.create_time) : NaN;
118
+ const timestamp = Number.isFinite(createTimeMs) ? new Date(createTimeMs) : new Date();
119
+ const inbound = {
120
+ id: message.message_id ?? `feishu_${Date.now()}`,
121
+ channel: {
122
+ id: chatId,
123
+ type: 'feishu',
124
+ isDM: message.chat_type === 'p2p',
125
+ isGroup: message.chat_type === 'group',
126
+ },
127
+ sender: {
128
+ id: senderId,
129
+ raw: body.sender,
130
+ },
131
+ content,
132
+ contentType: feishuMessageTypeToContentType(message.message_type),
133
+ timestamp,
134
+ raw,
135
+ };
136
+ if (message.thread_id)
137
+ inbound.threadId = message.thread_id;
138
+ if (message.parent_id)
139
+ inbound.replyTo = message.parent_id;
140
+ return inbound;
141
+ }
142
+ /**
143
+ * Legacy in-process adapter.
144
+ *
145
+ * The real outbound transport now lives in {@link FeishuChannel}, which mints a
146
+ * genuine `tenant_access_token` and POSTs through the REST `im/v1/messages`
147
+ * API. This class is retained for its still-useful, network-free helpers — the
148
+ * interactive-card builders, reasoning-stream hooks, and `getThreadMessages`
149
+ * (exercised by `tests/channels/feishu-cards.test.ts`). Its `sendText` /
150
+ * `sendCard` / `sendImage` / `replyMessage` methods, however, perform NO
151
+ * network I/O and return synthetic ids; they are kept only for backward-compat
152
+ * and are not on the real send path. Prefer {@link FeishuChannel.send}.
153
+ */
13
154
  export class FeishuAdapter {
14
155
  config;
15
156
  running = false;
@@ -39,6 +180,7 @@ export class FeishuAdapter {
39
180
  isRunning() {
40
181
  return this.running;
41
182
  }
183
+ /** @deprecated No network I/O — returns a synthetic id. Use {@link FeishuChannel.send}. */
42
184
  async sendText(chatId, text) {
43
185
  if (!this.running) {
44
186
  throw new Error('FeishuAdapter is not running');
@@ -47,6 +189,7 @@ export class FeishuAdapter {
47
189
  logger.debug('FeishuAdapter: send text', { chatId, textLength: text.length });
48
190
  return { success: true, messageId };
49
191
  }
192
+ /** @deprecated No network I/O — returns a synthetic id. Use {@link FeishuChannel.send}. */
50
193
  async sendCard(chatId, card) {
51
194
  if (!this.running) {
52
195
  throw new Error('FeishuAdapter is not running');
@@ -55,6 +198,7 @@ export class FeishuAdapter {
55
198
  logger.debug('FeishuAdapter: send card', { chatId });
56
199
  return { success: true, messageId };
57
200
  }
201
+ /** @deprecated No network I/O — returns a synthetic id. Use {@link FeishuChannel.send}. */
58
202
  async sendImage(chatId, imageKey) {
59
203
  if (!this.running) {
60
204
  throw new Error('FeishuAdapter is not running');
@@ -63,6 +207,7 @@ export class FeishuAdapter {
63
207
  logger.debug('FeishuAdapter: send image', { chatId, imageKey });
64
208
  return { success: true, messageId };
65
209
  }
210
+ /** @deprecated No network I/O. Use {@link FeishuChannel.send} with `replyTo` instead. */
66
211
  async replyMessage(messageId, text) {
67
212
  if (!this.running) {
68
213
  throw new Error('FeishuAdapter is not running');
@@ -194,11 +339,46 @@ export class FeishuAdapter {
194
339
  return [];
195
340
  }
196
341
  }
342
+ // ============================================================================
343
+ // Real-transport status
344
+ // ============================================================================
345
+ const DEFAULT_FEISHU_BASE_URL = 'https://open.feishu.cn';
197
346
  export class FeishuChannel extends BaseChannel {
198
347
  adapter = null;
348
+ baseUrl;
349
+ /** Cached tenant_access_token (re-minted on demand if the API rejects it). */
350
+ tenantToken = null;
351
+ receiveStatus = null;
352
+ /** Live official-SDK long-connection client, when the SDK is installed. */
353
+ wsClient = null;
199
354
  constructor(config) {
200
355
  super('feishu', config);
356
+ const rawBase = config.options?.['baseUrl'] ??
357
+ process.env.FEISHU_BASE_URL ??
358
+ process.env.LARK_BASE_URL ??
359
+ DEFAULT_FEISHU_BASE_URL;
360
+ this.baseUrl = rawBase.trim().replace(/\/+$/, '');
201
361
  }
362
+ /**
363
+ * Bring the channel up.
364
+ *
365
+ * Outbound (`send()`) is fully functional: it lazily mints a
366
+ * `tenant_access_token` and POSTs to `/open-apis/im/v1/messages`.
367
+ *
368
+ * Inbound (receiving user messages) requires Feishu's proprietary
369
+ * long-connection — see {@link FeishuReceiveStatus}. We do NOT fake it. We
370
+ * attempt to bring up the REAL inbound socket using the official Lark SDK
371
+ * (`@larksuiteoapi/node-sdk`), imported OPTIONALLY at runtime — it is not a
372
+ * declared dependency, so most installs won't have it. When it IS present
373
+ * (and app credentials are configured) we start its `WSClient` and register
374
+ * the `im.message.receive_v1` handler, which parses each message into an
375
+ * {@link InboundMessage} and re-emits it via `this.emit('message' | 'command')`.
376
+ * The SDK owns the long-connection + reconnect internally.
377
+ *
378
+ * When the SDK is absent (the default) we keep the honest "send-only" state:
379
+ * `status.connected = false`, the receive reason surfaced in
380
+ * `status.error` / `status.info`. No throw — outbound still works.
381
+ */
202
382
  async connect() {
203
383
  const cfg = this.config;
204
384
  this.adapter = new FeishuAdapter({
@@ -209,28 +389,185 @@ export class FeishuChannel extends BaseChannel {
209
389
  port: cfg.port,
210
390
  });
211
391
  await this.adapter.start();
212
- this.status.connected = true;
392
+ // Default to the honest send-only state. `tryStartInbound()` upgrades it
393
+ // in place if (and only if) the optional SDK is installed and usable.
394
+ this.receiveStatus = {
395
+ connected: false,
396
+ reason: 'lark-sdk-required',
397
+ detail: 'Feishu inbound long-connection (real-time receive) is not active: the ' +
398
+ 'official Lark SDK (@larksuiteoapi/node-sdk) — which owns the proprietary ' +
399
+ "Protobuf 'pbbp2' long-connection framing — is not installed (it is an " +
400
+ 'optional dependency). Install it and configure app credentials to enable ' +
401
+ 'real-time receive. Outbound send() is fully functional via the REST ' +
402
+ 'im/v1/messages API.',
403
+ };
404
+ this.status.connected = false;
405
+ this.status.authenticated = false;
406
+ this.status.error = this.receiveStatus.detail;
407
+ this.status.info = {
408
+ outbound: 'ready',
409
+ inbound: this.receiveStatus.reason,
410
+ };
411
+ await this.tryStartInbound(cfg);
412
+ }
413
+ /**
414
+ * Attempt to bring up the REAL inbound long-connection via the optional
415
+ * official Lark SDK. Mutates `this.receiveStatus` / `this.status` to the live
416
+ * `'lark-ws'` state on success; otherwise leaves the honest send-only state
417
+ * untouched. Never throws — a missing SDK or a start() failure degrades
418
+ * gracefully to outbound-only.
419
+ */
420
+ async tryStartInbound(cfg) {
421
+ // Optional dependency: the `as string` specifier keeps TS/Vite from trying
422
+ // to statically resolve a package that isn't installed, so this becomes a
423
+ // genuine runtime import that rejects (→ caught) when the SDK is absent.
424
+ const lark = (await import('@larksuiteoapi/node-sdk').catch(() => null));
425
+ if (!lark || typeof lark.WSClient !== 'function' || typeof lark.EventDispatcher !== 'function') {
426
+ // SDK not installed — keep the honest 'lark-sdk-required' state.
427
+ return;
428
+ }
429
+ if (!cfg.appId || !cfg.appSecret) {
430
+ // SDK present but unconfigured — still send-only. Keep the reason code
431
+ // (`lark-sdk-required`, so the honest-state contract holds) but correct
432
+ // the detail: the SDK is here; the missing piece is app credentials.
433
+ logger.warn('Feishu: Lark SDK installed but appId/appSecret missing — inbound disabled');
434
+ if (this.receiveStatus) {
435
+ this.receiveStatus.detail =
436
+ 'Feishu inbound long-connection is not active: the official Lark SDK ' +
437
+ '(@larksuiteoapi/node-sdk) IS installed, but appId/appSecret are not ' +
438
+ 'configured, so the WSClient cannot authenticate. Provide app credentials ' +
439
+ 'to enable real-time receive. Outbound send() is fully functional.';
440
+ this.status.error = this.receiveStatus.detail;
441
+ }
442
+ return;
443
+ }
444
+ try {
445
+ const dispatcher = new lark.EventDispatcher({}).register({
446
+ 'im.message.receive_v1': (data) => {
447
+ try {
448
+ this.dispatchInboundEvent(data);
449
+ }
450
+ catch (err) {
451
+ logger.warn(`Feishu inbound handler error: ${err instanceof Error ? err.message : err}`);
452
+ }
453
+ },
454
+ });
455
+ const wsClient = new lark.WSClient({
456
+ appId: cfg.appId,
457
+ appSecret: cfg.appSecret,
458
+ });
459
+ await wsClient.start({ eventDispatcher: dispatcher });
460
+ this.wsClient = wsClient;
461
+ this.receiveStatus = {
462
+ connected: true,
463
+ reason: 'lark-ws',
464
+ detail: 'Feishu inbound is live over the official Lark SDK WSClient long-connection ' +
465
+ '(@larksuiteoapi/node-sdk). im.message.receive_v1 events are parsed into ' +
466
+ 'InboundMessages and re-emitted; the SDK manages reconnect internally.',
467
+ };
468
+ this.status.connected = true;
469
+ this.status.authenticated = true;
470
+ delete this.status.error;
471
+ this.status.info = { outbound: 'ready', inbound: 'lark-ws' };
472
+ this.status.lastActivity = new Date();
473
+ logger.info('Feishu: inbound long-connection established via @larksuiteoapi/node-sdk');
474
+ }
475
+ catch (err) {
476
+ // start() failed (bad creds, network). Stay honest: send-only.
477
+ logger.warn(`Feishu: Lark WSClient failed to start, falling back to send-only: ${err instanceof Error ? err.message : err}`);
478
+ this.wsClient = null;
479
+ }
480
+ }
481
+ /**
482
+ * Parse a raw `im.message.receive_v1` event and re-emit it as a Code Buddy
483
+ * `message` (and `command`) event. This is the seam the live SDK handler
484
+ * delegates to; it is also unit-testable directly without a live tenant.
485
+ *
486
+ * @returns the parsed {@link InboundMessage}, or `null` if the event carried
487
+ * no usable message.
488
+ */
489
+ dispatchInboundEvent(event) {
490
+ const parsed = parseFeishuMessageEvent(event);
491
+ if (!parsed)
492
+ return null;
493
+ // Skip messages from the bot itself / disallowed users where configured.
494
+ if (!this.isUserAllowed(parsed.sender.id))
495
+ return null;
496
+ if (!this.isChannelAllowed(parsed.channel.id))
497
+ return null;
498
+ const withCommand = this.parseCommand(parsed);
499
+ this.status.lastActivity = new Date();
500
+ this.emit('message', withCommand);
501
+ if (withCommand.isCommand) {
502
+ this.emit('command', withCommand);
503
+ }
504
+ return withCommand;
213
505
  }
214
506
  async disconnect() {
507
+ if (this.wsClient) {
508
+ try {
509
+ await this.wsClient.stop?.();
510
+ }
511
+ catch (err) {
512
+ logger.debug(`Feishu: WSClient stop error: ${err instanceof Error ? err.message : err}`);
513
+ }
514
+ this.wsClient = null;
515
+ }
215
516
  if (this.adapter) {
216
517
  await this.adapter.stop();
217
518
  this.adapter = null;
218
519
  }
520
+ this.tenantToken = null;
521
+ this.receiveStatus = null;
219
522
  this.status.connected = false;
523
+ this.status.authenticated = false;
524
+ delete this.status.error;
525
+ this.emit('disconnected', this.type);
220
526
  }
527
+ /**
528
+ * Send an outbound message through the real Feishu REST API.
529
+ *
530
+ * Cards are sent with `msg_type: 'interactive'`; everything else as
531
+ * `msg_type: 'text'`. The chat id (`message.channelId`) becomes the
532
+ * `receive_id` with `receive_id_type=chat_id`.
533
+ */
221
534
  async send(message) {
222
535
  if (!this.adapter) {
223
536
  return { success: false, error: 'Not connected', timestamp: new Date() };
224
537
  }
225
538
  const chatId = message.channelId || '';
226
- // Send as card if channelData.feishu.card is provided
539
+ if (!chatId) {
540
+ return { success: false, error: 'Missing channelId (Feishu chat_id)', timestamp: new Date() };
541
+ }
227
542
  const feishuData = message.channelData?.feishu;
543
+ let msgType;
544
+ let content;
228
545
  if (feishuData?.card) {
229
- const result = await this.adapter.sendCard(chatId, feishuData.card);
230
- return { success: result.success, messageId: result.messageId, timestamp: new Date() };
546
+ msgType = 'interactive';
547
+ content = JSON.stringify(feishuData.card);
548
+ }
549
+ else {
550
+ msgType = 'text';
551
+ content = JSON.stringify({ text: message.content });
552
+ }
553
+ try {
554
+ return await this.postMessage(chatId, msgType, content);
555
+ }
556
+ catch (error) {
557
+ return {
558
+ success: false,
559
+ error: error instanceof Error ? error.message : String(error),
560
+ timestamp: new Date(),
561
+ };
231
562
  }
232
- const result = await this.adapter.sendText(chatId, message.content);
233
- return { success: result.success, messageId: result.messageId, timestamp: new Date() };
563
+ }
564
+ /**
565
+ * Honest, structured account of the inbound receive channel. Returns `null`
566
+ * before `connect()` has run; otherwise a {@link FeishuReceiveStatus}
567
+ * explaining why no live socket exists.
568
+ */
569
+ getReceiveStatus() {
570
+ return this.receiveStatus;
234
571
  }
235
572
  /**
236
573
  * Get the underlying adapter (for direct card/reasoning API access).
@@ -238,5 +575,93 @@ export class FeishuChannel extends BaseChannel {
238
575
  getAdapter() {
239
576
  return this.adapter;
240
577
  }
578
+ // ==========================================================================
579
+ // REST outbound (real)
580
+ // ==========================================================================
581
+ /**
582
+ * POST a message to `/open-apis/im/v1/messages`, minting (and caching) a
583
+ * tenant_access_token first. On an auth-class failure the token is dropped
584
+ * and the call retried once with a fresh token.
585
+ */
586
+ async postMessage(chatId, msgType, content, retried = false) {
587
+ const token = await this.ensureTenantToken();
588
+ const url = `${this.baseUrl}/open-apis/im/v1/messages?receive_id_type=chat_id`;
589
+ const response = await fetch(url, {
590
+ method: 'POST',
591
+ headers: {
592
+ Authorization: `Bearer ${token}`,
593
+ 'Content-Type': 'application/json; charset=utf-8',
594
+ },
595
+ body: JSON.stringify({ receive_id: chatId, msg_type: msgType, content }),
596
+ });
597
+ const text = await response.text().catch(() => '');
598
+ let parsed = {};
599
+ try {
600
+ parsed = text ? JSON.parse(text) : {};
601
+ }
602
+ catch {
603
+ parsed = {};
604
+ }
605
+ const code = typeof parsed.code === 'number' ? parsed.code : undefined;
606
+ // Feishu signals an expired/invalid tenant token with a non-zero `code`
607
+ // (99991663/99991664/...) and HTTP 200. Drop the cached token and retry once.
608
+ if (!retried && (code === 99991663 || code === 99991664 || response.status === 401)) {
609
+ this.tenantToken = null;
610
+ return this.postMessage(chatId, msgType, content, true);
611
+ }
612
+ if (!response.ok || (code !== undefined && code !== 0)) {
613
+ const msg = typeof parsed.msg === 'string' ? parsed.msg : text.slice(0, 300);
614
+ return {
615
+ success: false,
616
+ error: `Feishu send failed: status=${response.status} code=${code ?? 'n/a'} msg=${msg}`,
617
+ timestamp: new Date(),
618
+ };
619
+ }
620
+ const data = (parsed.data ?? {});
621
+ const messageId = typeof data.message_id === 'string' ? data.message_id : undefined;
622
+ this.status.lastActivity = new Date();
623
+ const result = { success: true, timestamp: new Date() };
624
+ if (messageId)
625
+ result.messageId = messageId;
626
+ return result;
627
+ }
628
+ /**
629
+ * Return a cached tenant_access_token, minting one via
630
+ * `/open-apis/auth/v3/tenant_access_token/internal` if needed. This is the
631
+ * same internal-app credential exchange used by `src/tools/feishu-tool.ts`.
632
+ */
633
+ async ensureTenantToken() {
634
+ if (this.tenantToken)
635
+ return this.tenantToken;
636
+ const cfg = this.config;
637
+ if (!cfg.appId || !cfg.appSecret) {
638
+ throw new Error('Feishu appId/appSecret are required to mint a tenant_access_token');
639
+ }
640
+ const response = await fetch(`${this.baseUrl}/open-apis/auth/v3/tenant_access_token/internal`, {
641
+ method: 'POST',
642
+ headers: { 'Content-Type': 'application/json; charset=utf-8' },
643
+ body: JSON.stringify({ app_id: cfg.appId, app_secret: cfg.appSecret }),
644
+ });
645
+ const text = await response.text().catch(() => '');
646
+ let parsed = {};
647
+ try {
648
+ parsed = text ? JSON.parse(text) : {};
649
+ }
650
+ catch {
651
+ parsed = {};
652
+ }
653
+ const code = typeof parsed.code === 'number' ? parsed.code : undefined;
654
+ if (!response.ok || (code !== undefined && code !== 0)) {
655
+ const msg = typeof parsed.msg === 'string' ? parsed.msg : text.slice(0, 300);
656
+ throw new Error(`Feishu tenant_access_token request failed: status=${response.status} code=${code ?? 'n/a'} msg=${msg}`);
657
+ }
658
+ const token = typeof parsed.tenant_access_token === 'string' ? parsed.tenant_access_token : '';
659
+ if (!token) {
660
+ throw new Error('Feishu tenant_access_token response missing tenant_access_token');
661
+ }
662
+ this.tenantToken = token;
663
+ this.status.authenticated = true;
664
+ return token;
665
+ }
241
666
  }
242
667
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Gateway Lifecycle Manager
3
+ *
4
+ * Unified lifecycle management (start/stop/restart/status) for the
5
+ * messaging gateway subsystem. Tracks which channel adapters are
6
+ * currently active and provides per-channel readiness information.
7
+ */
8
+ import { EventEmitter } from 'events';
9
+ import type { ChannelType } from './core.js';
10
+ import { ChannelManager } from './core.js';
11
+ /**
12
+ * Per-channel readiness state
13
+ */
14
+ export type ChannelReadiness = 'connected' | 'disconnected' | 'error' | 'not-configured';
15
+ /**
16
+ * Per-channel status entry in the gateway status report
17
+ */
18
+ export interface GatewayChannelStatus {
19
+ /** Channel type identifier */
20
+ channelId: ChannelType;
21
+ /** Current readiness state */
22
+ readiness: ChannelReadiness;
23
+ /** Whether the channel is authenticated */
24
+ authenticated: boolean;
25
+ /** Last activity timestamp (ISO string) */
26
+ lastActivity?: string;
27
+ /** Error message if readiness is 'error' */
28
+ error?: string;
29
+ /** Additional platform-specific info */
30
+ info?: Record<string, unknown>;
31
+ }
32
+ /**
33
+ * Full gateway status report
34
+ */
35
+ export interface GatewayStatus {
36
+ /** Overall gateway health */
37
+ ok: boolean;
38
+ /** Timestamp of this report */
39
+ generatedAt: string;
40
+ /** Total number of registered channels */
41
+ totalChannels: number;
42
+ /** Number of connected channels */
43
+ connectedCount: number;
44
+ /** Number of channels in error state */
45
+ errorCount: number;
46
+ /** Number of disconnected channels */
47
+ disconnectedCount: number;
48
+ /** Per-channel status entries */
49
+ channels: GatewayChannelStatus[];
50
+ }
51
+ /**
52
+ * Events emitted by the GatewayLifecycleManager
53
+ */
54
+ export interface GatewayLifecycleEvents {
55
+ 'channel:started': (channelId: ChannelType) => void;
56
+ 'channel:stopped': (channelId: ChannelType) => void;
57
+ 'channel:error': (channelId: ChannelType, error: Error) => void;
58
+ 'gateway:started': () => void;
59
+ 'gateway:stopped': () => void;
60
+ }
61
+ /**
62
+ * Manages the lifecycle of the messaging gateway and its channel adapters.
63
+ *
64
+ * Wraps the existing ChannelManager with explicit start/stop/restart/status
65
+ * semantics for individual channels and the entire gateway.
66
+ */
67
+ export declare class GatewayLifecycleManager extends EventEmitter {
68
+ private manager;
69
+ private activeChannels;
70
+ private channelErrors;
71
+ constructor(manager?: ChannelManager);
72
+ /**
73
+ * Get the underlying ChannelManager.
74
+ */
75
+ getManager(): ChannelManager;
76
+ /**
77
+ * Start a single channel adapter by its channel type.
78
+ *
79
+ * The channel must already be registered with the ChannelManager.
80
+ * This calls connect() on the adapter and marks it as active.
81
+ *
82
+ * @param channelId - The channel type to start
83
+ * @throws Error if the channel is not registered
84
+ */
85
+ start(channelId: ChannelType): Promise<void>;
86
+ /**
87
+ * Stop a single channel adapter by its channel type.
88
+ *
89
+ * Calls disconnect() on the adapter and removes it from the active set.
90
+ *
91
+ * @param channelId - The channel type to stop
92
+ * @throws Error if the channel is not registered
93
+ */
94
+ stop(channelId: ChannelType): Promise<void>;
95
+ /**
96
+ * Restart a single channel adapter (stop + start).
97
+ *
98
+ * @param channelId - The channel type to restart
99
+ */
100
+ restart(channelId: ChannelType): Promise<void>;
101
+ /**
102
+ * Start all registered channel adapters.
103
+ */
104
+ startAll(): Promise<void>;
105
+ /**
106
+ * Stop all active channel adapters.
107
+ */
108
+ stopAll(): Promise<void>;
109
+ /**
110
+ * Get the full gateway status report.
111
+ *
112
+ * Aggregates per-channel readiness from the ChannelManager's status,
113
+ * the active set, and any recorded errors.
114
+ */
115
+ status(): GatewayStatus;
116
+ /**
117
+ * Check whether a specific channel is currently active.
118
+ */
119
+ isActive(channelId: ChannelType): boolean;
120
+ /**
121
+ * Get the set of currently active channel IDs.
122
+ */
123
+ getActiveChannels(): ChannelType[];
124
+ }
125
+ /**
126
+ * Get the singleton GatewayLifecycleManager instance.
127
+ */
128
+ export declare function getGatewayLifecycle(): GatewayLifecycleManager;
129
+ /**
130
+ * Reset the singleton (for testing).
131
+ */
132
+ export declare function resetGatewayLifecycle(): void;