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.
- package/README.md +164 -4
- package/dist/adapters/iterm/core.d.ts +2 -0
- package/dist/adapters/iterm/core.d.ts.map +1 -1
- package/dist/adapters/iterm/core.js +13 -5
- package/dist/adapters/iterm/core.js.map +1 -1
- package/dist/adapters/iterm/iterm2-api.d.ts +20 -0
- package/dist/adapters/iterm/iterm2-api.d.ts.map +1 -0
- package/dist/adapters/iterm/iterm2-api.js +244 -0
- package/dist/adapters/iterm/iterm2-api.js.map +1 -0
- package/dist/adapters/iterm/sessions.d.ts.map +1 -1
- package/dist/adapters/iterm/sessions.js +3 -2
- package/dist/adapters/iterm/sessions.js.map +1 -1
- package/dist/adapters/kokoro/media.d.ts +2 -1
- package/dist/adapters/kokoro/media.d.ts.map +1 -1
- package/dist/adapters/kokoro/media.js +53 -5
- package/dist/adapters/kokoro/media.js.map +1 -1
- package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).d.ts +49 -0
- package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).d.ts.map +1 -0
- package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).js +632 -0
- package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).js.map +1 -0
- package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-59).js +632 -0
- package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).d.ts +49 -0
- package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
- package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).js +614 -0
- package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
- package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-46).js +614 -0
- package/dist/adapters/pailot/gateway.d.ts +48 -0
- package/dist/adapters/pailot/gateway.d.ts (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
- package/dist/adapters/pailot/gateway.d.ts (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
- package/dist/adapters/pailot/gateway.d.ts.map +1 -0
- package/dist/adapters/pailot/gateway.js +828 -0
- package/dist/adapters/pailot/gateway.js (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
- package/dist/adapters/pailot/gateway.js (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
- package/dist/adapters/pailot/gateway.js.map +1 -0
- package/dist/backend/api.d.ts +5 -1
- package/dist/backend/api.d.ts.map +1 -1
- package/dist/backend/api.js +74 -3
- package/dist/backend/api.js.map +1 -1
- package/dist/core/hybrid.d.ts +7 -0
- package/dist/core/hybrid.d.ts.map +1 -1
- package/dist/core/hybrid.js +33 -0
- package/dist/core/hybrid.js.map +1 -1
- package/dist/core/state.d.ts +3 -0
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/state.js +4 -0
- package/dist/core/state.js.map +1 -1
- package/dist/core/status-cache.d.ts +51 -0
- package/dist/core/status-cache.d.ts.map +1 -0
- package/dist/core/status-cache.js +62 -0
- package/dist/core/status-cache.js.map +1 -0
- package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).d.ts +63 -0
- package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
- package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).js +229 -0
- package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
- package/dist/daemon/adapter-registry.d.ts +63 -0
- package/dist/daemon/adapter-registry.d.ts.map +1 -0
- package/dist/daemon/adapter-registry.js +240 -0
- package/dist/daemon/adapter-registry.js.map +1 -0
- package/dist/daemon/cli.d.ts +14 -0
- package/dist/daemon/cli.d.ts.map +1 -0
- package/dist/daemon/cli.js +150 -0
- package/dist/daemon/cli.js.map +1 -0
- package/dist/daemon/command-context.d.ts +24 -0
- package/dist/daemon/command-context.d.ts.map +1 -0
- package/dist/daemon/command-context.js +13 -0
- package/dist/daemon/command-context.js.map +1 -0
- package/dist/daemon/commands.d.ts +22 -0
- package/dist/daemon/commands.d.ts.map +1 -0
- package/dist/daemon/commands.js +632 -0
- package/dist/daemon/commands.js.map +1 -0
- package/dist/daemon/core-handlers.d.ts +24 -0
- package/dist/daemon/core-handlers.d.ts.map +1 -0
- package/dist/daemon/core-handlers.js +640 -0
- package/dist/daemon/core-handlers.js.map +1 -0
- package/dist/daemon/create-adapter.d.ts +22 -0
- package/dist/daemon/create-adapter.d.ts.map +1 -0
- package/dist/daemon/create-adapter.js +153 -0
- package/dist/daemon/create-adapter.js.map +1 -0
- package/dist/daemon/image-gen.d.ts +28 -0
- package/dist/daemon/image-gen.d.ts.map +1 -0
- package/dist/daemon/image-gen.js +97 -0
- package/dist/daemon/image-gen.js.map +1 -0
- package/dist/daemon/index.d.ts +12 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +184 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/pai-projects.d.ts +68 -0
- package/dist/daemon/pai-projects.d.ts.map +1 -0
- package/dist/daemon/pai-projects.js +174 -0
- package/dist/daemon/pai-projects.js.map +1 -0
- package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).d.ts +12 -0
- package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).d.ts.map +1 -0
- package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).js +252 -0
- package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).js.map +1 -0
- package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-59).js +252 -0
- package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).d.ts +12 -0
- package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
- package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).js +240 -0
- package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
- package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-46).js +240 -0
- package/dist/daemon/screenshot.d.ts +12 -0
- package/dist/daemon/screenshot.d.ts (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
- package/dist/daemon/screenshot.d.ts (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
- package/dist/daemon/screenshot.d.ts.map +1 -0
- package/dist/daemon/screenshot.js +252 -0
- package/dist/daemon/screenshot.js (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
- package/dist/daemon/screenshot.js (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
- package/dist/daemon/screenshot.js.map +1 -0
- package/dist/daemon/session-content.d.ts +27 -0
- package/dist/daemon/session-content.d.ts.map +1 -0
- package/dist/daemon/session-content.js +76 -0
- package/dist/daemon/session-content.js.map +1 -0
- package/dist/daemon/vision.d.ts +46 -0
- package/dist/daemon/vision.d.ts.map +1 -0
- package/dist/daemon/vision.js +176 -0
- package/dist/daemon/vision.js.map +1 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/ipc/client.d.ts +4 -1
- package/dist/ipc/client.d.ts.map +1 -1
- package/dist/ipc/client.js +10 -1
- package/dist/ipc/client.js.map +1 -1
- package/dist/ipc/validate.d.ts +52 -0
- package/dist/ipc/validate.d.ts.map +1 -0
- package/dist/ipc/validate.js +129 -0
- package/dist/ipc/validate.js.map +1 -0
- package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).d.ts +23 -0
- package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).d.ts.map +1 -0
- package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).js +595 -0
- package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).js.map +1 -0
- package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-59).js +595 -0
- package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).d.ts +23 -0
- package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
- package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).js +592 -0
- package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
- package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-46).js +592 -0
- package/dist/mcp/index.d.ts +23 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +660 -0
- package/dist/mcp/index.js (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
- package/dist/mcp/index.js (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/adapter.d.ts +41 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +2 -0
- package/dist/types/adapter.js.map +1 -0
- package/dist/types/backend.d.ts +29 -1
- package/dist/types/backend.d.ts.map +1 -1
- package/dist/types/broker.d.ts +47 -0
- package/dist/types/broker.d.ts.map +1 -0
- package/dist/types/broker.js +21 -0
- package/dist/types/broker.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +12 -2
- package/templates/adapter/ONBOARDING_PROMPT.md +309 -0
- package/templates/adapter/README.md.tmpl +81 -0
- package/templates/adapter/package.json.tmpl +23 -0
- package/templates/adapter/src/watcher/cli.ts.tmpl +12 -0
- package/templates/adapter/src/watcher/commands.ts.tmpl +44 -0
- package/templates/adapter/src/watcher/connection.ts.tmpl +59 -0
- package/templates/adapter/src/watcher/index.ts.tmpl +201 -0
- package/templates/adapter/src/watcher/ipc-server.ts.tmpl +250 -0
- package/templates/adapter/src/watcher/send.ts.tmpl +62 -0
- package/templates/adapter/src/watcher/state.ts.tmpl +39 -0
- 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
|
+
}
|