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,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon/core-handlers.ts — Hub-level IPC handlers.
|
|
3
|
+
*
|
|
4
|
+
* Registers the core methods that any adapter or MCP client can call
|
|
5
|
+
* on the hub socket. Transport-specific methods (send, contacts, history)
|
|
6
|
+
* are NOT registered here — adapters handle those on their own sockets.
|
|
7
|
+
*
|
|
8
|
+
* Phase 1 methods:
|
|
9
|
+
* register_adapter — adapter announces itself to the hub
|
|
10
|
+
* unregister_adapter
|
|
11
|
+
* adapter_list — list connected adapters
|
|
12
|
+
* sessions — list hybrid sessions
|
|
13
|
+
* switch — switch active session
|
|
14
|
+
* end_session — end a hybrid session
|
|
15
|
+
* broadcast_status — push status to all PAILot clients
|
|
16
|
+
* voice_config — get/set TTS config
|
|
17
|
+
* status — hub health/connection summary
|
|
18
|
+
*/
|
|
19
|
+
import { createBrokerMessage } from "../types/broker.js";
|
|
20
|
+
import { broadcastStatus, broadcastVoice, broadcastImage, broadcastText } from "../adapters/pailot/gateway.js";
|
|
21
|
+
import { WatcherClient } from "../ipc/client.js";
|
|
22
|
+
import { saveVoiceConfig } from "../core/persistence.js";
|
|
23
|
+
import { voiceConfig, setVoiceConfig, activeItermSessionId } from "../core/state.js";
|
|
24
|
+
import { splitIntoChunks } from "../adapters/kokoro/media.js";
|
|
25
|
+
import { stripMarkdown } from "../core/markdown.js";
|
|
26
|
+
import { listPaiProjects, findPaiProject, launchPaiProject } from "./pai-projects.js";
|
|
27
|
+
import { readSessionContent, readAllSessionContent } from "./session-content.js";
|
|
28
|
+
import { statusCache, hashContent } from "../core/status-cache.js";
|
|
29
|
+
import { readFileSync } from "node:fs";
|
|
30
|
+
import { join, dirname } from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
const __dirname = dirname(__filename);
|
|
34
|
+
function getPackageVersion() {
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
|
|
37
|
+
return pkg.version ?? "unknown";
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return "unknown";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const HUB_VERSION = getPackageVersion();
|
|
44
|
+
export function registerCoreHandlers(server, registry, _apiBackend, manager) {
|
|
45
|
+
server.on("register_adapter", async (req) => {
|
|
46
|
+
const { name, socketPath } = req.params;
|
|
47
|
+
if (!name || !socketPath)
|
|
48
|
+
return { ok: false, error: "name and socketPath required" };
|
|
49
|
+
registry.register({ name, socketPath, registeredAt: Date.now() });
|
|
50
|
+
return { ok: true, result: { registered: true } };
|
|
51
|
+
});
|
|
52
|
+
server.on("unregister_adapter", async (req) => {
|
|
53
|
+
const { name } = req.params;
|
|
54
|
+
registry.unregister(name);
|
|
55
|
+
return { ok: true, result: { unregistered: true } };
|
|
56
|
+
});
|
|
57
|
+
server.on("adapter_list", async (_req) => {
|
|
58
|
+
return { ok: true, result: { adapters: registry.list() } };
|
|
59
|
+
});
|
|
60
|
+
server.on("sessions", async (_req) => {
|
|
61
|
+
const sessions = manager.listSessions().map((s, i) => ({
|
|
62
|
+
index: i + 1,
|
|
63
|
+
name: s.name,
|
|
64
|
+
kind: s.kind,
|
|
65
|
+
active: manager.activeSession?.id === s.id,
|
|
66
|
+
}));
|
|
67
|
+
return { ok: true, result: { sessions } };
|
|
68
|
+
});
|
|
69
|
+
server.on("switch", async (req) => {
|
|
70
|
+
const { target } = req.params;
|
|
71
|
+
const index = typeof target === "number" ? target : parseInt(String(target), 10);
|
|
72
|
+
const session = manager.switchToIndex(index);
|
|
73
|
+
if (!session)
|
|
74
|
+
return { ok: false, error: `Session ${target} not found` };
|
|
75
|
+
return { ok: true, result: { switched: true, name: session.name } };
|
|
76
|
+
});
|
|
77
|
+
server.on("end_session", async (req) => {
|
|
78
|
+
const { target } = req.params;
|
|
79
|
+
const index = typeof target === "number" ? target : parseInt(String(target), 10);
|
|
80
|
+
const session = manager.removeByIndex(index);
|
|
81
|
+
if (!session)
|
|
82
|
+
return { ok: false, error: `Session ${target} not found` };
|
|
83
|
+
return { ok: true, result: { ended: true, name: session.name } };
|
|
84
|
+
});
|
|
85
|
+
server.on("broadcast_status", async (req) => {
|
|
86
|
+
const { status } = req.params;
|
|
87
|
+
broadcastStatus(status);
|
|
88
|
+
return { ok: true, result: { status } };
|
|
89
|
+
});
|
|
90
|
+
server.on("voice_config", async (req) => {
|
|
91
|
+
const { action, ...updates } = req.params;
|
|
92
|
+
if (action === "get") {
|
|
93
|
+
return { ok: true, result: { config: voiceConfig } };
|
|
94
|
+
}
|
|
95
|
+
const merged = { ...voiceConfig, ...updates };
|
|
96
|
+
setVoiceConfig(merged);
|
|
97
|
+
saveVoiceConfig(merged);
|
|
98
|
+
return { ok: true, result: { success: true, config: merged } };
|
|
99
|
+
});
|
|
100
|
+
server.on("status", async (_req) => {
|
|
101
|
+
const adapterHealth = {};
|
|
102
|
+
for (const [name, health] of registry.getAllHealth()) {
|
|
103
|
+
adapterHealth[name] = health;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
result: {
|
|
108
|
+
version: HUB_VERSION,
|
|
109
|
+
adapters: registry.list().map(a => a.name),
|
|
110
|
+
activeSessions: manager.listSessions().length,
|
|
111
|
+
activeSession: manager.activeSession?.name ?? null,
|
|
112
|
+
adapterHealth,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
/**
|
|
117
|
+
* ping — Lightweight heartbeat for adapter health checks.
|
|
118
|
+
* Returns immediately with the hub uptime. No side effects.
|
|
119
|
+
*/
|
|
120
|
+
server.on("ping", async (_req) => {
|
|
121
|
+
return { ok: true, result: { pong: true, uptime: process.uptime() } };
|
|
122
|
+
});
|
|
123
|
+
// ── TTS / Voice Pipeline ──
|
|
124
|
+
/**
|
|
125
|
+
* tts — Convert text to voice note and deliver to requesting adapter.
|
|
126
|
+
*
|
|
127
|
+
* The hub generates the audio (Kokoro TTS) and sends the OGG buffer
|
|
128
|
+
* back to the adapter that requested it (via the "source" field).
|
|
129
|
+
*/
|
|
130
|
+
server.on("tts", async (req) => {
|
|
131
|
+
const { text, voice, source, recipient } = req.params;
|
|
132
|
+
if (!text)
|
|
133
|
+
return { ok: false, error: "text is required" };
|
|
134
|
+
const resolvedVoice = voice ?? voiceConfig.defaultVoice;
|
|
135
|
+
try {
|
|
136
|
+
const { textToVoiceNote } = await import("../adapters/kokoro/tts.js");
|
|
137
|
+
const audioBuffer = await textToVoiceNote(text, resolvedVoice);
|
|
138
|
+
// If a source adapter is specified, deliver the voice note through it
|
|
139
|
+
if (source) {
|
|
140
|
+
const adapter = registry.get(source);
|
|
141
|
+
if (adapter) {
|
|
142
|
+
const msg = createBrokerMessage("hub", "voice", {
|
|
143
|
+
buffer: audioBuffer.toString("base64"),
|
|
144
|
+
text: text.slice(0, 100),
|
|
145
|
+
recipient,
|
|
146
|
+
metadata: { voice: resolvedVoice },
|
|
147
|
+
});
|
|
148
|
+
await registry.deliverToAdapter(adapter, msg);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Also broadcast to PAILot clients
|
|
152
|
+
broadcastVoice(audioBuffer, text.slice(0, 200));
|
|
153
|
+
return { ok: true, result: { generated: true, voice: resolvedVoice, bytes: audioBuffer.length } };
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
return { ok: false, error: `TTS failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
/**
|
|
160
|
+
* speak — Play text locally via afplay (no network delivery).
|
|
161
|
+
*/
|
|
162
|
+
server.on("speak", async (req) => {
|
|
163
|
+
const { text, voice } = req.params;
|
|
164
|
+
if (!text)
|
|
165
|
+
return { ok: false, error: "text is required" };
|
|
166
|
+
try {
|
|
167
|
+
const { speakLocally } = await import("../adapters/kokoro/tts.js");
|
|
168
|
+
await speakLocally(text, voice ?? voiceConfig.defaultVoice);
|
|
169
|
+
return { ok: true, result: { speaking: true } };
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
return { ok: false, error: `Speak failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
/**
|
|
176
|
+
* dictate — Record from mic and transcribe via Whisper.
|
|
177
|
+
*/
|
|
178
|
+
server.on("dictate", async (req) => {
|
|
179
|
+
const { maxDuration } = req.params;
|
|
180
|
+
try {
|
|
181
|
+
const { recordFromMic, transcribeLocalAudio } = await import("../adapters/iterm/dictation.js");
|
|
182
|
+
const audioPath = await recordFromMic(maxDuration ?? 30);
|
|
183
|
+
const text = await transcribeLocalAudio(audioPath);
|
|
184
|
+
return { ok: true, result: { text, audioPath } };
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
return { ok: false, error: `Dictation failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
/**
|
|
191
|
+
* transcribe — Transcribe an audio buffer via Whisper.
|
|
192
|
+
*/
|
|
193
|
+
server.on("transcribe", async (req) => {
|
|
194
|
+
const { audioBase64, mimetype } = req.params;
|
|
195
|
+
if (!audioBase64)
|
|
196
|
+
return { ok: false, error: "audioBase64 is required" };
|
|
197
|
+
try {
|
|
198
|
+
const { transcribeAudio, mimetypeToExt } = await import("../adapters/kokoro/media.js");
|
|
199
|
+
const { writeFileSync, unlinkSync } = await import("node:fs");
|
|
200
|
+
const { tmpdir } = await import("node:os");
|
|
201
|
+
const { join } = await import("node:path");
|
|
202
|
+
const ext = mimetypeToExt(mimetype ?? "audio/ogg");
|
|
203
|
+
const tmpPath = join(tmpdir(), `aibroker-transcribe-${Date.now()}.${ext}`);
|
|
204
|
+
writeFileSync(tmpPath, Buffer.from(audioBase64, "base64"));
|
|
205
|
+
try {
|
|
206
|
+
const text = await transcribeAudio(tmpPath);
|
|
207
|
+
return { ok: true, result: { text } };
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
try {
|
|
211
|
+
unlinkSync(tmpPath);
|
|
212
|
+
}
|
|
213
|
+
catch { /* ignore */ }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
return { ok: false, error: `Transcription failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
/**
|
|
221
|
+
* list_voices — List available TTS voices.
|
|
222
|
+
*/
|
|
223
|
+
server.on("list_voices", async (_req) => {
|
|
224
|
+
const { listVoices } = await import("../adapters/kokoro/tts.js");
|
|
225
|
+
return { ok: true, result: { voices: listVoices() } };
|
|
226
|
+
});
|
|
227
|
+
// ── PAI Named Sessions ──
|
|
228
|
+
server.on("pai_projects", async (_req) => {
|
|
229
|
+
const projects = await listPaiProjects();
|
|
230
|
+
return { ok: true, result: { projects } };
|
|
231
|
+
});
|
|
232
|
+
server.on("pai_find", async (req) => {
|
|
233
|
+
const { name } = req.params;
|
|
234
|
+
if (!name)
|
|
235
|
+
return { ok: false, error: "name is required" };
|
|
236
|
+
const project = await findPaiProject(name);
|
|
237
|
+
if (!project)
|
|
238
|
+
return { ok: false, error: `Project "${name}" not found` };
|
|
239
|
+
return { ok: true, result: { project } };
|
|
240
|
+
});
|
|
241
|
+
server.on("pai_launch", async (req) => {
|
|
242
|
+
const { name } = req.params;
|
|
243
|
+
if (!name)
|
|
244
|
+
return { ok: false, error: "name is required" };
|
|
245
|
+
let itermSessionId;
|
|
246
|
+
let sessionId;
|
|
247
|
+
try {
|
|
248
|
+
({ itermSessionId, sessionId } = await launchPaiProject(name));
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
252
|
+
}
|
|
253
|
+
// Register the visual session with HybridSessionManager
|
|
254
|
+
const project = await findPaiProject(name);
|
|
255
|
+
const displayName = project?.displayName || project?.name || name;
|
|
256
|
+
manager.registerVisualSession(displayName, project?.rootPath ?? "", itermSessionId);
|
|
257
|
+
return { ok: true, result: { itermSessionId, sessionId, name } };
|
|
258
|
+
});
|
|
259
|
+
// ── Phase 6: Image Generation ──
|
|
260
|
+
/**
|
|
261
|
+
* generate_image — Generate an image from a text prompt.
|
|
262
|
+
*
|
|
263
|
+
* Optionally sends an "on it..." ack and delivers the generated image
|
|
264
|
+
* back to the requesting adapter.
|
|
265
|
+
*/
|
|
266
|
+
server.on("generate_image", async (req) => {
|
|
267
|
+
const { prompt, source, recipient, ack, width, height } = req.params;
|
|
268
|
+
if (!prompt)
|
|
269
|
+
return { ok: false, error: "prompt is required" };
|
|
270
|
+
// Send "on it..." ack to the requesting adapter
|
|
271
|
+
if (ack !== false && source) {
|
|
272
|
+
const adapter = registry.get(source);
|
|
273
|
+
if (adapter) {
|
|
274
|
+
const ackMsg = createBrokerMessage("hub", "text", {
|
|
275
|
+
text: "On it... generating your image.",
|
|
276
|
+
recipient,
|
|
277
|
+
});
|
|
278
|
+
registry.deliverToAdapter(adapter, ackMsg).catch(() => { });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const { generateImage } = await import("./image-gen.js");
|
|
283
|
+
const result = await generateImage({ prompt, width, height });
|
|
284
|
+
// Deliver image to requesting adapter
|
|
285
|
+
if (source && result.images.length > 0) {
|
|
286
|
+
const adapter = registry.get(source);
|
|
287
|
+
if (adapter) {
|
|
288
|
+
const imgMsg = createBrokerMessage("hub", "image", {
|
|
289
|
+
buffer: result.images[0].toString("base64"),
|
|
290
|
+
caption: prompt.slice(0, 200),
|
|
291
|
+
recipient,
|
|
292
|
+
metadata: { model: result.model, durationMs: result.durationMs },
|
|
293
|
+
});
|
|
294
|
+
await registry.deliverToAdapter(adapter, imgMsg);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Also broadcast to PAILot clients
|
|
298
|
+
if (result.images.length > 0) {
|
|
299
|
+
broadcastImage(result.images[0], prompt.slice(0, 200));
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
result: {
|
|
304
|
+
generated: true,
|
|
305
|
+
model: result.model,
|
|
306
|
+
durationMs: result.durationMs,
|
|
307
|
+
imageCount: result.images.length,
|
|
308
|
+
bytes: result.images.reduce((s, b) => s + b.length, 0),
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
return { ok: false, error: `Image generation failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
// ── Phase 7: Vision & Understanding ──
|
|
317
|
+
/**
|
|
318
|
+
* analyze_image — Save image and deliver to active Claude Code session.
|
|
319
|
+
*
|
|
320
|
+
* The image is saved to ~/.aibroker/media/ and the path is routed through
|
|
321
|
+
* the command handler to the active iTerm2 session. Claude Code in that
|
|
322
|
+
* session reads the image with its Read tool (covered by Max plan).
|
|
323
|
+
*/
|
|
324
|
+
server.on("analyze_image", async (req) => {
|
|
325
|
+
const { imageBase64, mimetype, prompt, source, recipient } = req.params;
|
|
326
|
+
if (!imageBase64)
|
|
327
|
+
return { ok: false, error: "imageBase64 is required" };
|
|
328
|
+
try {
|
|
329
|
+
const { saveReceivedImage } = await import("./vision.js");
|
|
330
|
+
const imageBuffer = Buffer.from(imageBase64, "base64");
|
|
331
|
+
const { path, sizeBytes } = saveReceivedImage(imageBuffer, mimetype);
|
|
332
|
+
// Route through the command handler → active iTerm2 session
|
|
333
|
+
const userPrompt = prompt ?? "Analyze this image.";
|
|
334
|
+
const messageText = `[Image: ${path}] ${userPrompt}`;
|
|
335
|
+
const sourceAdapter = source ? registry.get(source) : undefined;
|
|
336
|
+
const msg = createBrokerMessage(source ?? "hub", "command", {
|
|
337
|
+
text: messageText,
|
|
338
|
+
recipient,
|
|
339
|
+
});
|
|
340
|
+
await registry.route(msg);
|
|
341
|
+
return { ok: true, result: { saved: true, path, sizeBytes } };
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
return { ok: false, error: `Image analysis failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
/**
|
|
348
|
+
* analyze_video — Analyze a video using Gemini 2.0 Flash (free tier).
|
|
349
|
+
*
|
|
350
|
+
* Video can't be read by Claude Code's Read tool, so we use Gemini's
|
|
351
|
+
* native video understanding and deliver the text result back.
|
|
352
|
+
*/
|
|
353
|
+
server.on("analyze_video", async (req) => {
|
|
354
|
+
const { videoBase64, mimetype, prompt, source, recipient } = req.params;
|
|
355
|
+
if (!videoBase64)
|
|
356
|
+
return { ok: false, error: "videoBase64 is required" };
|
|
357
|
+
// Ack — video analysis takes longer
|
|
358
|
+
if (source) {
|
|
359
|
+
const adapter = registry.get(source);
|
|
360
|
+
if (adapter) {
|
|
361
|
+
const ackMsg = createBrokerMessage("hub", "text", {
|
|
362
|
+
text: "Analyzing your video...",
|
|
363
|
+
recipient,
|
|
364
|
+
});
|
|
365
|
+
registry.deliverToAdapter(adapter, ackMsg).catch(() => { });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
const { analyzeVideo, saveReceivedVideo } = await import("./vision.js");
|
|
370
|
+
const videoBuffer = Buffer.from(videoBase64, "base64");
|
|
371
|
+
const { path } = saveReceivedVideo(videoBuffer, mimetype);
|
|
372
|
+
const result = await analyzeVideo({ videoBuffer, mimetype, prompt });
|
|
373
|
+
// Deliver the analysis text to the active session
|
|
374
|
+
if (result.text) {
|
|
375
|
+
const analysisText = `[Video analysis of ${path}]\n\n${result.text}`;
|
|
376
|
+
const msg = createBrokerMessage(source ?? "hub", "command", {
|
|
377
|
+
text: analysisText,
|
|
378
|
+
recipient,
|
|
379
|
+
});
|
|
380
|
+
await registry.route(msg);
|
|
381
|
+
}
|
|
382
|
+
return { ok: true, result: { text: result.text, model: result.model, durationMs: result.durationMs, path } };
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
return { ok: false, error: `Video analysis failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
// ── Session Orchestration (Phase 1) ──
|
|
389
|
+
/**
|
|
390
|
+
* session_content — Read raw terminal content from iTerm2 sessions.
|
|
391
|
+
*
|
|
392
|
+
* If sessionId is provided, reads that specific session.
|
|
393
|
+
* If omitted, reads all sessions. Returns raw content + busy/idle flag
|
|
394
|
+
* + whether content has changed since last probe (via content hash).
|
|
395
|
+
*/
|
|
396
|
+
server.on("session_content", async (req) => {
|
|
397
|
+
const { sessionId, lines } = req.params;
|
|
398
|
+
const lineCount = lines ?? 100;
|
|
399
|
+
if (sessionId) {
|
|
400
|
+
const content = readSessionContent(sessionId, lineCount);
|
|
401
|
+
if (!content)
|
|
402
|
+
return { ok: false, error: `Session ${sessionId} not found in iTerm2` };
|
|
403
|
+
const contentHash = hashContent(content.content);
|
|
404
|
+
const changed = statusCache.hasChanged(sessionId, contentHash);
|
|
405
|
+
const cached = statusCache.get(sessionId);
|
|
406
|
+
if (!changed) {
|
|
407
|
+
statusCache.touch(sessionId);
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
ok: true,
|
|
411
|
+
result: {
|
|
412
|
+
session: {
|
|
413
|
+
...content,
|
|
414
|
+
contentHash,
|
|
415
|
+
changed,
|
|
416
|
+
cachedSummary: cached?.summary ?? null,
|
|
417
|
+
cachedAt: cached?.timestamp ?? null,
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
// All sessions
|
|
423
|
+
const contents = readAllSessionContent(lineCount);
|
|
424
|
+
const sessions = contents.map((c) => {
|
|
425
|
+
const contentHash = hashContent(c.content);
|
|
426
|
+
const changed = statusCache.hasChanged(c.sessionId, contentHash);
|
|
427
|
+
const cached = statusCache.get(c.sessionId);
|
|
428
|
+
if (!changed)
|
|
429
|
+
statusCache.touch(c.sessionId);
|
|
430
|
+
return {
|
|
431
|
+
...c,
|
|
432
|
+
contentHash,
|
|
433
|
+
changed,
|
|
434
|
+
cachedSummary: cached?.summary ?? null,
|
|
435
|
+
cachedAt: cached?.timestamp ?? null,
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
return { ok: true, result: { sessions } };
|
|
439
|
+
});
|
|
440
|
+
/**
|
|
441
|
+
* cache_status — Store a parsed summary for a session.
|
|
442
|
+
*
|
|
443
|
+
* Called by the requesting session's AI after parsing raw terminal content.
|
|
444
|
+
* The summary is cached with the content hash so future probes can skip parsing
|
|
445
|
+
* if content hasn't changed.
|
|
446
|
+
*/
|
|
447
|
+
server.on("cache_status", async (req) => {
|
|
448
|
+
const { sessionId, sessionName, summary, contentHash, state } = req.params;
|
|
449
|
+
if (!sessionId)
|
|
450
|
+
return { ok: false, error: "sessionId is required" };
|
|
451
|
+
if (!summary)
|
|
452
|
+
return { ok: false, error: "summary is required" };
|
|
453
|
+
statusCache.set(sessionId, {
|
|
454
|
+
sessionId,
|
|
455
|
+
sessionName: sessionName ?? sessionId,
|
|
456
|
+
timestamp: Date.now(),
|
|
457
|
+
state: state ?? "idle",
|
|
458
|
+
summary,
|
|
459
|
+
contentHash: contentHash ?? "",
|
|
460
|
+
lastProbeAt: Date.now(),
|
|
461
|
+
});
|
|
462
|
+
return { ok: true, result: { cached: true, sessionId } };
|
|
463
|
+
});
|
|
464
|
+
/**
|
|
465
|
+
* get_cached_status — Retrieve cached session summaries without re-probing.
|
|
466
|
+
*
|
|
467
|
+
* If sessionId is provided, returns that session's cached snapshot.
|
|
468
|
+
* If omitted, returns all cached snapshots.
|
|
469
|
+
*/
|
|
470
|
+
server.on("get_cached_status", async (req) => {
|
|
471
|
+
const { sessionId } = req.params;
|
|
472
|
+
if (sessionId) {
|
|
473
|
+
const cached = statusCache.get(sessionId);
|
|
474
|
+
if (!cached)
|
|
475
|
+
return { ok: true, result: { snapshot: null } };
|
|
476
|
+
return { ok: true, result: { snapshot: cached } };
|
|
477
|
+
}
|
|
478
|
+
return { ok: true, result: { snapshots: statusCache.getAll() } };
|
|
479
|
+
});
|
|
480
|
+
// ── Unified MCP Support ──
|
|
481
|
+
/**
|
|
482
|
+
* adapter_call — Proxy an IPC call to a named adapter through the hub.
|
|
483
|
+
* The unified MCP server uses this to reach adapter-specific methods
|
|
484
|
+
* (send, receive, contacts, history, etc.) without knowing socket paths.
|
|
485
|
+
*/
|
|
486
|
+
server.on("adapter_call", async (req) => {
|
|
487
|
+
const { adapter, method, params } = req.params;
|
|
488
|
+
if (!adapter)
|
|
489
|
+
return { ok: false, error: "adapter is required" };
|
|
490
|
+
if (!method)
|
|
491
|
+
return { ok: false, error: "method is required" };
|
|
492
|
+
const desc = registry.get(adapter);
|
|
493
|
+
if (!desc) {
|
|
494
|
+
return { ok: false, error: `Adapter '${adapter}' not registered. Is the ${adapter} daemon running?` };
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
const client = new WatcherClient(desc.socketPath);
|
|
498
|
+
const forwardParams = { ...(params ?? {}), sessionId: req.sessionId };
|
|
499
|
+
if (req.itermSessionId)
|
|
500
|
+
forwardParams.itermSessionId = req.itermSessionId;
|
|
501
|
+
const result = await client.call_raw(method, forwardParams);
|
|
502
|
+
return { ok: true, result };
|
|
503
|
+
}
|
|
504
|
+
catch (e) {
|
|
505
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
506
|
+
return { ok: false, error: `adapter_call to ${adapter}.${method} failed: ${msg}` };
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
/**
|
|
510
|
+
* pailot_send — Send text or voice to PAILot app clients via WS gateway.
|
|
511
|
+
*/
|
|
512
|
+
server.on("pailot_send", async (req) => {
|
|
513
|
+
const { text, voice, voiceName, sessionId: callerSessionId } = req.params;
|
|
514
|
+
if (!text)
|
|
515
|
+
return { ok: false, error: "text is required" };
|
|
516
|
+
// MCP server may not have ITERM_SESSION_ID — fall back to daemon's active session
|
|
517
|
+
const sessionId = callerSessionId || activeItermSessionId || undefined;
|
|
518
|
+
try {
|
|
519
|
+
if (voice) {
|
|
520
|
+
const { textToVoiceNote } = await import("../adapters/kokoro/tts.js");
|
|
521
|
+
const resolvedVoice = voiceName ?? voiceConfig.defaultVoice;
|
|
522
|
+
const plainText = stripMarkdown(text);
|
|
523
|
+
const chunks = splitIntoChunks(plainText);
|
|
524
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
525
|
+
if (i > 0)
|
|
526
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
527
|
+
const audioBuffer = await textToVoiceNote(chunks[i], resolvedVoice);
|
|
528
|
+
// First chunk gets the full original text as transcript;
|
|
529
|
+
// subsequent chunks get empty transcript (audio only)
|
|
530
|
+
const transcript = i === 0 ? plainText : "";
|
|
531
|
+
await broadcastVoice(audioBuffer, transcript, sessionId);
|
|
532
|
+
}
|
|
533
|
+
return { ok: true, result: { sent: true, chunks: chunks.length } };
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
broadcastText(text, sessionId);
|
|
537
|
+
}
|
|
538
|
+
return { ok: true, result: { sent: true } };
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
return { ok: false, error: `pailot_send failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
/**
|
|
545
|
+
* pailot_receive — Drain the PAILot message queue.
|
|
546
|
+
* Currently proxied to whazaa adapter's receive with from='pailot'.
|
|
547
|
+
*/
|
|
548
|
+
server.on("pailot_receive", async (req) => {
|
|
549
|
+
const adapterName = registry.get("whazaa") ? "whazaa" : "telex";
|
|
550
|
+
const desc = registry.get(adapterName);
|
|
551
|
+
if (!desc)
|
|
552
|
+
return { ok: true, result: { messages: [] } };
|
|
553
|
+
try {
|
|
554
|
+
const client = new WatcherClient(desc.socketPath);
|
|
555
|
+
const result = await client.call_raw("receive", {
|
|
556
|
+
from: "pailot",
|
|
557
|
+
sessionId: req.sessionId,
|
|
558
|
+
});
|
|
559
|
+
return { ok: true, result };
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
return { ok: true, result: { messages: [] } };
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
/**
|
|
566
|
+
* rename — Rename session in hub + forward to all adapters.
|
|
567
|
+
*/
|
|
568
|
+
server.on("rename", async (req) => {
|
|
569
|
+
const { name } = req.params;
|
|
570
|
+
if (!name)
|
|
571
|
+
return { ok: false, error: "name is required" };
|
|
572
|
+
// Update in hub's session manager
|
|
573
|
+
const session = manager.activeSession;
|
|
574
|
+
if (session)
|
|
575
|
+
manager.updateName(session.id, name);
|
|
576
|
+
// Forward to all adapters (best effort)
|
|
577
|
+
for (const adapter of registry.list()) {
|
|
578
|
+
try {
|
|
579
|
+
const client = new WatcherClient(adapter.socketPath);
|
|
580
|
+
await client.call_raw("rename", { name, sessionId: req.sessionId });
|
|
581
|
+
}
|
|
582
|
+
catch { /* best effort */ }
|
|
583
|
+
}
|
|
584
|
+
return { ok: true, result: { success: true, name } };
|
|
585
|
+
});
|
|
586
|
+
/**
|
|
587
|
+
* discover — Proxy to first available adapter for iTerm2 session scan.
|
|
588
|
+
*/
|
|
589
|
+
server.on("discover", async (req) => {
|
|
590
|
+
const adapters = registry.list();
|
|
591
|
+
if (adapters.length === 0)
|
|
592
|
+
return { ok: false, error: "No adapters registered" };
|
|
593
|
+
try {
|
|
594
|
+
const client = new WatcherClient(adapters[0].socketPath);
|
|
595
|
+
const result = await client.call_raw("discover", { sessionId: req.sessionId });
|
|
596
|
+
return { ok: true, result };
|
|
597
|
+
}
|
|
598
|
+
catch (e) {
|
|
599
|
+
return { ok: false, error: `discover failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
/**
|
|
603
|
+
* command — Execute a slash command through the hub command handler.
|
|
604
|
+
*/
|
|
605
|
+
server.on("command", async (req) => {
|
|
606
|
+
const { text } = req.params;
|
|
607
|
+
if (!text)
|
|
608
|
+
return { ok: false, error: "text is required" };
|
|
609
|
+
// Try the hub's command handler first
|
|
610
|
+
const adapters = registry.list();
|
|
611
|
+
if (adapters.length > 0) {
|
|
612
|
+
try {
|
|
613
|
+
const client = new WatcherClient(adapters[0].socketPath);
|
|
614
|
+
const result = await client.call_raw("command", { text, sessionId: req.sessionId });
|
|
615
|
+
return { ok: true, result };
|
|
616
|
+
}
|
|
617
|
+
catch { /* fall through */ }
|
|
618
|
+
}
|
|
619
|
+
return { ok: true, result: { executed: true, command: text } };
|
|
620
|
+
});
|
|
621
|
+
// ── Phase 2: Message Routing ──
|
|
622
|
+
/**
|
|
623
|
+
* route_message — Adapters send messages to the hub for routing.
|
|
624
|
+
*
|
|
625
|
+
* The hub inspects the BrokerMessage target and type, then delivers
|
|
626
|
+
* to the appropriate adapter via its IPC socket ("deliver" method).
|
|
627
|
+
*/
|
|
628
|
+
server.on("route_message", async (req) => {
|
|
629
|
+
const { message } = req.params;
|
|
630
|
+
if (!message || !message.source || !message.type) {
|
|
631
|
+
return { ok: false, error: "Invalid BrokerMessage: source and type are required" };
|
|
632
|
+
}
|
|
633
|
+
const result = await registry.route(message);
|
|
634
|
+
if (result.ok) {
|
|
635
|
+
return { ok: true, result: result };
|
|
636
|
+
}
|
|
637
|
+
return { ok: false, error: result.error ?? "Routing failed" };
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
//# sourceMappingURL=core-handlers.js.map
|