aibroker 0.2.6 → 0.6.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 (170) hide show
  1. package/README.md +164 -4
  2. package/dist/adapters/iterm/core.d.ts +2 -0
  3. package/dist/adapters/iterm/core.d.ts.map +1 -1
  4. package/dist/adapters/iterm/core.js +13 -5
  5. package/dist/adapters/iterm/core.js.map +1 -1
  6. package/dist/adapters/iterm/iterm2-api.d.ts +20 -0
  7. package/dist/adapters/iterm/iterm2-api.d.ts.map +1 -0
  8. package/dist/adapters/iterm/iterm2-api.js +244 -0
  9. package/dist/adapters/iterm/iterm2-api.js.map +1 -0
  10. package/dist/adapters/iterm/sessions.d.ts.map +1 -1
  11. package/dist/adapters/iterm/sessions.js +3 -2
  12. package/dist/adapters/iterm/sessions.js.map +1 -1
  13. package/dist/adapters/kokoro/media.d.ts +2 -1
  14. package/dist/adapters/kokoro/media.d.ts.map +1 -1
  15. package/dist/adapters/kokoro/media.js +53 -5
  16. package/dist/adapters/kokoro/media.js.map +1 -1
  17. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).d.ts +49 -0
  18. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).d.ts.map +1 -0
  19. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).js +632 -0
  20. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).js.map +1 -0
  21. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-59).js +632 -0
  22. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).d.ts +49 -0
  23. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
  24. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).js +614 -0
  25. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
  26. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-46).js +614 -0
  27. package/dist/adapters/pailot/gateway.d.ts +48 -0
  28. package/dist/adapters/pailot/gateway.d.ts (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
  29. package/dist/adapters/pailot/gateway.d.ts (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
  30. package/dist/adapters/pailot/gateway.d.ts.map +1 -0
  31. package/dist/adapters/pailot/gateway.js +828 -0
  32. package/dist/adapters/pailot/gateway.js (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
  33. package/dist/adapters/pailot/gateway.js (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
  34. package/dist/adapters/pailot/gateway.js.map +1 -0
  35. package/dist/backend/api.d.ts +5 -1
  36. package/dist/backend/api.d.ts.map +1 -1
  37. package/dist/backend/api.js +74 -3
  38. package/dist/backend/api.js.map +1 -1
  39. package/dist/core/hybrid.d.ts +7 -0
  40. package/dist/core/hybrid.d.ts.map +1 -1
  41. package/dist/core/hybrid.js +33 -0
  42. package/dist/core/hybrid.js.map +1 -1
  43. package/dist/core/state.d.ts +3 -0
  44. package/dist/core/state.d.ts.map +1 -1
  45. package/dist/core/state.js +4 -0
  46. package/dist/core/state.js.map +1 -1
  47. package/dist/core/status-cache.d.ts +51 -0
  48. package/dist/core/status-cache.d.ts.map +1 -0
  49. package/dist/core/status-cache.js +62 -0
  50. package/dist/core/status-cache.js.map +1 -0
  51. package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).d.ts +63 -0
  52. package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
  53. package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).js +229 -0
  54. package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
  55. package/dist/daemon/adapter-registry.d.ts +63 -0
  56. package/dist/daemon/adapter-registry.d.ts.map +1 -0
  57. package/dist/daemon/adapter-registry.js +240 -0
  58. package/dist/daemon/adapter-registry.js.map +1 -0
  59. package/dist/daemon/cli.d.ts +14 -0
  60. package/dist/daemon/cli.d.ts.map +1 -0
  61. package/dist/daemon/cli.js +150 -0
  62. package/dist/daemon/cli.js.map +1 -0
  63. package/dist/daemon/command-context.d.ts +24 -0
  64. package/dist/daemon/command-context.d.ts.map +1 -0
  65. package/dist/daemon/command-context.js +13 -0
  66. package/dist/daemon/command-context.js.map +1 -0
  67. package/dist/daemon/commands.d.ts +22 -0
  68. package/dist/daemon/commands.d.ts.map +1 -0
  69. package/dist/daemon/commands.js +632 -0
  70. package/dist/daemon/commands.js.map +1 -0
  71. package/dist/daemon/core-handlers.d.ts +24 -0
  72. package/dist/daemon/core-handlers.d.ts.map +1 -0
  73. package/dist/daemon/core-handlers.js +640 -0
  74. package/dist/daemon/core-handlers.js.map +1 -0
  75. package/dist/daemon/create-adapter.d.ts +22 -0
  76. package/dist/daemon/create-adapter.d.ts.map +1 -0
  77. package/dist/daemon/create-adapter.js +153 -0
  78. package/dist/daemon/create-adapter.js.map +1 -0
  79. package/dist/daemon/image-gen.d.ts +28 -0
  80. package/dist/daemon/image-gen.d.ts.map +1 -0
  81. package/dist/daemon/image-gen.js +97 -0
  82. package/dist/daemon/image-gen.js.map +1 -0
  83. package/dist/daemon/index.d.ts +12 -0
  84. package/dist/daemon/index.d.ts.map +1 -0
  85. package/dist/daemon/index.js +184 -0
  86. package/dist/daemon/index.js.map +1 -0
  87. package/dist/daemon/pai-projects.d.ts +68 -0
  88. package/dist/daemon/pai-projects.d.ts.map +1 -0
  89. package/dist/daemon/pai-projects.js +174 -0
  90. package/dist/daemon/pai-projects.js.map +1 -0
  91. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).d.ts +12 -0
  92. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).d.ts.map +1 -0
  93. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).js +252 -0
  94. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).js.map +1 -0
  95. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-59).js +252 -0
  96. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).d.ts +12 -0
  97. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
  98. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).js +240 -0
  99. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
  100. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-46).js +240 -0
  101. package/dist/daemon/screenshot.d.ts +12 -0
  102. package/dist/daemon/screenshot.d.ts (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
  103. package/dist/daemon/screenshot.d.ts (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
  104. package/dist/daemon/screenshot.d.ts.map +1 -0
  105. package/dist/daemon/screenshot.js +252 -0
  106. package/dist/daemon/screenshot.js (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
  107. package/dist/daemon/screenshot.js (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
  108. package/dist/daemon/screenshot.js.map +1 -0
  109. package/dist/daemon/session-content.d.ts +27 -0
  110. package/dist/daemon/session-content.d.ts.map +1 -0
  111. package/dist/daemon/session-content.js +76 -0
  112. package/dist/daemon/session-content.js.map +1 -0
  113. package/dist/daemon/vision.d.ts +46 -0
  114. package/dist/daemon/vision.d.ts.map +1 -0
  115. package/dist/daemon/vision.js +176 -0
  116. package/dist/daemon/vision.js.map +1 -0
  117. package/dist/index.d.ts +16 -2
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +12 -1
  120. package/dist/index.js.map +1 -1
  121. package/dist/ipc/client.d.ts +4 -1
  122. package/dist/ipc/client.d.ts.map +1 -1
  123. package/dist/ipc/client.js +10 -1
  124. package/dist/ipc/client.js.map +1 -1
  125. package/dist/ipc/validate.d.ts +52 -0
  126. package/dist/ipc/validate.d.ts.map +1 -0
  127. package/dist/ipc/validate.js +129 -0
  128. package/dist/ipc/validate.js.map +1 -0
  129. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).d.ts +23 -0
  130. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).d.ts.map +1 -0
  131. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).js +595 -0
  132. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).js.map +1 -0
  133. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-59).js +595 -0
  134. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).d.ts +23 -0
  135. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
  136. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).js +592 -0
  137. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
  138. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-46).js +592 -0
  139. package/dist/mcp/index.d.ts +23 -0
  140. package/dist/mcp/index.d.ts.map +1 -0
  141. package/dist/mcp/index.js +660 -0
  142. package/dist/mcp/index.js (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
  143. package/dist/mcp/index.js (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
  144. package/dist/mcp/index.js.map +1 -0
  145. package/dist/types/adapter.d.ts +41 -0
  146. package/dist/types/adapter.d.ts.map +1 -0
  147. package/dist/types/adapter.js +2 -0
  148. package/dist/types/adapter.js.map +1 -0
  149. package/dist/types/backend.d.ts +29 -1
  150. package/dist/types/backend.d.ts.map +1 -1
  151. package/dist/types/broker.d.ts +47 -0
  152. package/dist/types/broker.d.ts.map +1 -0
  153. package/dist/types/broker.js +21 -0
  154. package/dist/types/broker.js.map +1 -0
  155. package/dist/types/index.d.ts +2 -0
  156. package/dist/types/index.d.ts.map +1 -1
  157. package/dist/types/index.js +2 -0
  158. package/dist/types/index.js.map +1 -1
  159. package/package.json +12 -2
  160. package/templates/adapter/ONBOARDING_PROMPT.md +309 -0
  161. package/templates/adapter/README.md.tmpl +81 -0
  162. package/templates/adapter/package.json.tmpl +23 -0
  163. package/templates/adapter/src/watcher/cli.ts.tmpl +12 -0
  164. package/templates/adapter/src/watcher/commands.ts.tmpl +44 -0
  165. package/templates/adapter/src/watcher/connection.ts.tmpl +59 -0
  166. package/templates/adapter/src/watcher/index.ts.tmpl +201 -0
  167. package/templates/adapter/src/watcher/ipc-server.ts.tmpl +250 -0
  168. package/templates/adapter/src/watcher/send.ts.tmpl +62 -0
  169. package/templates/adapter/src/watcher/state.ts.tmpl +39 -0
  170. package/templates/adapter/tsconfig.json.tmpl +14 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * watcher/index.ts — {{DISPLAY_NAME}} adapter entry point
3
+ *
4
+ * Thin transport adapter: connects to {{DISPLAY_NAME}}, forwards all messages
5
+ * to the AIBroker hub daemon for processing, and delivers hub responses back
6
+ * via the upstream service.
7
+ *
8
+ * Requires the AIBroker daemon to be running. Does not function standalone.
9
+ */
10
+
11
+ import { unlinkSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ import {
16
+ WatcherClient,
17
+ DAEMON_SOCKET_PATH,
18
+ setAppDir,
19
+ log,
20
+ setLogPrefix,
21
+ createBrokerMessage,
22
+ } from "aibroker";
23
+
24
+ import { connectWatcher } from "./connection.js";
25
+ import { startIpcServer } from "./ipc-server.js";
26
+ import { createMessageHandler } from "./commands.js";
27
+ import {
28
+ ADAPTER_SOCKET_PATH,
29
+ adapterStats,
30
+ setConnectionStatus,
31
+ } from "./state.js";
32
+
33
+ // ── Hub connection ──────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Connect to the AIBroker hub daemon. Retries up to 3 times with 2s delay.
37
+ * Throws if the hub is not reachable — adapter cannot function without it.
38
+ */
39
+ async function connectToHub(): Promise<WatcherClient> {
40
+ const client = new WatcherClient(DAEMON_SOCKET_PATH);
41
+ const MAX_RETRIES = 3;
42
+ const RETRY_DELAY = 2000;
43
+
44
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
45
+ try {
46
+ const result = await Promise.race([
47
+ client.call_raw("ping", {}),
48
+ new Promise<null>((_, reject) =>
49
+ setTimeout(() => reject(new Error("timeout")), 2000),
50
+ ),
51
+ ]);
52
+ if (result !== null) return client;
53
+ } catch {
54
+ if (attempt < MAX_RETRIES) {
55
+ log(`Hub not reachable (attempt ${attempt}/${MAX_RETRIES}), retrying in ${RETRY_DELAY}ms...`);
56
+ await new Promise((r) => setTimeout(r, RETRY_DELAY));
57
+ }
58
+ }
59
+ }
60
+
61
+ throw new Error(
62
+ `AIBroker daemon not reachable at ${DAEMON_SOCKET_PATH}. ` +
63
+ `Start it with: aibroker start`
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Slash commands handled locally (require direct upstream service access).
69
+ * Everything else goes to the hub.
70
+ */
71
+ const LOCAL_SLASH_COMMANDS = new Set(["/restart", "/login"]);
72
+
73
+ function isLocalSlashCommand(text: string): boolean {
74
+ return LOCAL_SLASH_COMMANDS.has(text.trim());
75
+ }
76
+
77
+ // ── Main ──────────────────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Start the {{DISPLAY_NAME}} adapter watcher — thin transport adapter.
81
+ *
82
+ * All command handling, session management, TTS, and screenshots are owned
83
+ * by the AIBroker hub daemon. This process only:
84
+ * 1. Connects to the upstream service
85
+ * 2. Forwards incoming messages to the hub
86
+ * 3. Delivers hub-originated messages via the upstream service (through IPC "deliver")
87
+ * 4. Handles /restart and /login locally
88
+ */
89
+ export async function watch(): Promise<void> {
90
+ setLogPrefix("{{ADAPTER_NAME}}-watch");
91
+ setAppDir(join(homedir(), ".{{ADAPTER_NAME}}"));
92
+
93
+ // Connect to AIBroker hub (required — no standalone mode)
94
+ let hubClient: WatcherClient;
95
+ try {
96
+ hubClient = await connectToHub();
97
+ } catch (err) {
98
+ console.error(`[{{ADAPTER_NAME}}-watch] FATAL: ${err instanceof Error ? err.message : String(err)}`);
99
+ console.error("[{{ADAPTER_NAME}}-watch] The AIBroker daemon must be running. Exiting.");
100
+ process.exit(1);
101
+ }
102
+
103
+ console.log(`{{DISPLAY_NAME}} Adapter Watch`);
104
+ console.log(` Socket: ${ADAPTER_SOCKET_PATH}`);
105
+ console.log(` Hub: ${DAEMON_SOCKET_PATH}`);
106
+ console.log();
107
+
108
+ // Graceful shutdown
109
+ let cleanupConnection: (() => void) | null = null;
110
+
111
+ const shutdown = (signal: string) => {
112
+ console.log(`\n[{{ADAPTER_NAME}}-watch] ${signal} received. Stopping.`);
113
+ clearInterval(heartbeatTimer);
114
+ setConnectionStatus("disconnected");
115
+ if (cleanupConnection) cleanupConnection();
116
+ hubClient.call_raw("unregister_adapter", { name: "{{ADAPTER_NAME}}" }).catch(() => {});
117
+ try { unlinkSync(ADAPTER_SOCKET_PATH); } catch { /* ignore */ }
118
+ process.exit(0);
119
+ };
120
+ process.on("SIGINT", () => shutdown("SIGINT"));
121
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
122
+
123
+ // Local handler for /restart and /login only
124
+ const handleMessage = createMessageHandler();
125
+
126
+ // Connect to the upstream service
127
+ console.log(`Connecting to {{DISPLAY_NAME}}...\n`);
128
+ setConnectionStatus("connecting");
129
+
130
+ const { cleanup, triggerLogin } = await connectWatcher(
131
+ (text: string, timestamp: number) => {
132
+ log(`[{{ADAPTER_NAME}}] incoming: ${text.slice(0, 80)}`);
133
+ adapterStats.messagesReceived++;
134
+ adapterStats.lastMessageAt = timestamp;
135
+
136
+ // Local commands stay in the adapter
137
+ if (isLocalSlashCommand(text)) {
138
+ handleMessage(text, timestamp);
139
+ return;
140
+ }
141
+
142
+ // Everything else → hub
143
+ const message = createBrokerMessage(
144
+ "{{ADAPTER_NAME}}",
145
+ text.trim().startsWith("/") ? "command" : "text",
146
+ { text },
147
+ );
148
+ message.timestamp = timestamp;
149
+
150
+ hubClient.call_raw("route_message", {
151
+ message: message as unknown as Record<string, unknown>,
152
+ }).catch((err) => {
153
+ log(`Hub route_message failed: ${err instanceof Error ? err.message : String(err)}`);
154
+ });
155
+ },
156
+ );
157
+
158
+ cleanupConnection = cleanup;
159
+ setConnectionStatus("connected");
160
+
161
+ // Start the adapter IPC server
162
+ startIpcServer(triggerLogin);
163
+
164
+ // Register with hub
165
+ hubClient.call_raw("register_adapter", {
166
+ name: "{{ADAPTER_NAME}}",
167
+ socketPath: ADAPTER_SOCKET_PATH,
168
+ }).then(() => {
169
+ log("Registered with AIBroker hub daemon");
170
+ }).catch((err) => {
171
+ log(`Hub registration failed: ${err instanceof Error ? err.message : String(err)}`);
172
+ });
173
+
174
+ // Hub heartbeat — re-register if the daemon restarts
175
+ const HUB_HEARTBEAT_INTERVAL = 30_000; // 30 seconds
176
+ const heartbeatTimer = setInterval(async () => {
177
+ try {
178
+ const result = await Promise.race([
179
+ hubClient.call_raw("ping", {}),
180
+ new Promise<null>((_, reject) =>
181
+ setTimeout(() => reject(new Error("timeout")), 5000),
182
+ ),
183
+ ]);
184
+ if (result === null) throw new Error("null response");
185
+ } catch {
186
+ // Hub unreachable — try to re-register
187
+ log("Hub heartbeat failed — attempting re-registration...");
188
+ hubClient.call_raw("register_adapter", {
189
+ name: "{{ADAPTER_NAME}}",
190
+ socketPath: ADAPTER_SOCKET_PATH,
191
+ }).then(() => {
192
+ log("Re-registered with AIBroker hub daemon");
193
+ }).catch((err) => {
194
+ log(`Hub re-registration failed: ${err instanceof Error ? err.message : String(err)}`);
195
+ });
196
+ }
197
+ }, HUB_HEARTBEAT_INTERVAL);
198
+
199
+ // Keep process alive
200
+ await new Promise(() => {});
201
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * ipc-server.ts — Adapter-side IPC server for {{DISPLAY_NAME}}
3
+ *
4
+ * Registers the handlers that the AIBroker hub (and MCP tools) can call
5
+ * on this adapter's Unix Domain Socket.
6
+ *
7
+ * Required handlers (called by the hub):
8
+ * deliver — hub pushes a BrokerMessage to this adapter for outbound delivery
9
+ * health — hub polls adapter health
10
+ * connection_status — returns the upstream connection status string
11
+ *
12
+ * Optional handlers (called by MCP tools or the hub):
13
+ * send — send a text message
14
+ * send_voice — send a voice note (OGG Opus path)
15
+ * send_file — send a file attachment
16
+ * login — trigger a new login / QR pairing flow
17
+ * status — human-readable status summary
18
+ */
19
+
20
+ import { IpcServer, log } from "aibroker";
21
+ import type { BrokerMessage } from "aibroker";
22
+
23
+ import { ADAPTER_SOCKET_PATH, adapterStats, connectionStatus, getLastMessageAgo } from "./state.js";
24
+ import { sendText, sendVoice, sendFile } from "./send.js";
25
+
26
+ let triggerLoginFn: (() => Promise<string>) | null = null;
27
+
28
+ /**
29
+ * Create, register handlers on, and start the adapter IPC server.
30
+ *
31
+ * @param triggerLogin - Async function that starts a fresh login flow and
32
+ * returns a human-readable status string.
33
+ * @returns The IpcServer instance (already started).
34
+ */
35
+ export function startIpcServer(triggerLogin: () => Promise<string>): IpcServer {
36
+ triggerLoginFn = triggerLogin;
37
+
38
+ const server = new IpcServer(ADAPTER_SOCKET_PATH);
39
+
40
+ // ── Hub-required handlers ──────────────────────────────────────────────────
41
+
42
+ /**
43
+ * deliver — Hub pushes a BrokerMessage for outbound delivery.
44
+ *
45
+ * The hub calls this when it has routed a message to this adapter.
46
+ * Inspect message.type to decide how to deliver:
47
+ * "text" → sendText
48
+ * "voice" → sendVoice
49
+ * "file" → sendFile
50
+ * "command" → run through local command handler
51
+ */
52
+ server.on("deliver", async (req) => {
53
+ const { message } = req.params as { message: BrokerMessage };
54
+ if (!message || !message.type) {
55
+ return { ok: false, error: "Invalid BrokerMessage: type is required" };
56
+ }
57
+
58
+ log(`[{{ADAPTER_NAME}}] deliver: type=${message.type} source=${message.source}`);
59
+
60
+ try {
61
+ const payload = message.payload ?? {};
62
+ const recipient = typeof payload.recipient === "string" ? payload.recipient : undefined;
63
+
64
+ switch (message.type) {
65
+ case "text": {
66
+ const text = typeof payload.text === "string" ? payload.text : String(payload.text ?? "");
67
+ await sendText(text, recipient);
68
+ break;
69
+ }
70
+ case "voice": {
71
+ // Hub sends pre-encoded audio as base64 buffer, or audioPath as fallback
72
+ const voiceB64 = typeof payload.buffer === "string" ? payload.buffer : "";
73
+ if (voiceB64) {
74
+ // Pre-encoded audio from hub — write to temp file and send
75
+ const { writeFileSync } = await import("node:fs");
76
+ const { join } = await import("node:path");
77
+ const { tmpdir } = await import("node:os");
78
+ const tmpPath = join(tmpdir(), `{{ADAPTER_NAME}}-voice-${Date.now()}.ogg`);
79
+ writeFileSync(tmpPath, Buffer.from(voiceB64, "base64"));
80
+ await sendVoice(tmpPath, recipient);
81
+ } else {
82
+ const audioPath = typeof payload.audioPath === "string" ? payload.audioPath : null;
83
+ if (!audioPath) return { ok: false, error: "deliver(voice): buffer or audioPath is required" };
84
+ await sendVoice(audioPath, recipient);
85
+ }
86
+ break;
87
+ }
88
+ case "file": {
89
+ const filePath = typeof payload.filePath === "string" ? payload.filePath : null;
90
+ if (!filePath) return { ok: false, error: "deliver(file): filePath is required" };
91
+ const caption = typeof payload.caption === "string" ? payload.caption : undefined;
92
+ const mimetype = typeof payload.mimetype === "string" ? payload.mimetype : undefined;
93
+ await sendFile(filePath, caption, mimetype, recipient);
94
+ break;
95
+ }
96
+ case "image": {
97
+ // Image delivery — payload contains base64-encoded buffer and optional caption
98
+ const buffer = typeof payload.buffer === "string"
99
+ ? Buffer.from(payload.buffer, "base64")
100
+ : null;
101
+ if (!buffer) return { ok: false, error: "deliver(image): buffer is required" };
102
+ const imgCaption = typeof payload.text === "string" ? payload.text : undefined;
103
+ // TODO: implement image sending via your SDK. For now, fall back to caption text.
104
+ if (imgCaption) await sendText(imgCaption, recipient);
105
+ log(`[{{ADAPTER_NAME}}] deliver(image): ${buffer.length} bytes — implement image sending`);
106
+ break;
107
+ }
108
+ case "command": {
109
+ // Commands from the hub (e.g. routed from another adapter) — deliver as text
110
+ const text = typeof payload.text === "string" ? payload.text : String(payload.text ?? "");
111
+ await sendText(text, recipient);
112
+ break;
113
+ }
114
+ default:
115
+ log(`[{{ADAPTER_NAME}}] deliver: unhandled message type "${message.type}"`);
116
+ return { ok: false, error: `Unhandled message type: ${message.type}` };
117
+ }
118
+
119
+ return { ok: true, result: { delivered: true } };
120
+ } catch (err) {
121
+ const msg = err instanceof Error ? err.message : String(err);
122
+ log(`[{{ADAPTER_NAME}}] deliver error: ${msg}`);
123
+ adapterStats.errors++;
124
+ return { ok: false, error: msg };
125
+ }
126
+ });
127
+
128
+ /**
129
+ * health — Hub polls this to determine adapter liveness.
130
+ *
131
+ * Returns AdapterHealth-compatible shape.
132
+ */
133
+ server.on("health", async (_req) => {
134
+ const isConnected = connectionStatus === "connected";
135
+ return {
136
+ ok: true,
137
+ result: {
138
+ status: isConnected ? "ok" : connectionStatus === "connecting" ? "degraded" : "down",
139
+ connectionStatus,
140
+ stats: {
141
+ messagesReceived: adapterStats.messagesReceived,
142
+ messagesSent: adapterStats.messagesSent,
143
+ errors: adapterStats.errors,
144
+ },
145
+ lastMessageAgo: getLastMessageAgo(),
146
+ },
147
+ };
148
+ });
149
+
150
+ /**
151
+ * connection_status — Returns the upstream connection status as a string.
152
+ */
153
+ server.on("connection_status", async (_req) => {
154
+ return {
155
+ ok: true,
156
+ result: { status: connectionStatus },
157
+ };
158
+ });
159
+
160
+ // ── Optional outbound handlers ─────────────────────────────────────────────
161
+
162
+ /**
163
+ * send — Send a text message.
164
+ */
165
+ server.on("send", async (req) => {
166
+ const { message, recipient } = req.params as { message?: string; recipient?: string };
167
+ if (!message) return { ok: false, error: "message is required" };
168
+ try {
169
+ await sendText(message, recipient);
170
+ return { ok: true, result: { sent: true } };
171
+ } catch (err) {
172
+ adapterStats.errors++;
173
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
174
+ }
175
+ });
176
+
177
+ /**
178
+ * send_voice — Send a voice note from a file path (OGG Opus).
179
+ */
180
+ server.on("send_voice", async (req) => {
181
+ const { audioPath, recipient } = req.params as { audioPath?: string; recipient?: string };
182
+ if (!audioPath) return { ok: false, error: "audioPath is required" };
183
+ try {
184
+ await sendVoice(audioPath, recipient);
185
+ return { ok: true, result: { sent: true } };
186
+ } catch (err) {
187
+ adapterStats.errors++;
188
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
189
+ }
190
+ });
191
+
192
+ /**
193
+ * send_file — Send a file attachment.
194
+ */
195
+ server.on("send_file", async (req) => {
196
+ const { filePath, caption, mimetype, recipient } = req.params as {
197
+ filePath?: string;
198
+ caption?: string;
199
+ mimetype?: string;
200
+ recipient?: string;
201
+ };
202
+ if (!filePath) return { ok: false, error: "filePath is required" };
203
+ try {
204
+ await sendFile(filePath, caption, mimetype, recipient);
205
+ return { ok: true, result: { sent: true } };
206
+ } catch (err) {
207
+ adapterStats.errors++;
208
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
209
+ }
210
+ });
211
+
212
+ /**
213
+ * login — Trigger a fresh login / QR pairing flow.
214
+ */
215
+ server.on("login", async (_req) => {
216
+ if (!triggerLoginFn) return { ok: false, error: "Login not available" };
217
+ try {
218
+ const result = await triggerLoginFn();
219
+ return { ok: true, result: { message: result } };
220
+ } catch (err) {
221
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
222
+ }
223
+ });
224
+
225
+ /**
226
+ * status — Human-readable adapter status summary.
227
+ */
228
+ server.on("status", async (_req) => {
229
+ const agoMs = getLastMessageAgo();
230
+ const agoStr = agoMs !== null
231
+ ? `${Math.round(agoMs / 1000)}s ago`
232
+ : "never";
233
+ return {
234
+ ok: true,
235
+ result: {
236
+ adapter: "{{ADAPTER_NAME}}",
237
+ displayName: "{{DISPLAY_NAME}}",
238
+ connectionStatus,
239
+ messagesReceived: adapterStats.messagesReceived,
240
+ messagesSent: adapterStats.messagesSent,
241
+ errors: adapterStats.errors,
242
+ lastMessageAgo: agoStr,
243
+ },
244
+ };
245
+ });
246
+
247
+ server.start();
248
+ log(`[{{ADAPTER_NAME}}] IPC server started on ${ADAPTER_SOCKET_PATH}`);
249
+ return server;
250
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * send.ts — Outbound message delivery for {{DISPLAY_NAME}}
3
+ *
4
+ * These functions are called by the IPC server handlers when the hub (or an
5
+ * MCP tool) wants to send a message to the upstream service.
6
+ *
7
+ * TODO: Replace each stub with real delivery logic using your SDK.
8
+ */
9
+
10
+ import { adapterStats } from "./state.js";
11
+
12
+ // ── Text ─────────────────────────────────────────────────────────────────────
13
+
14
+ /**
15
+ * Send a plain-text message to the upstream service.
16
+ *
17
+ * @param text The message body.
18
+ * @param recipient Optional channel/user/room identifier. Defaults to self-chat.
19
+ */
20
+ export async function sendText(text: string, recipient?: string): Promise<void> {
21
+ // TODO: deliver text via your SDK
22
+ console.log(`[{{ADAPTER_NAME}}] sendText stub — text="${text.slice(0, 60)}" recipient=${recipient ?? "(default)"}`);
23
+ adapterStats.messagesSent++;
24
+ }
25
+
26
+ // ── Voice ────────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Send an audio file as a voice note.
30
+ *
31
+ * The file at audioPath is an OGG/Opus buffer produced by Kokoro TTS.
32
+ * Most messaging platforms accept OGG Opus for voice notes.
33
+ *
34
+ * @param audioPath Path to the OGG Opus audio file on disk.
35
+ * @param recipient Optional channel/user/room identifier.
36
+ */
37
+ export async function sendVoice(audioPath: string, recipient?: string): Promise<void> {
38
+ // TODO: upload and send audio file via your SDK
39
+ console.log(`[{{ADAPTER_NAME}}] sendVoice stub — path="${audioPath}" recipient=${recipient ?? "(default)"}`);
40
+ adapterStats.messagesSent++;
41
+ }
42
+
43
+ // ── File ─────────────────────────────────────────────────────────────────────
44
+
45
+ /**
46
+ * Send an arbitrary file as a document attachment.
47
+ *
48
+ * @param filePath Path to the file on disk.
49
+ * @param caption Optional caption shown beneath the attachment.
50
+ * @param mimetype Optional MIME type override. Detected automatically if omitted.
51
+ * @param recipient Optional channel/user/room identifier.
52
+ */
53
+ export async function sendFile(
54
+ filePath: string,
55
+ caption?: string,
56
+ mimetype?: string,
57
+ recipient?: string,
58
+ ): Promise<void> {
59
+ // TODO: upload and send file via your SDK
60
+ console.log(`[{{ADAPTER_NAME}}] sendFile stub — path="${filePath}" caption="${caption ?? ""}" mime="${mimetype ?? "auto"}" recipient=${recipient ?? "(default)"}`);
61
+ adapterStats.messagesSent++;
62
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * state.ts — {{DISPLAY_NAME}} adapter local state
3
+ *
4
+ * Tracks connection status, per-run statistics, and the socket path
5
+ * for the adapter's IPC server. All fields are module-level singletons
6
+ * written by the watcher and read by the IPC server handlers.
7
+ */
8
+
9
+ import type { AdapterConnectionStatus } from "aibroker";
10
+
11
+ // ── Connection tracking ──────────────────────────────────────────────────────
12
+
13
+ /** Current upstream connection status. */
14
+ export let connectionStatus: AdapterConnectionStatus = "disconnected";
15
+
16
+ export function setConnectionStatus(s: AdapterConnectionStatus): void {
17
+ connectionStatus = s;
18
+ }
19
+
20
+ // ── Per-run statistics ───────────────────────────────────────────────────────
21
+
22
+ /** Statistics reset on each watcher restart (not persisted). */
23
+ export const adapterStats = {
24
+ messagesReceived: 0,
25
+ messagesSent: 0,
26
+ errors: 0,
27
+ lastMessageAt: null as number | null,
28
+ };
29
+
30
+ // ── Socket path ──────────────────────────────────────────────────────────────
31
+
32
+ /** Unix Domain Socket path for the adapter's IPC server. */
33
+ export const ADAPTER_SOCKET_PATH = "/tmp/{{ADAPTER_NAME}}-watcher.sock";
34
+
35
+ /** Timestamp of the last incoming message (epoch ms). */
36
+ export function getLastMessageAgo(): number | null {
37
+ if (adapterStats.lastMessageAt === null) return null;
38
+ return Date.now() - adapterStats.lastMessageAt;
39
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src"]
14
+ }