@runfusion/fusion 0.22.0 → 0.24.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 (206) hide show
  1. package/dist/bin.js +30071 -20735
  2. package/dist/client/assets/AgentDetailView-BwJaLqZh.css +1 -0
  3. package/dist/client/assets/AgentDetailView-gy_5SUj2.js +18 -0
  4. package/dist/client/assets/AgentsView-BkB9FiMT.js +29 -0
  5. package/dist/client/assets/AgentsView-CV3vm7Qk.css +1 -0
  6. package/dist/client/assets/ChatView-B_-B8fqu.js +1 -0
  7. package/dist/client/assets/ChatView-DwJAd5G1.css +1 -0
  8. package/dist/client/assets/{DevServerView-l8RCyL2k.js → DevServerView-BkvtjZBa.js} +1 -1
  9. package/dist/client/assets/{DirectoryPicker-CS1dwqcC.js → DirectoryPicker-BK-KbnhP.js} +1 -1
  10. package/dist/client/assets/{DocumentsView-DmthQWDZ.js → DocumentsView-BEg1CQAk.js} +1 -1
  11. package/dist/client/assets/{DocumentsView-BrhyOdeE.css → DocumentsView-gv4zG3aT.css} +1 -1
  12. package/dist/client/assets/EvalsView-Berf9bQm.js +1 -0
  13. package/dist/client/assets/EvalsView-CUNJ1TLc.css +1 -0
  14. package/dist/client/assets/{agentSkills-DDHJnrkn.css → ExperimentalAgentOnboardingModal-B-APN_lM.css} +1 -1
  15. package/dist/client/assets/ExperimentalAgentOnboardingModal-jcInE50G.js +499 -0
  16. package/dist/client/assets/InsightsView-B0J4mhzV.css +1 -0
  17. package/dist/client/assets/InsightsView-BX5bSF1J.js +11 -0
  18. package/dist/client/assets/{MemoryView-CPwlKnUI.js → MemoryView-CKElJY_3.js} +2 -2
  19. package/dist/client/assets/NodesView-DLUOBLf6.js +14 -0
  20. package/dist/client/assets/NodesView-DT4pXowv.css +1 -0
  21. package/dist/client/assets/{PiExtensionsManager-j8rPXqmB.js → PiExtensionsManager-COlJf0Kx.js} +2 -2
  22. package/dist/client/assets/PluginManager-CfW55BF4.js +1 -0
  23. package/dist/client/assets/PluginManager-DtRQXia5.css +1 -0
  24. package/dist/client/assets/{ResearchView-D9DNJYDq.js → ResearchView-B256Lr8I.js} +1 -1
  25. package/dist/client/assets/SettingsModal-BeA_nQtW.js +31 -0
  26. package/dist/client/assets/SettingsModal-DzsLquBu.css +1 -0
  27. package/dist/client/assets/{SettingsModal-fxvTFLtR.js → SettingsModal-yRqM4DV8.js} +1 -1
  28. package/dist/client/assets/SetupWizardModal-uUZk3TKT.js +1 -0
  29. package/dist/client/assets/{SkillsView-Ddf0YL8z.js → SkillsView-CP8JX0P_.js} +1 -1
  30. package/dist/client/assets/TodoView-Cx9cVhq7.css +1 -0
  31. package/dist/client/assets/TodoView-DCRIkDZ-.js +6 -0
  32. package/dist/client/assets/createLucideIcon-BazL2hk5.js +21 -0
  33. package/dist/client/assets/dashboard-view-BkTMSZYn.css +1 -0
  34. package/dist/client/assets/dashboard-view-CyWN-d02.js +63 -0
  35. package/dist/client/assets/dashboard-view-lR7YYmSC.js +21 -0
  36. package/dist/client/assets/{folder-open-BiJpmnaT.js → folder-open-DHjELt8-.js} +1 -1
  37. package/dist/client/assets/index-CQyVRLOb.js +692 -0
  38. package/dist/client/assets/index-CxA2Nn0_.css +1 -0
  39. package/dist/client/assets/projectDetection-G3XuxD2X.js +1 -0
  40. package/dist/client/assets/{star-BwRZmiuZ.js → star-DYesq1AV.js} +1 -1
  41. package/dist/client/assets/{upload-D4NwZhPp.js → upload-DTWF3Db5.js} +1 -1
  42. package/dist/client/assets/{users-DNISDtI1.js → users--syrel4l.js} +1 -1
  43. package/dist/client/index.html +12 -20
  44. package/dist/client/theme-data.css +106 -0
  45. package/dist/client/version.json +1 -1
  46. package/dist/droid-cli/package.json +1 -1
  47. package/dist/extension.js +17072 -9627
  48. package/dist/pi-claude-cli/package.json +1 -1
  49. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +218 -0
  50. package/dist/plugins/fusion-plugin-cursor-runtime/manifest.json +6 -0
  51. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +11 -0
  52. package/dist/plugins/fusion-plugin-dependency-graph/manifest.json +1 -1
  53. package/dist/plugins/fusion-plugin-dependency-graph/package.json +6 -4
  54. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.css +58 -0
  55. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.tsx +301 -0
  56. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphHighlight.css +27 -0
  57. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.css +157 -0
  58. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx +126 -0
  59. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.css +35 -0
  60. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.tsx +36 -0
  61. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx +112 -0
  62. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.persistence.test.tsx +115 -0
  63. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.test.tsx +128 -0
  64. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.drag.test.tsx +82 -0
  65. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.test.tsx +307 -0
  66. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphToolbar.test.tsx +60 -0
  67. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx +75 -0
  68. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filtering.test.tsx +62 -0
  69. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filters.test.ts +78 -0
  70. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/graphPositionStorage.test.ts +95 -0
  71. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/host-integration.test.ts +74 -0
  72. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/index.test.ts +58 -0
  73. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/interactions.test.tsx +121 -0
  74. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/layout.test.ts +70 -0
  75. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/persistence.test.tsx +89 -0
  76. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphData.test.ts +86 -0
  77. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphInteraction.test.ts +167 -0
  78. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphPositions.test.ts +66 -0
  79. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useNodeDrag.test.ts +81 -0
  80. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-interop.d.ts +35 -0
  81. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx +19 -0
  82. package/dist/plugins/fusion-plugin-dependency-graph/src/edges.tsx +70 -0
  83. package/dist/plugins/fusion-plugin-dependency-graph/src/filters.ts +8 -0
  84. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/__tests__/useDependencyChain.test.ts +53 -0
  85. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useDependencyChain.ts +60 -0
  86. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useGraphPositions.ts +45 -0
  87. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useNodeDrag.ts +114 -0
  88. package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +1 -2
  89. package/dist/plugins/fusion-plugin-dependency-graph/src/layout.ts +91 -0
  90. package/dist/plugins/fusion-plugin-dependency-graph/src/styles/drag.css +15 -0
  91. package/dist/plugins/fusion-plugin-dependency-graph/src/types.ts +21 -0
  92. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphData.ts +17 -0
  93. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphInteraction.ts +292 -0
  94. package/dist/plugins/fusion-plugin-dependency-graph/src/utils/graphPositionStorage.ts +65 -0
  95. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +136680 -0
  96. package/dist/plugins/fusion-plugin-droid-runtime/manifest.json +13 -0
  97. package/dist/plugins/fusion-plugin-droid-runtime/mcp-schema-server.cjs +49 -0
  98. package/dist/plugins/fusion-plugin-droid-runtime/package.json +11 -0
  99. package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +176 -7
  100. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  101. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +93 -6
  102. package/dist/plugins/fusion-plugin-openclaw-runtime/mcp-schema-server.cjs +59 -0
  103. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  104. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  105. package/dist/plugins/fusion-plugin-reports/manifest.json +33 -0
  106. package/dist/plugins/fusion-plugin-reports/package.json +26 -0
  107. package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +51 -0
  108. package/dist/plugins/fusion-plugin-reports/src/__tests__/review-panel.test.ts +166 -0
  109. package/dist/plugins/fusion-plugin-reports/src/__tests__/settings.test.ts +157 -0
  110. package/dist/plugins/fusion-plugin-reports/src/index.ts +41 -0
  111. package/dist/plugins/fusion-plugin-reports/src/review-panel.ts +294 -0
  112. package/dist/plugins/fusion-plugin-reports/src/review-types.ts +75 -0
  113. package/dist/plugins/fusion-plugin-reports/src/settings.ts +105 -0
  114. package/dist/plugins/fusion-plugin-roadmap/manifest.json +16 -0
  115. package/dist/plugins/fusion-plugin-roadmap/package.json +48 -0
  116. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +101 -0
  117. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +92 -0
  118. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +48 -0
  119. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +31 -0
  120. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +1299 -0
  121. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +2559 -0
  122. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +1144 -0
  123. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +1756 -0
  124. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +70 -0
  125. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +7 -0
  126. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +1 -0
  127. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +8 -0
  128. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +1188 -0
  129. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +20 -0
  130. package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +6 -0
  131. package/dist/plugins/fusion-plugin-roadmap/src/index.ts +74 -0
  132. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +1 -0
  133. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +41 -0
  134. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +15 -0
  135. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +15 -0
  136. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +283 -0
  137. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +1 -0
  138. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +21 -0
  139. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +1 -0
  140. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +310 -0
  141. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +5 -0
  142. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +1 -0
  143. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +361 -0
  144. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +1 -0
  145. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +408 -0
  146. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +68 -0
  147. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +1 -0
  148. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +300 -0
  149. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +1 -0
  150. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +381 -0
  151. package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +3 -0
  152. package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +1 -0
  153. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +445 -0
  154. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +334 -0
  155. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +1318 -0
  156. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +163 -0
  157. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +37 -0
  158. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +1 -0
  159. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +188 -0
  160. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +1 -0
  161. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +311 -0
  162. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +299 -0
  163. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +1 -0
  164. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +765 -0
  165. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +1 -0
  166. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +1001 -0
  167. package/dist/plugins/fusion-plugin-whatsapp-chat/manifest.json +8 -0
  168. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +34 -0
  169. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/auth-state.test.ts +99 -0
  170. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/connection.test.ts +145 -0
  171. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.ts +216 -0
  172. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/reply.test.ts +52 -0
  173. package/dist/plugins/fusion-plugin-whatsapp-chat/src/auth-state.ts +89 -0
  174. package/dist/plugins/fusion-plugin-whatsapp-chat/src/connection.ts +253 -0
  175. package/dist/plugins/fusion-plugin-whatsapp-chat/src/index.ts +262 -0
  176. package/dist/plugins/fusion-plugin-whatsapp-chat/src/qrcode.d.ts +1 -0
  177. package/dist/plugins/fusion-plugin-whatsapp-chat/src/reply.ts +37 -0
  178. package/package.json +2 -2
  179. package/skill/fusion/SKILL.md +2 -2
  180. package/skill/fusion/references/engine-tools.md +8 -2
  181. package/skill/fusion/references/extension-tools.md +39 -0
  182. package/skill/fusion/references/fusion-capabilities.md +3 -0
  183. package/dist/client/assets/AgentDetailView-BKKpbp1S.js +0 -18
  184. package/dist/client/assets/AgentDetailView-CeO_1MK7.css +0 -1
  185. package/dist/client/assets/AgentsView-BRXFmrcJ.js +0 -527
  186. package/dist/client/assets/AgentsView-Bs03ptrd.css +0 -1
  187. package/dist/client/assets/ChatView-D7L2e_qu.js +0 -1
  188. package/dist/client/assets/InsightsView-AWo5o_81.css +0 -1
  189. package/dist/client/assets/InsightsView-DvXpMKmH.js +0 -11
  190. package/dist/client/assets/NodesView-BLlfUfsy.js +0 -14
  191. package/dist/client/assets/NodesView-fXqDk9ur.css +0 -1
  192. package/dist/client/assets/PluginManager-DA_T0GHn.css +0 -1
  193. package/dist/client/assets/PluginManager-pW6RMz5z.js +0 -1
  194. package/dist/client/assets/RoadmapsView-Djc_X35v.js +0 -6
  195. package/dist/client/assets/SettingsModal-BWe0KrGY.css +0 -1
  196. package/dist/client/assets/SettingsModal-WGCF_pk8.js +0 -31
  197. package/dist/client/assets/SetupWizardModal-tG_MF_nA.js +0 -1
  198. package/dist/client/assets/agentSkills-EwIwBlG8.js +0 -1
  199. package/dist/client/assets/index-D6ebxTPF.css +0 -1
  200. package/dist/client/assets/index-DYDLmOcK.js +0 -694
  201. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.css +0 -132
  202. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.tsx +0 -428
  203. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraphView.test.tsx +0 -261
  204. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/storage.test.ts +0 -31
  205. package/dist/plugins/fusion-plugin-dependency-graph/src/storage.ts +0 -23
  206. /package/dist/client/assets/{RoadmapsView-DdGlfuu-.css → dashboard-view-DdGlfuu-.css} +0 -0
@@ -0,0 +1,253 @@
1
+ import { DisconnectReason, makeWASocket, type ConnectionState, type WAMessage, type WAMessageContent, type WASocket } from "@whiskeysockets/baileys";
2
+ import type { PluginContext } from "@fusion/plugin-sdk";
3
+ import pino from "pino";
4
+ import qrcode from "qrcode";
5
+ import { clearAuthState, createPluginDbAuthState } from "./auth-state.js";
6
+ import {
7
+ getAllowedSenders,
8
+ getDedupeRetentionDays,
9
+ getHistoryTurnLimit,
10
+ loadHistory,
11
+ markProcessed,
12
+ saveHistory,
13
+ wasProcessed,
14
+ type ChatTurn,
15
+ type PluginDb,
16
+ } from "./index.js";
17
+
18
+ export type ConnectionStatus = {
19
+ state: "starting" | "awaiting-qr" | "awaiting-code" | "connected" | "disconnected" | "error";
20
+ qr?: string;
21
+ qrDataUrl?: string;
22
+ pairingCode?: string;
23
+ lastError?: string;
24
+ jid?: string;
25
+ };
26
+
27
+ export type ReplyGenerator = (ctx: PluginContext, sender: string, text: string, history: ChatTurn[]) => Promise<string>;
28
+
29
+ const BACKOFF_MS = [1000, 2000, 5000, 15000, 30000];
30
+ const FALLBACK_TEXT = "Sorry, I hit an internal error while processing that message.";
31
+ const MAX_WHATSAPP_MESSAGE_CHARS = 4096;
32
+
33
+ function extractText(message?: WAMessageContent | null): string | null {
34
+ const text = message?.conversation ?? message?.extendedTextMessage?.text;
35
+ if (!text || !text.trim()) return null;
36
+ return text.trim();
37
+ }
38
+
39
+ function normalizeSender(jid: string): string {
40
+ return jid.split("@")[0]?.replace(/\D+/g, "") ?? "";
41
+ }
42
+
43
+ function isLoggedOutDisconnect(error: unknown): boolean {
44
+ const statusCode = (error as { output?: { statusCode?: unknown } })?.output?.statusCode;
45
+ return statusCode === DisconnectReason.loggedOut;
46
+ }
47
+
48
+ function splitMessageForWhatsapp(text: string): string[] {
49
+ if (text.length <= MAX_WHATSAPP_MESSAGE_CHARS) return [text];
50
+
51
+ const chunks: string[] = [];
52
+ let remaining = text;
53
+ while (remaining.length > 0) {
54
+ if (remaining.length <= MAX_WHATSAPP_MESSAGE_CHARS) {
55
+ chunks.push(remaining);
56
+ break;
57
+ }
58
+ const candidate = remaining.slice(0, MAX_WHATSAPP_MESSAGE_CHARS);
59
+ const splitAt = Math.max(candidate.lastIndexOf("\n"), candidate.lastIndexOf(" "));
60
+ const breakpoint = splitAt > 0 ? splitAt : MAX_WHATSAPP_MESSAGE_CHARS;
61
+ chunks.push(remaining.slice(0, breakpoint).trim());
62
+ remaining = remaining.slice(breakpoint).trimStart();
63
+ }
64
+
65
+ return chunks.filter(Boolean);
66
+ }
67
+
68
+ export class WhatsAppConnection {
69
+ private sock: WASocket | null = null;
70
+ private status: ConnectionStatus = { state: "disconnected" };
71
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
72
+ private reconnectAttempt = 0;
73
+ private stopped = true;
74
+ private authState: ReturnType<typeof createPluginDbAuthState>;
75
+
76
+ public constructor(
77
+ private readonly ctx: PluginContext,
78
+ private readonly fusionVersion: string,
79
+ private readonly generateReply: ReplyGenerator,
80
+ private readonly db: PluginDb,
81
+ ) {
82
+ this.authState = createPluginDbAuthState(this.db);
83
+ }
84
+
85
+ public async start(): Promise<void> {
86
+ this.stopped = false;
87
+ this.status = { state: "starting" };
88
+ await this.connect();
89
+ }
90
+
91
+ public async stop(): Promise<void> {
92
+ if (this.stopped) return;
93
+ this.stopped = true;
94
+ this.clearReconnectTimer();
95
+
96
+ const socket = this.sock;
97
+ this.sock = null;
98
+ this.status = { state: "disconnected" };
99
+
100
+ if (socket) {
101
+ socket.ev.off("creds.update", this.authState.saveCreds);
102
+ socket.ev.off("connection.update", this.onConnectionUpdate);
103
+ socket.ev.off("messages.upsert", this.onMessagesUpsert);
104
+ await socket.end(undefined);
105
+ }
106
+ }
107
+
108
+ public getStatus(): ConnectionStatus {
109
+ return { ...this.status };
110
+ }
111
+
112
+ public async requestPairingCode(phoneNumberE164: string): Promise<string> {
113
+ if (!this.sock) throw new Error("WhatsApp socket not initialized");
114
+ const pairingCode = await this.sock.requestPairingCode(phoneNumberE164);
115
+ this.status = { ...this.status, state: "awaiting-code", pairingCode };
116
+ return pairingCode;
117
+ }
118
+
119
+ public async logout(): Promise<void> {
120
+ try {
121
+ await this.sock?.logout();
122
+ } finally {
123
+ clearAuthState(this.db);
124
+ this.authState = createPluginDbAuthState(this.db);
125
+ this.status = { state: "disconnected" };
126
+ }
127
+ }
128
+
129
+ private async connect(): Promise<void> {
130
+ if (this.stopped) return;
131
+
132
+ this.status = { state: "starting" };
133
+ const socket = makeWASocket({
134
+ auth: this.authState.state,
135
+ printQRInTerminal: false,
136
+ browser: ["Fusion", "Chrome", this.fusionVersion],
137
+ logger: pino({ level: "silent" }),
138
+ });
139
+
140
+ this.sock = socket;
141
+ socket.ev.on("creds.update", this.authState.saveCreds);
142
+ socket.ev.on("connection.update", this.onConnectionUpdate);
143
+ socket.ev.on("messages.upsert", this.onMessagesUpsert);
144
+ }
145
+
146
+ private readonly onConnectionUpdate = async (update: Partial<ConnectionState>): Promise<void> => {
147
+ if (update.qr) {
148
+ const qrDataUrl = await qrcode.toDataURL(update.qr);
149
+ this.ctx.logger.info("WhatsApp pairing QR updated", update.qr);
150
+ this.status = { state: "awaiting-qr", qr: update.qr, qrDataUrl };
151
+ }
152
+
153
+ if (update.connection === "open") {
154
+ this.reconnectAttempt = 0;
155
+ this.status = { state: "connected", jid: this.sock?.user?.id };
156
+ return;
157
+ }
158
+
159
+ if (update.connection === "close") {
160
+ if (isLoggedOutDisconnect(update.lastDisconnect?.error)) {
161
+ clearAuthState(this.db);
162
+ this.authState = createPluginDbAuthState(this.db);
163
+ this.status = { state: "disconnected", lastError: "loggedOut" };
164
+ return;
165
+ }
166
+
167
+ const closeError = update.lastDisconnect?.error;
168
+ this.status = {
169
+ state: "disconnected",
170
+ lastError: closeError instanceof Error ? closeError.message : "connection closed",
171
+ };
172
+ this.scheduleReconnect();
173
+ }
174
+ };
175
+
176
+ private readonly onMessagesUpsert = async (upsert: { type?: string; messages?: WAMessage[] }): Promise<void> => {
177
+ if (upsert.type !== "notify") return;
178
+
179
+ for (const message of upsert.messages ?? []) {
180
+ const jid = message.key.remoteJid;
181
+ const messageId = message.key.id;
182
+ if (!jid || !messageId) continue;
183
+ if (jid.endsWith("@g.us") || jid.endsWith("@broadcast") || jid === "status@broadcast") continue;
184
+ if (message.key.fromMe) continue;
185
+
186
+ const text = extractText(message.message);
187
+ if (!text) continue;
188
+
189
+ const sender = normalizeSender(jid);
190
+ const allowedSenders = getAllowedSenders(this.ctx.settings);
191
+ if (allowedSenders.size === 0 || (!allowedSenders.has(sender) && !allowedSenders.has(jid))) continue;
192
+ if (wasProcessed(this.db, messageId)) continue;
193
+
194
+ markProcessed(this.db, messageId, sender, getDedupeRetentionDays(this.ctx.settings));
195
+
196
+ try {
197
+ const history = loadHistory(this.db, sender);
198
+ const reply = await this.generateReply(this.ctx, sender, text, history);
199
+ const now = new Date().toISOString();
200
+ const nextHistory: ChatTurn[] = [
201
+ ...history,
202
+ { role: "user" as const, text, createdAt: now },
203
+ { role: "assistant" as const, text: reply, createdAt: now },
204
+ ].slice(-getHistoryTurnLimit(this.ctx.settings));
205
+ saveHistory(this.db, sender, nextHistory);
206
+
207
+ for (const chunk of splitMessageForWhatsapp(reply)) {
208
+ await this.sock?.sendMessage(jid, { text: chunk });
209
+ }
210
+ } catch (error) {
211
+ this.ctx.logger.error("WhatsApp chat processing failed", error);
212
+ try {
213
+ await this.sock?.sendMessage(jid, { text: FALLBACK_TEXT });
214
+ } catch {
215
+ // no-op
216
+ }
217
+ }
218
+ }
219
+ };
220
+
221
+ private scheduleReconnect(): void {
222
+ if (this.stopped || this.reconnectTimer) return;
223
+ const delay = BACKOFF_MS[Math.min(this.reconnectAttempt, BACKOFF_MS.length - 1)] ?? 30000;
224
+ this.reconnectAttempt += 1;
225
+
226
+ this.reconnectTimer = setTimeout(async () => {
227
+ this.reconnectTimer = null;
228
+ await this.connect();
229
+ }, delay);
230
+ }
231
+
232
+ private clearReconnectTimer(): void {
233
+ if (!this.reconnectTimer) return;
234
+ clearTimeout(this.reconnectTimer);
235
+ this.reconnectTimer = null;
236
+ }
237
+
238
+ public static splitMessageForWhatsapp(text: string, max = 4096): string[] {
239
+ const chunks = splitMessageForWhatsapp(text);
240
+ if (max === 4096) return chunks;
241
+ return chunks.flatMap((chunk) => {
242
+ if (chunk.length <= max) return [chunk];
243
+ const split: string[] = [];
244
+ let remaining = chunk;
245
+ while (remaining.length > max) {
246
+ split.push(remaining.slice(0, max));
247
+ remaining = remaining.slice(max);
248
+ }
249
+ if (remaining.length) split.push(remaining);
250
+ return split;
251
+ });
252
+ }
253
+ }
@@ -0,0 +1,262 @@
1
+ import { definePlugin } from "@fusion/plugin-sdk";
2
+ import type { FusionPlugin, PluginContext, PluginRouteDefinition, PluginRouteResponse, PluginSettingSchema } from "@fusion/plugin-sdk";
3
+ import { WhatsAppConnection } from "./connection.js";
4
+ import { generateReply } from "./reply.js";
5
+
6
+ const DEFAULT_HISTORY_TURN_LIMIT = 40;
7
+ const DEFAULT_DEDUPE_RETENTION_DAYS = 7;
8
+
9
+ export type ChatTurn = { role: "user" | "assistant"; text: string; createdAt: string };
10
+
11
+ export type PluginDb = {
12
+ exec(sql: string): void;
13
+ prepare(sql: string): {
14
+ get(...args: unknown[]): unknown;
15
+ run(...args: unknown[]): unknown;
16
+ };
17
+ };
18
+
19
+ const settingsSchema: Record<string, PluginSettingSchema> = {
20
+ pairingMode: {
21
+ type: "enum",
22
+ label: "Pairing Mode",
23
+ enumValues: ["qr", "code"],
24
+ defaultValue: "qr",
25
+ },
26
+ pairingPhoneNumber: {
27
+ type: "string",
28
+ label: "Pairing Phone Number",
29
+ description: "E.164 digits without + (required when pairingMode is code)",
30
+ },
31
+ allowedSenders: { type: "array", label: "Allowed WhatsApp Senders", itemType: "string" },
32
+ agentSystemPrompt: {
33
+ type: "string",
34
+ label: "Agent System Prompt",
35
+ multiline: true,
36
+ defaultValue: "You are a helpful assistant replying in WhatsApp chats.",
37
+ },
38
+ historyTurnLimit: {
39
+ type: "number",
40
+ label: "History Turn Limit",
41
+ defaultValue: DEFAULT_HISTORY_TURN_LIMIT,
42
+ },
43
+ dedupeRetentionDays: {
44
+ type: "number",
45
+ label: "Dedupe Retention (days)",
46
+ description: "How long inbound message IDs are kept for replay protection. Older rows are pruned on each inbound message.",
47
+ defaultValue: DEFAULT_DEDUPE_RETENTION_DAYS,
48
+ },
49
+ };
50
+
51
+ const connections = new Map<string, WhatsAppConnection>();
52
+
53
+ function getConnectionKey(ctx: PluginContext): string {
54
+ return `${ctx.taskStore.getRootDir()}::${ctx.pluginId}`;
55
+ }
56
+
57
+ export function getSettingString(settings: Record<string, unknown>, key: string): string | undefined {
58
+ const value = settings[key];
59
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
60
+ }
61
+
62
+ export function getAllowedSenders(settings: Record<string, unknown>): Set<string> {
63
+ const senders = settings.allowedSenders;
64
+ if (!Array.isArray(senders)) return new Set<string>();
65
+ return new Set(senders.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim()));
66
+ }
67
+
68
+ export function getHistoryTurnLimit(settings: Record<string, unknown>): number {
69
+ const value = settings.historyTurnLimit;
70
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
71
+ return DEFAULT_HISTORY_TURN_LIMIT;
72
+ }
73
+ return Math.floor(value);
74
+ }
75
+
76
+ export function getDedupeRetentionDays(settings: Record<string, unknown>): number {
77
+ const value = settings.dedupeRetentionDays;
78
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
79
+ return DEFAULT_DEDUPE_RETENTION_DAYS;
80
+ }
81
+ return Math.floor(value);
82
+ }
83
+
84
+ export function splitMessageForWhatsapp(text: string): string[] {
85
+ return WhatsAppConnection.splitMessageForWhatsapp(text);
86
+ }
87
+
88
+ export function ensureSchema(db: PluginDb): void {
89
+ db.exec(`
90
+ CREATE TABLE IF NOT EXISTS whatsapp_chat_sessions (
91
+ sender TEXT PRIMARY KEY,
92
+ history TEXT NOT NULL,
93
+ updatedAt TEXT NOT NULL
94
+ );
95
+
96
+ CREATE TABLE IF NOT EXISTS whatsapp_chat_dedupe (
97
+ messageId TEXT PRIMARY KEY,
98
+ sender TEXT NOT NULL,
99
+ receivedAt TEXT NOT NULL
100
+ );
101
+
102
+ CREATE TABLE IF NOT EXISTS whatsapp_auth_creds (
103
+ id TEXT PRIMARY KEY,
104
+ value TEXT NOT NULL,
105
+ updatedAt TEXT NOT NULL
106
+ );
107
+
108
+ CREATE TABLE IF NOT EXISTS whatsapp_auth_keys (
109
+ category TEXT NOT NULL,
110
+ keyId TEXT NOT NULL,
111
+ value TEXT NOT NULL,
112
+ updatedAt TEXT NOT NULL,
113
+ PRIMARY KEY (category, keyId)
114
+ );
115
+ `);
116
+ }
117
+
118
+ export function loadHistory(db: PluginDb, sender: string): ChatTurn[] {
119
+ const row = db.prepare("SELECT history FROM whatsapp_chat_sessions WHERE sender = ?").get(sender) as { history: string } | undefined;
120
+ if (!row) return [];
121
+ try {
122
+ const parsed = JSON.parse(row.history) as ChatTurn[];
123
+ return Array.isArray(parsed) ? parsed : [];
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+
129
+ export function saveHistory(db: PluginDb, sender: string, history: ChatTurn[]): void {
130
+ const now = new Date().toISOString();
131
+ db.prepare(`
132
+ INSERT INTO whatsapp_chat_sessions(sender, history, updatedAt)
133
+ VALUES(?, ?, ?)
134
+ ON CONFLICT(sender) DO UPDATE SET history = excluded.history, updatedAt = excluded.updatedAt
135
+ `).run(sender, JSON.stringify(history), now);
136
+ }
137
+
138
+ export function wasProcessed(db: PluginDb, messageId: string): boolean {
139
+ const row = db.prepare("SELECT 1 as found FROM whatsapp_chat_dedupe WHERE messageId = ?").get(messageId) as { found: number } | undefined;
140
+ return Boolean(row?.found);
141
+ }
142
+
143
+ export function markProcessed(
144
+ db: PluginDb,
145
+ messageId: string,
146
+ sender: string,
147
+ retentionDays: number = DEFAULT_DEDUPE_RETENTION_DAYS,
148
+ ): void {
149
+ const now = new Date().toISOString();
150
+ const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
151
+ db.prepare("DELETE FROM whatsapp_chat_dedupe WHERE receivedAt < ?").run(cutoff);
152
+ db.prepare("INSERT INTO whatsapp_chat_dedupe(messageId, sender, receivedAt) VALUES(?, ?, ?)").run(messageId, sender, now);
153
+ }
154
+
155
+
156
+ function getDbFromTaskStore(ctx: PluginContext): PluginDb {
157
+ const pluginStore = ctx.taskStore.getPluginStore();
158
+ const db = (pluginStore as unknown as { db?: PluginDb }).db;
159
+ if (!db) {
160
+ throw new Error("Plugin database unavailable");
161
+ }
162
+ return db;
163
+ }
164
+
165
+ function getConnectionOrResponse(ctx: PluginContext): { connection?: WhatsAppConnection; error?: PluginRouteResponse } {
166
+ const connection = connections.get(getConnectionKey(ctx));
167
+ if (!connection) {
168
+ return { error: { status: 503, body: { error: "WhatsApp connection is not initialized" } } };
169
+ }
170
+ return { connection };
171
+ }
172
+
173
+ const routes: PluginRouteDefinition[] = [
174
+ {
175
+ method: "GET",
176
+ path: "/status",
177
+ handler: async (_req, ctx) => {
178
+ const { connection, error } = getConnectionOrResponse(ctx);
179
+ if (!connection) return error as PluginRouteResponse;
180
+ const status = connection.getStatus();
181
+ return {
182
+ status: 200,
183
+ body: {
184
+ status: status.state,
185
+ jid: status.jid,
186
+ allowedSenders: Array.from(getAllowedSenders(ctx.settings)),
187
+ },
188
+ };
189
+ },
190
+ },
191
+ {
192
+ method: "GET",
193
+ path: "/qr",
194
+ handler: async (_req, ctx) => {
195
+ const { connection, error } = getConnectionOrResponse(ctx);
196
+ if (!connection) return error as PluginRouteResponse;
197
+ const status = connection.getStatus();
198
+ if (status.state !== "awaiting-qr" || !status.qrDataUrl || !status.qr) {
199
+ return { status: 409, body: { error: "QR is not currently available" } };
200
+ }
201
+ return { status: 200, body: { qrDataUrl: status.qrDataUrl, qr: status.qr } };
202
+ },
203
+ },
204
+ {
205
+ method: "POST",
206
+ path: "/pair-code",
207
+ handler: async (req, ctx) => {
208
+ const { connection, error } = getConnectionOrResponse(ctx);
209
+ if (!connection) return error as PluginRouteResponse;
210
+ const body = (req as { body?: { phoneNumber?: unknown } })?.body;
211
+ const phoneNumber = typeof body?.phoneNumber === "string" ? body.phoneNumber.trim() : "";
212
+ if (!phoneNumber) {
213
+ return { status: 400, body: { error: "phoneNumber is required" } };
214
+ }
215
+ const pairingCode = await connection.requestPairingCode(phoneNumber);
216
+ return { status: 200, body: { pairingCode } };
217
+ },
218
+ },
219
+ {
220
+ method: "POST",
221
+ path: "/logout",
222
+ handler: async (_req, ctx) => {
223
+ const { connection, error } = getConnectionOrResponse(ctx);
224
+ if (!connection) return error as PluginRouteResponse;
225
+ await connection.logout();
226
+ return { status: 200, body: { ok: true } };
227
+ },
228
+ },
229
+ ];
230
+
231
+ const plugin: FusionPlugin = definePlugin({
232
+ manifest: {
233
+ id: "fusion-plugin-whatsapp-chat",
234
+ name: "WhatsApp Chat",
235
+ version: "0.1.0",
236
+ description: "WhatsApp Web (multi-device) bridge that pairs via QR/code and forwards messages to Fusion AI",
237
+ author: "Fusion Team",
238
+ settingsSchema,
239
+ },
240
+ state: "installed",
241
+ routes,
242
+ hooks: {
243
+ onSchemaInit: (db) => {
244
+ ensureSchema(db as PluginDb);
245
+ },
246
+ onLoad: async (ctx) => {
247
+ const db = getDbFromTaskStore(ctx);
248
+ const connection = new WhatsAppConnection(ctx, plugin.manifest.version, generateReply, db);
249
+ connections.set(getConnectionKey(ctx), connection);
250
+ await connection.start();
251
+ },
252
+ onUnload: async (ctx) => {
253
+ const connectionKey = getConnectionKey(ctx);
254
+ const connection = connections.get(connectionKey);
255
+ if (!connection) return;
256
+ await connection.stop();
257
+ connections.delete(connectionKey);
258
+ },
259
+ },
260
+ });
261
+
262
+ export default plugin;
@@ -0,0 +1 @@
1
+ declare module "qrcode";
@@ -0,0 +1,37 @@
1
+ import type { PluginContext } from "@fusion/plugin-sdk";
2
+ import { getSettingString, type ChatTurn } from "./index.js";
3
+
4
+ export async function generateReply(ctx: PluginContext, _sender: string, text: string, history: ChatTurn[]): Promise<string> {
5
+ if (!ctx.createAiSession) {
6
+ throw new Error("AI session factory unavailable: engine not registered");
7
+ }
8
+
9
+ const systemPrompt = getSettingString(ctx.settings, "agentSystemPrompt") ?? "You are a helpful assistant replying in WhatsApp chats.";
10
+ const sessionResult = await ctx.createAiSession({
11
+ cwd: ctx.taskStore.getRootDir(),
12
+ systemPrompt,
13
+ tools: "readonly",
14
+ });
15
+
16
+ const promptLines = [
17
+ "Continue this WhatsApp conversation.",
18
+ ...history.map((turn) => `${turn.role === "user" ? "User" : "Assistant"}: ${turn.text}`),
19
+ `User: ${text}`,
20
+ "Assistant:",
21
+ ];
22
+
23
+ await sessionResult.session.prompt(promptLines.join("\n"));
24
+ const assistantMessages = sessionResult.session.state.messages.filter((message) => message.role === "assistant");
25
+ const latest = assistantMessages[assistantMessages.length - 1];
26
+ const content = latest?.content;
27
+
28
+ if (typeof content === "string" && content.trim()) return content.trim();
29
+ if (Array.isArray(content)) {
30
+ const textParts = content
31
+ .map((part) => (part && typeof part === "object" && "text" in part && typeof (part as { text?: unknown }).text === "string") ? (part as { text: string }).text : "")
32
+ .filter(Boolean);
33
+ if (textParts.length > 0) return textParts.join("\n").trim();
34
+ }
35
+
36
+ throw new Error("AI session returned no assistant text");
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runfusion/fusion",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "license": "MIT",
5
5
  "description": "Fusion CLI: HTTP API server, daemon, dashboard launcher, and task tooling for the Fusion AI coding agent.",
6
6
  "homepage": "https://github.com/Runfusion/Fusion#readme",
@@ -96,7 +96,7 @@
96
96
  "typecheck": "tsc --noEmit",
97
97
  "test": "vitest run --silent=passed-only --reporter=dot",
98
98
  "test:slow-cli": "cross-env FUSION_TEST_SLOW_CLI=1 vitest run src/commands/__tests__/agent-export.test.ts --silent=passed-only --reporter=dot",
99
- "test:extension-integration": "cross-env FUSION_TEST_EXTENSION_INTEGRATION=1 vitest run src/__tests__/extension.test.ts --silent=passed-only --reporter=dot",
99
+ "test:extension-integration": "cross-env FUSION_TEST_EXTENSION_INTEGRATION=1 vitest run src/__tests__/extension-integration.test.ts --silent=passed-only --reporter=dot",
100
100
  "test:build-exe": "cross-env FUSION_TEST_BUILD_EXE=1 vitest run --config vitest.build-exe.config.ts --silent=passed-only --reporter=dot",
101
101
  "test:pre-release": "pnpm test:slow-cli && pnpm test:build-exe"
102
102
  }
@@ -30,10 +30,10 @@ Mission → Milestone → Slice → Feature → Task
30
30
  - **Task tools** — `fn_task_create`, `fn_task_update`, `fn_task_list`, `fn_task_show`, `fn_task_attach`, `fn_task_pause`, `fn_task_unpause`, `fn_task_retry`, `fn_task_duplicate`, `fn_task_refine`, `fn_task_archive`, `fn_task_unarchive`, `fn_task_delete`, `fn_task_plan`
31
31
  - **GitHub tools** — `fn_task_import_github`, `fn_task_import_github_issue`, `fn_task_browse_github_issues`
32
32
  - **Mission tools** — `fn_mission_create`, `fn_mission_list`, `fn_mission_show`, `fn_mission_delete`, `fn_milestone_add`, `fn_slice_add`, `fn_feature_add`, `fn_slice_activate`, `fn_feature_link_task`
33
- - **Agent tools** — `fn_agent_stop`, `fn_agent_start`, `fn_list_agents`, `fn_delegate_task`, `fn_agent_show`, `fn_agent_org_chart`
33
+ - **Agent tools** — `fn_agent_stop`, `fn_agent_start`, `fn_agent_create`, `fn_agent_delete`, `fn_list_agents`, `fn_delegate_task`, `fn_agent_show`, `fn_agent_org_chart`
34
34
  - **Skills tools** — `fn_skills_search`, `fn_skills_install`
35
35
  - **Insight tools** — `fn_insight_list`, `fn_insight_show`, `fn_insight_run_list`, `fn_insight_run_show`
36
- - **Other tools** — `fn_research_run`, `fn_research_list`, `fn_research_get`, `fn_research_cancel`, `fn_research_retry`
36
+ - **Other tools** — `fn_web_fetch`, `fn_research_run`, `fn_research_list`, `fn_research_get`, `fn_research_cancel`, `fn_research_retry`
37
37
  <!-- END: tool-categories -->
38
38
  - **Dashboard** — Use `/fn` command to start/stop the dashboard
39
39
 
@@ -4,6 +4,7 @@ These tools are **not** part of the user-invokable extension surface. They are i
4
4
 
5
5
  - Source files: `packages/engine/src/agent-tools.ts`, `triage.ts`, `executor.ts`, `merger.ts`, `agent-heartbeat.ts`
6
6
  - Availability: only when the engine creates a session for the matching agent role
7
+ - Runtime contract: engine sessions now forward requested skill names (`skillSelection.requestedSkillNames`) into the generic runtime `skills` field so non-pi runtimes can still receive Fusion skill intent.
7
8
  - Important: do not tell users to call these directly from the generic extension tool list
8
9
 
9
10
  ## Shared runtime tools (`agent-tools.ts`)
@@ -17,6 +18,7 @@ These tools are **not** part of the user-invokable extension surface. They are i
17
18
  | `fn_memory_search` | triage, executor, heartbeat | Search project memory plus per-agent layered memory snippets | `query` (string), `limit?` (number) |
18
19
  | `fn_memory_get` | triage, executor, heartbeat | Read a bounded memory file window (including bounded per-agent layered paths) | `path` (string), `startLine?` (number), `lineCount?` (number) |
19
20
  | `fn_memory_append` | executor, heartbeat (when writable backend enabled) | Append memory notes with explicit scope: `scope="agent"` for private operating context, `scope="project"` for workspace-wide durable knowledge | `scope?` (`project` \| `agent`), `layer` (`long-term` \| `daily`), `content` (string) |
21
+ | `fn_web_fetch` | executor, step-session, reviewer, merger, triage, heartbeat | Lightweight HTTP fetch with HTML→text extraction, timeout/size caps, and SSRF guard (no JS rendering) | `url` (string), `prompt?` (string), `timeoutMs?` (number), `maxBytes?` (number) |
20
22
  | `fn_research_run` | triage, executor | Start a bounded research run (optionally wait for completion) and return structured findings metadata | `query` (string), `wait_for_completion?` (boolean), `max_wait_ms?` (number) |
21
23
  | `fn_research_list` | triage, executor | List recent research runs with status/summary metadata | `status?` (`pending` \| `running` \| `completed` \| `failed` \| `cancelled`), `limit?` (number) |
22
24
  | `fn_research_get` | triage, executor | Read one research run's structured findings/citations payload | `id` (string) |
@@ -28,8 +30,10 @@ These tools are **not** part of the user-invokable extension surface. They are i
28
30
  | `fn_delegate_task` | triage, executor, heartbeat | Create and assign a new task to a specific agent | `agent_id` (string), `description` (string), `dependencies?` (string[]) |
29
31
  | `fn_get_agent_config` | executor, heartbeat | Read full config for a direct-report agent | `agent_id` (string) |
30
32
  | `fn_update_agent_config` | executor, heartbeat | Update config fields for a direct-report, non-ephemeral agent | `agent_id` (string), optional: `soul`, `instructions_text`, `instructions_path`, `heartbeat_procedure_path`, `heartbeat_interval_ms`, `heartbeat_timeout_ms`, `max_concurrent_runs`, `message_response_mode` |
31
- | `fn_send_message` | executor, heartbeat | Send inbox messages to agents/users | `to_id` (string), `content` (string), `type?` (`agent-to-agent` \| `agent-to-user`), `reply_to_message_id?` (string) |
32
- | `fn_read_messages` | executor, heartbeat | Read inbox messages | `unread_only?` (boolean), `limit?` (number) |
33
+ | `fn_agent_create` | executor, heartbeat | Create a non-ephemeral direct-report agent | `name` (string), `role` (string), optional: `soul`, `instructions_text`, `instructions_path`, `reportsTo`, `heartbeat_interval_ms`, `heartbeat_timeout_ms`, `max_concurrent_runs`, `message_response_mode` |
34
+ | `fn_agent_delete` | executor, heartbeat | Delete a non-ephemeral direct-report agent | `agent_id` (string), optional: `force` (boolean), `reassign_to` (string) |
35
+ | `fn_send_message` | executor, step-session, heartbeat | Send inbox messages to agents/users | `to_id` (string), `content` (string), `type?` (`agent-to-agent` \| `agent-to-user`), `reply_to_message_id?` (string) |
36
+ | `fn_read_messages` | executor, step-session, heartbeat | Read inbox messages | `unread_only?` (boolean), `limit?` (number) |
33
37
 
34
38
  ## Triage-only runtime tools (`triage.ts`)
35
39
 
@@ -41,6 +45,8 @@ These tools are **not** part of the user-invokable extension surface. They are i
41
45
 
42
46
  ## Executor-only runtime tools (`executor.ts`)
43
47
 
48
+ Note: step-session execution (`step-session-executor.ts`) reuses executor coordination tools (`fn_send_message`, `fn_read_messages`, `fn_list_agents`, `fn_delegate_task`, task-document tools, and memory tools) so spawned/session-sliced execution keeps parity with main executor runs.
49
+
44
50
  | Tool | Purpose | Parameters |
45
51
  |---|---|---|
46
52
  | `fn_task_update` | Update a spec step status (`pending`/`in-progress`/`done`/`skipped`) | `step` (number), `status` (enum) |
@@ -263,6 +263,33 @@ Start a stopped agent — resumes its execution. Transitions the agent from paus
263
263
  |-----------|------|----------|-------------|
264
264
  | `id` | string | ✓ | Agent ID to start (e.g., agent-abc123) |
265
265
 
266
+ ### fn_agent_create
267
+
268
+ Create a new non-ephemeral agent.
269
+
270
+ | Parameter | Type | Required | Description |
271
+ |-----------|------|----------|-------------|
272
+ | `name` | string | ✓ | Agent name |
273
+ | `role` | string | ✓ | Agent role/capability |
274
+ | `soul` | string | — | Agent personality/identity text |
275
+ | `instructions_text` | string | — | Inline custom instructions |
276
+ | `instructions_path` | string | — | Path to instructions markdown |
277
+ | `reportsTo` | string | — | Manager agent ID |
278
+ | `heartbeat_interval_ms` | number | — | |
279
+ | `heartbeat_timeout_ms` | number | — | |
280
+ | `max_concurrent_runs` | number | — | |
281
+ | `message_response_mode` | union | — | |
282
+
283
+ ### fn_agent_delete
284
+
285
+ Delete a non-ephemeral agent.
286
+
287
+ | Parameter | Type | Required | Description |
288
+ |-----------|------|----------|-------------|
289
+ | `id` | string | ✓ | Agent ID to delete |
290
+ | `force` | boolean | — | Force delete when holding checkout |
291
+ | `reassign_to` | string | — | Optional replacement agent for assigned tasks |
292
+
266
293
  ### fn_list_agents
267
294
 
268
295
  List all available agents in the system. Shows each agent's name, role, state, personality (soul), and current assignment. Use this to discover which agents exist and what they specialize in before delegating work.
@@ -282,6 +309,7 @@ Create a new task and assign it to a specific agent for execution. The task goes
282
309
  | `agent_id` | string | ✓ | The agent ID to delegate work to |
283
310
  | `description` | string | ✓ | What needs to be done |
284
311
  | `dependencies` | array | — | Task IDs this new task depends on (e.g. [\"KB-001\"] |
312
+ | `override` | boolean | — | Set true to bypass executor-role assignment policy |
285
313
 
286
314
  ### fn_agent_show
287
315
 
@@ -363,6 +391,17 @@ Show a single insight-generation run by ID.
363
391
 
364
392
  ## Other Tools
365
393
 
394
+ ### fn_web_fetch
395
+
396
+ Lightweight URL fetch (no JS rendering). Use agent-browser skill for JS-heavy pages. URL to fetch (http/https) Optional extraction hint for downstream summarization Timeout in milliseconds (default: 30000) Max bytes to return (default: 512000)
397
+
398
+ | Parameter | Type | Required | Description |
399
+ |-----------|------|----------|-------------|
400
+ | `url` | string | ✓ | URL to fetch (http/https) |
401
+ | `prompt` | string | — | Optional extraction hint for downstream summarization |
402
+ | `timeoutMs` | number | — | Timeout in milliseconds (default: 30000) |
403
+ | `maxBytes` | number | — | Max bytes to return (default: 512000) |
404
+
366
405
  ### fn_research_run
367
406
 
368
407
  Start a bounded research run and optionally wait for findings.