feature/pi-refactor
1#!/usr/bin/env node
2/**
3 * Daneel - XMPP Research Bot (Pi Edition)
4 * Main entry point using Pi libraries
5 */
6
7import { XmppClient } from "./xmpp/client.js";
8import { XmppAgent } from "./pi/agent-wrapper.js";
9import { getDefaultModel } from "./pi/config.js";
10import { bareJid } from "./xmpp/types.js";
11import { statusTool } from "./pi/tools/status.js";
12import { webSearchTool } from "./pi/tools/websearch.js";
13import { researchTool } from "./pi/tools/research.js";
14import { withLogging } from "./pi/tools/logging.js";
15import * as os from "os";
16
17interface Config {
18 xmpp: {
19 jid: string;
20 password: string;
21 ownerJid: string;
22 };
23 paths: {
24 dataDir: string;
25 inboxPath: string;
26 };
27 debug: boolean;
28}
29
30function loadConfig(): Config {
31 const jid = process.env.DANEEL_XMPP_JID;
32 const password = process.env.DANEEL_XMPP_PASSWORD;
33 const ownerJid = process.env.DANEEL_OWNER_JID;
34
35 if (!jid || !password || !ownerJid) {
36 console.error("Missing required environment variables:");
37 if (!jid) console.error(" DANEEL_XMPP_JID");
38 if (!password) console.error(" DANEEL_XMPP_PASSWORD");
39 if (!ownerJid) console.error(" DANEEL_OWNER_JID");
40 process.exit(1);
41 }
42
43 const dataDir = process.env.DANEEL_DATA_DIR || "./data";
44 const inboxPath = process.env.DANEEL_INBOX_PATH ||
45 `${os.homedir()}/desktop/org/inbox.org`;
46 const debug = process.env.DANEEL_DEBUG === "true";
47
48 return {
49 xmpp: { jid, password, ownerJid },
50 paths: { dataDir, inboxPath },
51 debug,
52 };
53}
54
55async function main() {
56 console.log("Daneel - XMPP Research Bot");
57 console.log("===========================\n");
58
59 const config = loadConfig();
60
61 // Get default model
62 const defaultModel = getDefaultModel();
63 console.log(`Default model: ${defaultModel.provider}/${defaultModel.id}`);
64
65 // Available tools (wrap with logging if debug enabled)
66 const rawTools = [statusTool, webSearchTool, researchTool];
67 const tools = rawTools.map(tool => withLogging(tool, config.debug));
68 console.log(`Tools available: ${rawTools.map(t => t.name).join(", ")}`);
69
70 // Map of JID -> XmppAgent
71 const agents = new Map<string, XmppAgent>();
72
73 function getOrCreateAgent(jid: string): XmppAgent {
74 const bare = bareJid(jid);
75 let agent = agents.get(bare);
76
77 if (!agent) {
78 if (config.debug) {
79 console.log(`[Main] Creating new agent for JID: ${bare}`);
80 }
81 agent = new XmppAgent(bare, defaultModel, tools, config.debug);
82 agents.set(bare, agent);
83 }
84
85 return agent;
86 }
87
88 // Create XMPP client
89 const xmpp = new XmppClient(config.xmpp);
90
91 // Set up message handler
92 xmpp.onMessage(async (message) => {
93 const timestamp = new Date().toISOString();
94 const from = bareJid(message.from);
95 const preview = message.body.slice(0, 50);
96 const requestStartTime = Date.now();
97
98 console.log(`[${timestamp}] ← Message from ${from}: ${preview}${message.body.length > 50 ? "..." : ""}`);
99
100 try {
101 const agent = getOrCreateAgent(message.from);
102 const response = await agent.processMessage(message.body);
103
104 await xmpp.sendMessage(message.from, response);
105
106 const elapsed = Date.now() - requestStartTime;
107 const timestamp2 = new Date().toISOString();
108
109 if (config.debug) {
110 console.log(`[${timestamp2}] → Response sent to ${from} (${response.length} chars, ${elapsed}ms total)`);
111 } else {
112 console.log(`[${timestamp2}] → Response sent to ${from} (${elapsed}ms)`);
113 }
114 } catch (error) {
115 const elapsed = Date.now() - requestStartTime;
116 console.error(`[${timestamp}] ✗ Error after ${elapsed}ms:`, error);
117
118 // Check if it's an API key error
119 const errorMsg = error instanceof Error ? error.message : "Unknown error";
120
121 let userMessage = `Sorry, I encountered an error: ${errorMsg}`;
122
123 if (errorMsg.includes("No API key for provider")) {
124 const provider = errorMsg.match(/provider: (\w+)/)?.[1];
125 userMessage = `I tried to use ${provider} but don't have an API key configured. I'm working with limited capabilities right now. Try asking simpler questions or use slash commands like /status.`;
126 }
127
128 try {
129 await xmpp.sendMessage(message.from, userMessage);
130 console.log(`[${timestamp}] → Error message sent to ${from}`);
131 } catch (sendError) {
132 console.error(`[${timestamp}] ✗ Failed to send error message:`, sendError);
133 }
134 }
135 });
136
137 // Graceful shutdown
138 const shutdown = async (signal: string) => {
139 console.log(`\nReceived ${signal}, shutting down gracefully...`);
140
141 // TODO: Save sessions to disk (Phase 9)
142
143 await xmpp.stop();
144 console.log("XMPP client stopped");
145
146 process.exit(0);
147 };
148
149 process.on("SIGINT", () => shutdown("SIGINT"));
150 process.on("SIGTERM", () => shutdown("SIGTERM"));
151
152 // Start XMPP client
153 console.log("\nConfiguration:");
154 console.log(` Bot JID: ${config.xmpp.jid}`);
155 console.log(` Owner JID: ${config.xmpp.ownerJid}`);
156 console.log(` Data dir: ${config.paths.dataDir}`);
157 console.log(` Inbox path: ${config.paths.inboxPath}`);
158 console.log(` Debug mode: ${config.debug}`);
159 console.log();
160 console.log("Connecting to XMPP server...");
161
162 await xmpp.start();
163
164 console.log("Connected! Waiting for messages...\n");
165}
166
167// Run main function
168main().catch((error) => {
169 console.error("Fatal error:", error);
170 process.exit(1);
171});